question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Please guide how to send email using gmail api via gsuit service account

See original GitHub issue

My goal is for my nodejs app to send mail to notify me if there were unhandled code exception on the server.

The code below is what I use for testing, which I modified gmail api quickstart code to use keyFile instead of oauth. However, I stuck with the error “The API returned an error: Error: Invalid Credentials”.

I use this auth code with spreadsheet api before and it was success. I also did enable Domain-wide Delegation and add the app to Google Admin access control with Trusted permission level.

Now I’m stuck and cannot find any nodejs example for gmail. Please help.

import googleapis from 'googleapis'

const {google} = googleapis

const auth = new google.auth.GoogleAuth({
    keyFile: './credentials/gmail-key.json',
    scopes: ['https://www.googleapis.com/auth/gmail.send,email,profile']
})

const gmail = google.gmail({version: 'v1', auth})

// console.log(gmail.users.messages.send)

listLabels(auth)

function listLabels(auth) {
    const gmail = google.gmail({version: 'v1', auth});
    gmail.users.labels.list({
// I use service account email
      userId: 'SERVICE_ACCOUNT_NAME@APP_NAME.iam.gserviceaccount.com',
    }, (err, res) => {
      if (err) return console.log('The API returned an error: ' + err);
      const labels = res.data.labels;
      if (labels.length) {
        console.log('Labels:');
        labels.forEach((label) => {
          console.log(`- ${label.name}`);
        });
      } else {
        console.log('No labels found.');
      }
    });
  }

Issue Analytics

  • State:open
  • Created 3 years ago
  • Comments:8 (2 by maintainers)

github_iconTop GitHub Comments

16reactions
Tallyraldcommented, Feb 22, 2021

I found a way to make this work in a relatively painless way so I’ll share in case someone needs it later. As you know, the docs are kind of useless for us nodejs folks so I’ll put some explaining here too.

