Search code examples
google-apps-scripttriggersgmailadd-on

Getting draft body on Gmail Addon


I have a similar problem as this Question and that subject bug is already solved, but I need to get the "Body" from the message the user is writing, and also the "messageId".

I have one contextualTrigger but it doesn’t log on STACKDRIVER

function onGmailMessageOpen(e) {
  console.log(e);
  
  // Get the ID of the message the user has open.
  var messageId = e.gmail.messageId;
}
"gmail": {
      "contextualTriggers": [
        {
          "unconditional": {},
          "onTriggerFunction": "onGmailMessageOpen"
        }
      ],

Is there any way to get the "body" and "messageId" of current composing message, regardless if its a new draft or a reply, or is this a limitation of the addon?


Solution

  • TL;DR

    1. You are fusing two different triggers into one.
    2. Compose UI event object does not have a body field.
    3. Compose UI event object should not have a subject field (but has).

    This is not a bug

    This behaviour is how event objects for triggers firing in the context of the compose UI were supposed to work. In the documentation, there is no mention of either subject or body field (despite subject now being available de facto, probably as a result of the feature request mentioned in the Q&A you referenced).

    Event object structure

    Presently, gmail resource can only have the following properties:

    | Property      | Type     | Always present?     |
    | ------------- | -------- | ------------------- |
    | accessToken   | string   | Yes                 |
    | bccRecipients | string[] | disabled by default |
    | ccRecipients  | string[] | disabled by default |
    | messageId     | string   | Yes                 |
    | threadId      | string   | Yes                 |
    | toRecipients  | string[] | disabled by default |
    

    However, this event object structure is specific to the message UI and is not constructed in full in the compose UI context.

    Compose UI event object

    The compose UI trigger specified in the composeTrigger manifest field does not have access to the open message metadata. Given the METADATA scope is present, the event object looks like this (if the subject is empty, it will be missing from the resource):

    {
      commonEventObject: {
        platform: 'WEB',
        hostApp: 'GMAIL'
      },
      gmail: {
        subject: '12345'
      },
      clientPlatform: 'web',
      draftMetadata: {
        toRecipients: [],
        subject: '12345',
        bccRecipients: [],
        ccRecipients: []
      },
      hostApp: 'gmail'
    }
    

    Now, try building a Card and add an actionable widget to it (i.e. a TextButton):

    const onComposeAction = (e) => {
        const builder = CardService.newCardBuilder();
    
        const section = CardService.newCardSection();
    
        const action = CardService.newAction();
        action.setFunctionName("handleButtonClick"); //<-- callback name to test event object;
        
        const widget = CardService.newTextButton();
        widget.setText("Test Event Object");
        widget.setOnClickAction(action);
    
        section.addWidget(widget);
    
        builder.addSection(section);
    
        return builder.build();
    };
    

    Upon triggering the action, if you log the event object, you will see that it looks pretty similar to the previous one with action event object properties attached:

    {
      hostApp: 'gmail',
      formInputs: {}, //<-- multi-value inputs
      formInput: {}, //<-- single-value inputs
      draftMetadata: {
        subject: '12345',
        ccRecipients: [],
        toRecipients: [],
        bccRecipients: []
      },
      gmail: {
        subject: '12345'
      },
      parameters: {}, //<-- parameters passed to builder
      clientPlatform: 'web',
      commonEventObject: {
        hostApp: 'GMAIL',
        platform: 'WEB'
      }
    }
    

    Note the absence of accessToken, threadId, and messageId properties - the trigger fires in the context of the currently open draft, not the open email.

    Message UI event object

    On the contrary, the message UI event object (the one constructed in response to opening an email in reading mode and passed to a function specified in onTriggerFunction manifest property) does contain the necessary metadata:

    {
      messageMetadata: {
        accessToken: 'token here',
        threadId: 'thread id here',
        messageId: 'message id here'
      },
      clientPlatform: 'web',
      gmail: {
        messageId: 'message id here',
        threadId: 'thread id here',
        accessToken: 'token here'
      },
      commonEventObject: {
        platform: 'WEB',
        hostApp: 'GMAIL'
      },
      hostApp: 'gmail'
    }
    

    Workaround

    A viable workaround is to use the getDraftMessages method and extract the first matching draft (reasonably assuming that in the meantime a full duplicate of the draft is not created). An example of such a utility would be:

    const listDraftGmailMessages = ({
        subject,
        toRecipients: to,
        ccRecipients: cc,
        bccRecipients: bcc
    } = {}) => {
    
        const drafts = GmailApp.getDraftMessages();
    
        return drafts.filter((draft) => {
            const s = draft.getSubject();
            const t = draft.getTo().split(",");
            const c = draft.getCc().split(",");
            const b = draft.getBcc().split(",");
    
            const sameSubj = subject ? s === subject : true;
            const sameTo = to ? t.every(r => to.includes(trimFrom(r))) : true;
            const sameCc = cc ? c.every(r => cc.includes(trimFrom(r))) : true;
            const sameBcc = bcc ? b.every(r => bcc.includes(trimFrom(r))) : true;
    
            return sameSubj && sameTo && sameCc && sameBcc;
        });
    };
    

    Note that getTo, getCc and getBcc all return recipients in the form of name <email>, so they have to be trimmed. A "good enough" utility trimFrom should do the trick:

    const trimFrom = (input) => {
        try {
            const regex = /<([-\w.]+@\w+(?:\.\w+)+)>/i;
            const [, email] = input.match(regex) || [input];
            return email || input;
        } catch (error) {
            console.warn(error);
            return input;
        }
    };
    

    After extracting the first matching draft, you can do with it as you wish (in your case, use the getBody method).