7

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')
        }
    });
}
informatik01
  • 16,038
  • 10
  • 74
  • 104
moxched
  • 163
  • 2
  • 10

4 Answers4

18

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: fakeemail@gmail.com`,
`References: `,
`From: fakeemail2@gmail.com`,
`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: "FAKE_EMAIL@gmail.com",
      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----------------------------------------------------


}
LPS
  • 375
  • 3
  • 16
  • since i posted this question my project was on hold your answer is very helpful i'll continue working on it thank you – moxched Sep 13 '18 at 15:04
  • @moxched, No problem, if you liked it or if it was helpful you can vote it up :) – LPS Sep 14 '18 at 19:01
  • This should be the accepted answer. Even if the approach of using gmail-api-create-message-body worked for me, it is not the best that you lose the ability to send the email using the API (for instance you don't get the email id you have just sent back). Using nodemailer is the way to go since building all the message body from scratch is cumbersome and error pron. Don't forget to add contentType property as well into the attachments. – Hernan Veiras Apr 04 '19 at 15:12
  • 1
    Feel free to upvote it or accepting it, as you might imagine i have no problem with that :) Thank you – LPS Apr 08 '19 at 07:13
  • 1
    bro can you explain me why you replace '+' and '/'? – Bipin Maharjan Nov 26 '21 at 10:23
  • This was hugely helpful for trying to add a last-minute attachment late at night. Thank you! – RobertMcReed Jan 10 '22 at 08:46
  • @RobertMcReed good to know! Feel free to upvote the answer. – LPS Jan 11 '22 at 16:13
  • Thanks, this extremely helpful! One more thing: is it possible to invoke the inlineBase64 plugin from MailComposer, and how? Normally plugins are connected to the transporter by `transport.use('compile', plugin)`, but in this code we use no transport, so I wonder. Many thanks! – bfredo123 Aug 13 '22 at 10:59
1

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.

Joris Mans
  • 6,024
  • 6
  • 42
  • 69
  • If I wanted to bash, SO style, I would have asked to have this question marked as duplicate with a reference to the question you posted :D – Joris Mans Jun 11 '18 at 13:55
  • yo Joris, hey man I see you're a previous game dev and seems now you're on web dev. Can I add you (twitter, email, etc)? would like to know some tips on that field. I'm a bit 2D gamedev myself and also learning web dev. – ReyAnthonyRenacia Jun 11 '18 at 13:57
  • I'm by no means a web dev "guru". My main speciality is iOS, but I have been doing Nodejs for backend and React for frontend work for a couple of years now. That game dev stuff is long time ago. From 2000 until 2007 :) – Joris Mans Jun 11 '18 at 19:37
  • nice man..im going the NodeJS and React path too (currently VueJS). let me add u – ReyAnthonyRenacia Jun 11 '18 at 20:56
0

There's an instruction with regard to this in Creating messages with attachments:

Creating a message with an attachment is like creating any other message, but the process of uploading the file as a multi-part MIME message depends on the programming language.

For the NodeJS sample reference, check this SO Post.

ReyAnthonyRenacia
  • 17,219
  • 5
  • 37
  • 56
  • 6
    The SO post you reference is completely useless. It does not show how to do it, it just says "look for yourself" – Joris Mans Jun 10 '18 at 12:00
0

I was able to do it by using nodemailer mail composer. here is the complete source code based on the nodejs quick-start: https://developers.google.com/gmail/api/quickstart/nodejs

const fs = require('fs').promises;
const path = require('path');
const process = require('process');
const {authenticate} = require('@google-cloud/local-auth');
const {google} = require('googleapis');

const OAuth2 = google.auth.OAuth2;
const REDIRECT_URI = ["http://localhost"]
// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.compose'];
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
const TOKEN_PATH = path.join(process.cwd(), 'token.json');
const CREDENTIALS_PATH = path.join(process.cwd(), 'google_credentials.json');

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

async function loadSavedCredentialsIfExist() {
   try {
       const content = await fs.readFile(TOKEN_PATH);
       const credentials = JSON.parse(content);
       return google.auth.fromJSON(credentials);
   } catch (err) {
     return null;
   }
}

async function saveCredentials(client) {
   const content = await fs.readFile(CREDENTIALS_PATH);
   const keys = JSON.parse(content);
   const key = keys.installed || keys.web;
   const payload = JSON.stringify({
      type: 'authorized_user',
      client_id: key.client_id,
      client_secret: key.client_secret,
      refresh_token: client.credentials.refresh_token,
   });
   await fs.writeFile(TOKEN_PATH, payload);
   }

   async function authorize() {
   let client = await loadSavedCredentialsIfExist();
  if (client) {
    return client;
  }
 client = await authenticate({
   scopes: SCOPES,
   keyfilePath: CREDENTIALS_PATH,
});
 if (client.credentials) {
   await saveCredentials(client);
  }
   return client;
}

let EMAIL_ADDRESS = process.env.GMAIL_EMAIL_ADDRESS

function streamToString (stream) {
  const chunks = [];
  return new Promise((resolve, reject) => {
    stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
    stream.on('error', (err) => reject(err));
    stream.on('end', () =>     resolve(Buffer.concat(chunks).toString('utf8')));
  })
 }

 let messagePayload = {
    subject: "Exploring Gmail API",
    text: "Hi, this is a test email from Node.js using Gmail API",
    to: "email@gmail.com",
    from: EMAIL_ADDRESS,
    attachments: [{filename: 'doc.pdf', path: './doc.pdf'}]
  }

  async function sendEmail(auth) {
    const gmail = google.gmail({version: 'v1', auth});
    var mail = new MailComposer(messagePayload);
    var stream = mail.compile().createReadStream();
    const messageResult = await streamToString(stream)
    const encodedMessage = Buffer.from(messageResult).toString('base64')
   .replace(/\+/g, '-')
   .replace(/\//g, '_')
   .replace(/=+$/, '');

   const res = await gmail.users.messages.send({
       userId: 'me',
      requestBody: {
        raw: encodedMessage,
      },
    });
    console.log(res.data);
     return res.data;
    }

   authorize().then(sendEmail).catch(console.error);
ycode
  • 301
  • 3
  • 7