Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to send an email with attachment using Gmail API in Node.js?

I'm new to Node.js and I'm trying to create a mail box using the Gmail API everything works fine except uploading an attachment in email. I found examples with Java, Python and C#, yet I can't find any documentation with node about it. Any tips would be very much appreciated.

Here is my code:

function makeBody(to, from, subject, message) {
    var str = ["Content-Type: multipart/mixed; charset=\"UTF-8\"\n",
        "MIME-Version: 1.0\n",
        "Content-Transfer-Encoding: 7bit\n",
        "to: ", to, "\n",
        "from: ", from, "\n",
        "subject: ", subject, "\n\n",
        message,
        file
    ].join('');

    var encodedMail = new Buffer(str).toString("base64").replace(/\+/g, '-').replace(/\//g, '_');

    return encodedMail;
}

function sendMessage(auth) {
    var raw = makeBody(tap, 'me', response.subject, response.content, response.files);
    gmail.users.messages.send({
        auth: auth,
        userId: 'me',
        resource: {
            raw: raw
        }
    }, function (err, response) {
        if (err) {
            console.log('Error  ' + err);
            return;
        }

        if (response) {
            res.sendFile(__dirname + '/boite.html')
        }
    });
}
like image 959
moxched Avatar asked Feb 27 '18 21:02

moxched


2 Answers

This might go a little bit to late, anyway i will take the time in the case someone later wants an alternative.

The major problem with Moxched approach was that probably he needed to take a closer look at the MIME spec (which was a big pain for me) to understand better a few things that are necessary to send attachments.

From where i stand, to be able to use the gmail API to send attachments and a lot of other stuff you have to build the all request according to MIME spec, to do that you need to understand how things in MIME work including boundaries.

Joris approach works but ends up not using the nodeJS lib to send the email. The reason why he wasnt able to use the answer from the gmail-api-create-message-body package with the gmail API, is because for some reason this lib generates at the top of its MIME message the following:

'Content-Type: multipart/related; boundary="foo_bar_baz"',
`In-Reply-To: [email protected]`,
`References: `,
`From: [email protected]`,
`Subject: SUBJECT`,
`MIME-Version: 1.0`,
'',
`--foo_bar_baz`,
`Content-Type: application/json; charset="UTF-8"`,
'',
`{`,
`}`,
'',
`--foo_bar_baz`,
`Content-Type: message/rfc822`,
'',
...

For some reason the gmailAPI doesn't like this...

My suggestion is to understand a little bit better the MIME spec, a really easy way to that is to use some old reverse engineering, for that i suggest looking at the replies from gmail-api-create-message-body and mail-composer from nodemailer.

Using nodemailer/lib/mail-composer you will be able to generate the necessary MIME message according to the MIME spec with ease, it includes attachment support and all bunch of other stuff. The MIME messages generated are compatible with the Gmail API. I leave a working example, based on the examples of NodeJS docs, that sends an email with 2 attachments.

Hope this helps!

const fs = require('fs');
const path = require('path');
const readline = require('readline');
const {google} = require('googleapis');

const MailComposer = require('nodemailer/lib/mail-composer');

// If modifying these scopes, delete token.json.
const SCOPES = [
  'https://mail.google.com',
  'https://www.googleapis.com/auth/gmail.readonly'
];
const TOKEN_PATH = 'token.json';

// Load client secrets from a local file.
fs.readFile('credentials.json', (err, content) => {
  if (err) return console.log('Error loading client secret file:', err);
  // Authorize a client with credentials, then call the Gmail API.
  //authorize(JSON.parse(content), listLabels);

  authorize(JSON.parse(content), sendEmail);

});

/**
 * Create an OAuth2 client with the given credentials, and then execute the
 * given callback function.
 * @param {Object} credentials The authorization client credentials.
 * @param {function} callback The callback to call with the authorized client.
 */
function authorize(credentials, callback) {
  const {client_secret, client_id, redirect_uris} = credentials.installed;
  const oAuth2Client = new google.auth.OAuth2(
    client_id, client_secret, redirect_uris[0]);

  // Check if we have previously stored a token.
  fs.readFile(TOKEN_PATH, (err, token) => {
    if (err) return getNewToken(oAuth2Client, callback);
    oAuth2Client.setCredentials(JSON.parse(token));
    callback(oAuth2Client);
  });
}

/**
 * Get and store new token after prompting for user authorization, and then
 * execute the given callback with the authorized OAuth2 client.
 * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
 * @param {getEventsCallback} callback The callback for the authorized client.
 */
function getNewToken(oAuth2Client, callback) {
  const authUrl = oAuth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES,
  });
  console.log('Authorize this app by visiting this url:', authUrl);
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
  rl.question('Enter the code from that page here: ', (code) => {
    rl.close();
    oAuth2Client.getToken(code, (err, token) => {
      if (err) return console.error('Error retrieving access token', err);
      oAuth2Client.setCredentials(token);
      // Store the token to disk for later program executions
      fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
        if (err) return console.error(err);
        console.log('Token stored to', TOKEN_PATH);
      });
      callback(oAuth2Client);
    });
  });
}