First of all, create the service account via Cloud Console or other methods & give it domain-wide delegation. No need to give any other roles or permissions as the GMail API is not exactly a GCloud app. Then head over to the Google Workspaces Admin website (https://admin.google.com) and under security -> API setting (or something similarly named) put the delegation ClientId into the allowed apps list. This is where you set the appropriate scopes too.

Now consider the following (tl;dr at bottom):

// Warning lots of comments ahead
import { gmail_v1, google } from 'googleapis';
import { encode } from 'js-base64'; // btoa can probably be used too

const { google } = googleapis;

/** Sends a test email (to & from) a test email account */
public static async SendTestEmail() {
    // Create an auth solution. This could be any authentication method that the googleapis package allows
    const authClient = new google.auth.JWT({
        keyFile: 'path/to/keyFile.json', // Service account key file
        scopes: ['https://mail.google.com/'], // Very important to use appropriate scopes. This one gives full access (only if you gave this to the service account too)
        subject: 'email-address-to-impersonate@your-domain.com', // This is an email address that exists in your Google Workspaces account. The app will use this as 'me' during later execution.
    });

    // I'm not sure if this is necessary, but it visualizes that you are now logged in.
    await authClient.authorize();

    // Let's create a Gmail client via the googleapis package
    const gmail = new gmail_v1.Gmail({ auth: authClient });

    // The names are a bit confusing, but the important part is the users.messages.send is responsible for executing the send operation.
    // This means a single email, not batch sending
    const result = await gmail.users.messages.send({
        auth: authClient, // Pass the auth object created above
        requestBody: {
            // I'm fairly certain that the raw property is the most sure-fire way of telling the API what you want to send
            // This is actually a whole email, not just the body, see below
            raw: this.MakeEmail('email-to-send-to@target-domain.com', 'email-to-send-from@your-domain.com', 'Subject string', 'Email body string'),
        },
        userId: 'me', // Using me will set the authenticated user (in the auth object) as the requester
    });

    return result;
}

/** Creates a very basic email structure
 * @param to Email address to send to
 * @param from Email address to put as sender
 * @param subject The subject of the email [warn: encoding, see comment]
 * @param message The body of the email
 */
private static MakeEmail(to: string, from: string, subject: string, message: string) {
    // OK, so here's the magic
    // The array is used only to make this easier to understand, everything is concatenated at the end
    // Set the headers first, then the recipient(s), sender & subject, and finally the message itself
    const str = [
        'Content-Type: text/plain; charset="UTF-8"\n', // Setting the content type as UTF-8 makes sure that the body is interpreted as such by Google
        'to: ', to,'\n',
        'from: ', from,'\n',
        // Here's the trick: by telling the interpreter that the string is base64 encoded UTF-8 string, you can send non-7bit-ASCII characters in the subject
        // I'm not sure why this is so not intuitive (probably historical/compatibility reasons),
        // but you need to make sure the encoding of the file, the server environment & everything else matches what you specify here
        'subject: =?utf-8?B?', encode(subject, true),'?=\n\n', // Encoding is base64 with URL safe settings - just in case you want a URL in the subject (pls no, doesn't make sense)
        message, // The message body can be whatever you want. Parse templates, write simple text, do HTML magic or whatever you like - just use the correct content type header
    ].join('');

    const encodedMail = encode(str, true); // Base64 encode using URL safe settings

    return encodedMail;
}

tl;dr:

  • Authenticate an account email address to impersonate
  • Compose email body however you want (and make sure the content type header reflects what you’ve put there)
  • Concatenate email headers, recipient(s), sender, subject and body into a single string
  • Base64 encode the whole thing
  • Send it to the API

I haven’t used attachments yet. If or when I do, I’ll probably update this.

6reactions
sqrrrlcommented, Nov 4, 2020

There’s not, but something our tech writers are looking at improving.

Most Workspace APIs expect to be called as an end-user, not a service account. Support for services accounts is the exception, not the rule, and there’s only a handful of APIs where it’s appropriate (e.g. Drive, though still discouraged.) Gmail and Calendar specifically do not allow it.

They all support domain-wide delegation where a service account is used to impersonate an end-user. But that’s not what this code example is doing. It’s using the service account identity itself which isn’t allowed here. To do delegation, the credentials need to be scoped to the target user by setting the sub claim in the token. Looks like for this client that means creating the JWT client directly (https://github.com/googleapis/google-auth-library-nodejs/blob/master/src/auth/jwtclient.ts) and setting the subject arg to the user’s email address. Some of the other libraries have convenience methods for doing this (similar to createScoped(scopes) – createDelegated(user)) and that could be useful to add here.

The other change would be using the user email (or the ‘me’ alias which just means whoever the effective user is) for the userId parameter in the API request.

To summarize:

  • Workspace APIs should (almost) always be called as an end-user, not a service account
  • Service accounts are useful for admin-managed apps that need to impersonate users in a domain without their explicit consent (effective user for the credential is still an end-user though, consistent with the first point.)
  • Node.js client could make that a little easier with convenience methods to get a delegated credential
Read more comments on GitHub >

github_iconTop Results From Across the Web

Sending Email | Gmail - Google Developers
There are two ways to send email using the Gmail API: You can send it directly using the messages.send method. You can send...
Read more >
GMail API - Can I send email using the Service Account?
The Gmail API is for Gmail users and service accounts are just for doing ... If you want to send the email from...
Read more >
Sending emails programmatically with Gmail API and Python
Authorize the service account to send emails · Go to your G Suite domain's Admin console. · Select Security from the list of...
Read more >
How to Send Email in WordPress using the Gmail SMTP Server
Step by step guide on how to use the Gmail SMTP servers to send emails in WordPress. Gmail SMTP plugin helps fix the...
Read more >
How to Send and Read Emails with Gmail API | Mailtrap Blog
Step 1: Create a project at Google API Console · Step 2: Enable Gmail API · Step 3: Credentials and authentication with OAuth...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found