GmailApp – Add label to specific message, not the thread

You can do this using the GMail API instead. You need to enable the API for your project. In the editor, select Resources > Advanced Google services, then click on “These services must also be enabled in the Google Developers Console.” There, enable the GMail API.

modifyMessage

Example of use:

modifyMessage('me', messageId, ['stack','overflow']);       // add two labels
modifyMessage('me', messageId, null, ['stack','overflow']); // remove two labels

TL;DR The full code is available in this gist, and below in a hidden code snippet.

/**
 * Modify the Labels a Message is associated with.
 * Throws if unsuccessful.
 * see https://developers.google.com/gmail/api/v1/reference/users/messages/modify
 *
 * @param  {String} userId           User's email address. The special value 'me'
 *                                   can be used to indicate the authenticated user.
 * @param  {String} messageId        ID of Message to modify.
 * @param  {String[]} labelsToAdd    Array of Label names to add.
 * @param  {String[]} labelsToRemove Array of Label names to remove.
 *
 * @returns {Object}                 Users.messages resource, see reference. 
 */
function modifyMessage(userId, messageId, labelsToAdd, labelsToRemove) {
  labelsToAdd = labelsToAdd || [];
  labelsToRemove = labelsToRemove || [];
  // see https://developers.google.com/gmail/api/v1/reference/users/messages/modify
  var url="https://www.googleapis.com/gmail/v1/users/${userId}/messages/${id}/modify"
            .replace("${userId}","me")
            .replace("${id}", messageId );
  var headers = {
    Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
  };
  var addLabelIds = [];
  for (var i=0; i<labelsToAdd.length; i++) {
    addLabelIds[i] = getLabelId( labelsToAdd[i] );
  }
  var removeLabelIds = [];
  for (var i=0; i<labelsToRemove.length; i++) {
    removeLabelIds[i] = getLabelId( labelsToRemove[i], false );
  }
  var request = {
    'addLabelIds': addLabelIds,
    'removeLabelIds': removeLabelIds
  };
  var params = {
    method: "post",
    contentType: "application/json",
    headers: headers,
    payload: JSON.stringify(request),
    muteHttpExceptions: true
  };
  //var check = UrlFetchApp.getRequest(url, params); // for debugging
  var response = UrlFetchApp.fetch(url, params);

  var result = response.getResponseCode();
  if (result == '200') {  // OK
    return JSON.parse(response.getContentText());
  }
  else {
    // This is only needed when muteHttpExceptions == true
    var err = JSON.parse(response.getContentText());
    throw new Error( 'Error (' + result + ") " + err.error.message );
  }
}

/**
 * Get the Label ID for the given LabelName. If Label isn't found, it will be created
 * depending on the state of ok2Create.
 * Throws if unsuccessful.
 * See https://developers.google.com/gmail/api/v1/reference/users/messages/modify.
 *
 * @param {String}   labelName        Name of label to retrieve ID for.
 * @param {Boolean}  ok2Create        (optional) Set true if a label should be created when not found.
 *                                    Default is true.
 *
 * @returns {String}                  ID of Label, or null if not found or created.
 */
function getLabelId( labelName, ok2Create ) {
  if (typeof ok2Create == 'undefined') ok2Create = true;
  
  var id = null;
  // see https://developers.google.com/gmail/api/v1/reference/users/labels/list
  var url="https://www.googleapis.com/gmail/v1/users/${userId}/labels"
            .replace("${userId}","me")  // The user's email address. The special value me can be used to indicate the authenticated user.
  var headers = {
    Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
  };
  var params = {
    method: "get",
    contentType: "application/json",
    headers: headers,
    muteHttpExceptions: true
  };
  
  //var check = UrlFetchApp.getRequest(url, params); // for debugging
  var response = UrlFetchApp.fetch(url, params);

  var result = response.getResponseCode();
  if (result == '200') {  // OK
    var labels = JSON.parse(response.getContentText()).labels;
    var found = false;
    for (var i=0; i<labels.length & !found; i++) {
      if (labels[i].name == labelName) {
        found = true;
        id = labels[i].id;
      }
    }
    if (!found && ok2Create) {
      id = createLabel( labelName );
    }
    return id;
  }
  else {
    // This is only needed when muteHttpExceptions == true
    var err = JSON.parse(response.getContentText());
    throw new Error( 'Error (' + result + ") " + err.error.message );
  }
}

/**
 * Create Label for given `labelName`.
 * Throws if unsuccessful.
 * See https://developers.google.com/gmail/api/v1/reference/users/messages/modify.
 *
 * @param {String}   labelName        Name of label to create
 *
 * @returns {String}                  ID of Label.
 */
