Search code examples
javascriptgoogle-apps-scriptgmail

Reply to an email in Gmail with AppScript with changed recipients ends up in a new thread


I have an email in my mailbox and I want the AppScript program to reply to it with just me and a special google group as the recipients. The purpose of this is communication of the program with me as the program replies to the message once it has processed it with necessary details about the processing in the reply body. There might also be other recipients apart from me in the original message and I don't want the program to send the reply to them.

So I need to reply with a changed set of recipients. When I do it in the Gmail GUI it works just fine, I hit reply, change the recipients, send the message and the reply ends up in the original thread. However when I do it in the script the reply always ends up in a new thread. Originally I thought Gmail decides based on the subject of the email but it seems there's more to it (perhaps it has recently changed as I think it used to work that way).

I tried multitude of slightly different approached, one of them being:

var messageBody = "foo";
var newRecipients = "[email protected], [email protected]";
var messageToReplyTo = ...;
var advancedParams = {from : "[email protected]"};

var replyDraft = messageToReplyTo.createDraftReply(messageBody);
var replySubject = replyDraft.getMessage().getSubject();
var replyBody = replyDraft.getMessage().getBody();

replyDraft.update(newRecipients, replySubject, replyBody, advancedParams);

replyDraft.send();

Solution

  • There are a couple fun things you need to do in order to achieve this, but you can do it without too much trouble. You should definitely review the guide to Drafts.

    Per the API spec:

    In order to be part of a thread, a message or draft must meet the following criteria:

    1. The requested threadId must be specified on the Message or Draft.Message you supply with your request.
    2. The References and In-Reply-To headers must be set in compliance with the RFC 2822 standard.
    3. The Subject headers must match.

    To start, you need to get a reference to the draft you want to update. This is probably simplest by using GmailApp:

    const thread = /** get the thread somehow */;
    const newBody = /** your plaintext here */;
    const reply = thread.createDraftReply(newBody);
    

    The primary issue with Gmail & Drafts is that a Draft is an immutable message to server resources. If you change any of it, you change all of it. Thus, to change a header value such as the recipient address, you need to completely rebuild the message. This is why using the GmailApp methods to update a draft fail to maintain the existing thread information - you can't specify it as one of the advanced options for building the new message. Thus, you must use the Gmail REST API for this task:

    const rawMsg = Gmail.Users.Drafts.get("me", reply.getId(), {format: "raw"}).message;
    

    To update a draft, you need to supply an RFC 2822 formatted message encoded in base64. If you are comfortable converting the rich format message parts into such a valid string, by all means work with the non-raw format, as you have direct access to the headers in the message.payload.

    To work with the raw message, know that Apps Script casts the described base64 encoded string to a byte array in the above call. The leap is then to treat that byte array as string bytes, specifically, charCodes:

    const msg_string = rawMsg.raw.reduce(function (acc, b) { return acc + String.fromCharCode(b); }, "");
    console.log({message: "Converted byte[] to str", bytes: rawMsg.raw, str: msg_string});
    

    Once you have the message as a string, you can use regular expressions to update your desired headers:

    const pattern = /^To: .+$/m;
    var new_msg_string = msg_string.replace(pattern, "To: <....>");
    // new_msg_string += ....
    

    Since the Gmail API endpoint to update a Draft expects a base64 web-safe encoded string, you can compute that:

    const encoded_msg = Utilities.base64EncodeWebSafe(new_msg_string);
    

    And the only remaining bit is to perform the call (and/or send the updated draft).

    const resource = {
      id: <draft id>, // e.g. reply.getId()
      message: {
        threadId: <thread id>, // e.g. thread.getId()
        raw: encoded_msg
      }
    }
    const resp = Gmail.Users.Drafts.update(resource, "me", reply.getId());
    const sent_msg = Gmail.Users.Drafts.send({id: resp.id}, "me");
    console.log({message: "Sent the draft", msg: sent_msg});
    

    I don't claim that the handling of the Byte array returned from the Message.raw property is 100% correct, only that it seems correct and didn't result in any errors in the test message I sent. There may also be an easier approach, as the Apps Script service has a Drafts.update endpoint which accepts a Blob input and I have not investigated how one would use that.