function sendEmail(auth) {

  // ----------nodemailer test----------------------------------------------------

  let mail = new MailComposer(
    {
      to: "[email protected]",
      text: "I hope this works",
      html: " <strong> I hope this works </strong>",
      subject: "Test email gmail-nodemailer-composer",
      textEncoding: "base64",
      attachments: [
        {   // encoded string as an attachment
          filename: 'text1.txt',
          content: 'aGVsbG8gd29ybGQh',
          encoding: 'base64'
        },
        {   // encoded string as an attachment
          filename: 'text2.txt',
          content: 'aGVsbG8gd29ybGQh',
          encoding: 'base64'
        },
      ]
    });

  mail.compile().build( (error, msg) => {
    if (error) return console.log('Error compiling email ' + error);

    const encodedMessage = Buffer.from(msg)
      .toString('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '');

    const gmail = google.gmail({version: 'v1', auth});
    gmail.users.messages.send({
      userId: 'me',
      resource: {
        raw: encodedMessage,
      }
    }, (err, result) => {
      if (err) return console.log('NODEMAILER - The API returned an error: ' + err);

      console.log("NODEMAILER - Sending email reply from server:", result.data);
    });

  })

  // ----------nodemailer test----------------------------------------------------


}
like image 87
LPS Avatar answered Sep 18 '22 22:09

LPS


Being stuck on the same problem, I managed to build a solution by grabbing stuff left and right.

what you need to use is the npm package gmail-api-create-message-body

npm package page

  const body = createBody({
    headers:{
      To:(msg.to.name) + " <" + msg.to.email + ">",
      From:(msg.from.name) + " <" + msg.from.email + ">",
      Subject:msg.subject
    },
    textHtml:msg.body.html,
    textPlain:msg.body.text,
    attachments:msg.files
  })

The files are an array of the following format. This is just an example:

    files: [{
      type: "image/jpeg",
      name: "id1.jpg",
      data:base64ImageData
    }, {
      type: "image/jpeg",
      name: "id2.jpg",
      data: base64ImageData
    }]

Next I needed to mix 2 api's. I wanted to do everything through the Google API's but that did not work and I didn't want to waste hours understanding why (and their documentation + examples for node are a disaster)

In order to do the call we need the authentication token. This can be found using the npm package google-auth-library

await oauth2Client.getAccessToken()

The full details of how to OAuth2 with Google are out of scope for this answer I think.

Next we need to actually send the mail. Impossible for me to get it to work with the official Gmail api (kept getting Error: Recipient address required), so I used request-promise as shown in the example of gmail-api-create-message-body

await rp({
  method: 'POST',
  uri: 'https://www.googleapis.com/upload/gmail/v1/users/me/messages/send',
  headers: {
    Authorization: `Bearer ${oauth2Client.credentials.access_token}`,
    'Content-Type': 'multipart/related; boundary="foo_bar_baz"'
  },
  body: body
});

And this al worked perfectly.

like image 45
Joris Mans Avatar answered Sep 20 '22 22:09

Joris Mans