function createLabel( labelName ) {
  var id = null;
  // see https://developers.google.com/gmail/api/v1/reference/users/labels/create
  var url="https://www.googleapis.com/gmail/v1/users/${userId}/labels"
            .replace("${userId}","me")  // The user's email address. The special value me can be used to indicate the authenticated user.
  var headers = {
    Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
  };
  var request = {
    'name': labelName
  };
  var params = {
    method: "post",
    contentType: "application/json",
    headers: headers,
    payload: JSON.stringify(request),
    muteHttpExceptions: true
  };
  
  //var check = UrlFetchApp.getRequest(url, params); // for debugging
  var response = UrlFetchApp.fetch(url, params);

  var result = response.getResponseCode();
  if (result == '200') {  // OK
    var label = JSON.parse(response.getContentText());
    id = label.id;
    return id;
  }
  else {
    // This is only needed when muteHttpExceptions == true
    var err = JSON.parse(response.getContentText());
    throw new Error( 'Error (' + result + ") " + err.error.message );
  }
}
/**
 * Modify the Labels a Message is associated with.
 * Throws if unsuccessful.
 * see https://developers.google.com/gmail/api/v1/reference/users/messages/modify
 *
 * @param  {String} userId           User's email address. The special value 'me'
 *                                   can be used to indicate the authenticated user.
 * @param  {String} messageId        ID of Message to modify.
 * @param  {String[]} labelsToAdd    Array of Label names to add.
 * @param  {String[]} labelsToRemove Array of Label names to remove.
 *
 * @returns {Object}                 Users.messages resource, see reference. 
 */
function modifyMessage(userId, messageId, labelsToAdd, labelsToRemove) {
  labelsToAdd = labelsToAdd || [];
  labelsToRemove = labelsToRemove || [];
  // see https://developers.google.com/gmail/api/v1/reference/users/messages/modify
  var url="https://www.googleapis.com/gmail/v1/users/${userId}/messages/${id}/modify"
            .replace("${userId}","me")
            .replace("${id}", messageId );

The GMail API is a RESTful API with more flexible and detailed access to Threads, Messages, Labels, Drafts, and History than is provided using the built-in GMailApp Service. From our script, we access the API by URLs – here, we start by filling in details in the base URL for the modify Message method.

  var headers = {
    Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
  };

In addition to enabling the API for our project, each request requires user authentication, which is done by embedding a Bearer Token in the command header.

Next, we will build the rest of the command parameters.

  var addLabelIds = [];
  for (var i=0; i<labelsToAdd.length; i++) {
    addLabelIds[i] = getLabelId( labelsToAdd[i] );
  }
  var removeLabelIds = [];
  for (i=0; i<labelsToRemove.length; i++) {
    removeLabelIds[i] = getLabelId( labelsToRemove[i], false );
  }
  var request = {
    'addLabelIds': addLabelIds,
    'removeLabelIds': removeLabelIds
  };

The specification for modify calls for a POST operation, with a payload containing lists of Label IDs. Those IDs are not available through the built-in GMail Service, so we’ll also use the GMail API for that – getLabelId() is another function similar to this one, specialized for retrieving the ID of labels by name, and creating new labels as needed. See the gist or snippet for that code.

  var params = {
    method: "post",
    contentType: "application/json",
    headers: headers,
    payload: JSON.stringify(request),
    muteHttpExceptions: true
  };
  //var check = UrlFetchApp.getRequest(url, params); // for debugging
  var response = UrlFetchApp.fetch(url, params);

Finally, we have assembled the params object for the command, with properties containing the headers with our authentication token, and payload with the command parameters, stringify-d for transport. Setting muteHttpExceptions: true squelches system-generated exceptions in case of failure, so that we can handle them ourselves.

I’ve commented out a call to UrlFetchApp.getRequest() – it’s handy for validating the content of a command before sending it.

The call to UrlFetchApp.fetch() transmits our command to the service running at the URL we built up top.

  var result = response.getResponseCode();
  if (result == '200') {  // OK
    return JSON.parse(response.getContentText());
  }

When a response is received, we first check the result of the command, which is in the form of an HTTP Status Code. A 200 OK tells us that we were successful, so we go ahead and pull our result out of the response. JSON.parse() turns the JSON string in the message into a JavaScript object, which we return to the caller.

  else {
    // This is only needed when muteHttpExceptions == true
    var err = JSON.parse(response.getContentText());
    throw new Error( 'Error (' + result + ") " + err.error.message );
  }
}

If we encountered an error, though, we throw our own exception. Depending on the details of a command, we may want to handle specific errors differently. For example, on this modify command, if we provide a Label ID that does not exist, we will receive an error:

Error: Error (400) Invalid label: “Invalid-label”

Leave a Comment