Search code examples
javascriptnode.jsgoogle-apigoogle-drive-apigmail-api

NodeJS code to create a Gmail email draft with an attachment from Google Drive given a FileID


I need to compose a draft email using the GMail APIs with an attachment given a fileId from Google Drive using Google Drive APIs.

I have a working GMail API call that creates a draft email (without an attachment):

const res = await gmail.users.drafts.create({
    userId: user.email,
    requestBody: {
        message: {
            raw: await createRawMessage(to, subject, htmlBody),
        },
    },
});

Here is the code to my createRawMessage function:

export async function createRawMessage(recipientEmail, subject, body) {
    const message = [
        `Content-Type: text/html; charset="UTF-8"`,
        `to: ${recipientEmail}`,
        `subject: ${subject}\n`,
        body,
    ].join("\n");
    return Buffer.from(message)
        .toString("base64")
        .replace(/\+/g, "-")
        .replace(/\//g, "_")
        .replace(/=+$/, "");
}

Is there a way to just include a fileId from Google Drive? I would prefer not downloading the file to Base64Encode the file content since it already is sitting there in Google Drive and I have the fileId.

Also, I do not want to use another library like NodeMailer.

I have looked at how to send emails with attachments (not creating drafts) to reference that code in my draft function, but I haven't been able to find a sample that pulls the file from Google Drive. The only examples require downloading the file from Google Drive and then incorporating it into the raw property of the requestBody.message parameter of the gmail.users.drafts.create API.

I have this working in a Google Apps Script, but now trying to migrate this functionality to NodeJS.

UPDATE: Here is the code that works Google Apps Script:

    var attachments = [];
    var file = DriveApp.getFileById(appDetails.resume_file_id);

    attachments.push(file.getAs(file.getMimeType()));

    var draft = GmailApp.createDraft(
      `${contactDetails.contact_name} <${contactDetails.contact_email}>`, 
      `${appDetails.job_title} role @ ${appDetails.company}`, 
      textBody,
      {
        htmlBody: htmlBody,
        attachments: attachments,
      }
    );```

Solution

  • As a simple approach, how about the following modification? In this modification, the webViewLink of the file on Google Drive is added to htmlBody of your script. In order to retrieve webViewLink, Drive API is used.

    Of course, if you already know the values of webViewLink and name of the file, you can directly use them. At that time, Drive API is not required to be used.

    As an important point, in this case, in order to open the file on the user side, it is required to share the file with the user. Please be careful about it.

    Modified script:

    If you test this script, please enable Drive API and include a scope of https://www.googleapis.com/auth/drive.metadata.readonly to your current scopes.

    const gmail = google.gmail({ version: "v1", auth }); // Please set your client
    const drive = google.drive({ version: "v3", auth }); // Please set your client
    
    // Please set your values.
    const fileId = "###";
    const user = { email: "###" };
    const to = "###";
    const subject = "sample subject";
    const htmlBody = "sample HTML body.";
    
    
    const { data: { name, webViewLink } } = await drive.files.get({ fileId, fields: "name,webViewLink" });
    const res = await gmail.users.drafts.create({
      userId: user.email,
      requestBody: {
        message: {
          raw: await createRawMessage(
            to,
            subject,
            htmlBody + `<div><a href="${webViewLink}" target="_blank"><span>${name}</span></a></div>`
          ),
        },
      },
    });
    console.log(res.data);
    

    Note:

    • This modification uses webViewLink. But, if you want to attach the file as binary data, if the file is not a Google Docs file (Documents, Spreadsheets, Slides, and so on), you can directly attach the file. But, if the file is a Google Docs file, it is required to convert it to another mimeType because, in the current stage, the Google Docs files cannot be directly exported with the original mimeType. Please be careful about this. But, from your question, I guessed that this might not be your expected result.

    • If this approach is not your expected result, please provide your current script of this working in a Google Apps Script of I have this working in a Google Apps Script, but now trying to migrate this functionality to NodeJS..

    Reference:

    Added:

    From the OP's reply,

    I have thought of just putting the webViewLink in the body, but I'd like to actually add the attachment.

    I noticed that OP wanted to attach a file content that is not Google Docs files. In this case, the sample script is as follows. In this case, the file content could be directly downloaded.

    In this modification, please include a scope of https://www.googleapis.com/auth/drive.readonly to download the file content.

    function getData(drive, fileId) {
      return new Promise((resolve, reject) => {
        drive.files.get(
          { fileId, alt: "media", supportsAllDrives: true },
          { responseType: "stream" },
          function (err, { data }) {
            if (err) {
              return reject("The API returned an error: " + err);
            }
            let buf = [];
            data.on("data", function (e) {
              buf.push(e);
            });
            data.on("end", function () {
              const buffer = Buffer.concat(buf);
              resolve(buffer.toString("base64"));
            });
          }
        );
      });
    }
    
    async function createRawMessage(recipientEmail, subject, body, drive, fileId) {
      const { data: { name, mimeType } } = await drive.files.get({ fileId, fields: "name,mimeType" });
      const fileContent = await getData(drive, fileId);
    
      // Ref: This is my answer. https://stackoverflow.com/a/53891937
      const message = [
        "MIME-Version: 1.0",
        `to: ${recipientEmail}`,
        `subject: ${subject}`,
        "Content-Type: multipart/mixed; boundary=boundary_mail1\n",
        "--boundary_mail1",
        "Content-Type: multipart/alternative; boundary=boundary_mail2\n",
        "--boundary_mail2",
        "Content-Type: text/html; charset=UTF-8",
        "Content-Transfer-Encoding: quoted-printable\n",
        `${body}\n`,
        "--boundary_mail2--",
        "--boundary_mail1",
        `Content-Type: ${mimeType}`,
        `Content-Disposition: attachment; filename="${name}"`,
        "Content-Transfer-Encoding: base64\n",
        fileContent,
        "--boundary_mail1--",
      ].join("\n");
    
      return Buffer.from(message)
        .toString("base64")
        .replace(/\+/g, "-")
        .replace(/\//g, "_")
        .replace(/=+$/, "");
    }
    
    
    const gmail = google.gmail({ version: "v1", auth }); // Please set your client
    const drive = google.drive({ version: "v3", auth }); // Please set your client
    
    // Please set your values.
    const fileId = "###";
    const user = { email: "###" };
    const to = "###";
    const subject = "sample subject";
    const htmlBody = "sample HTML body.";
    
    const res = await gmail.users.drafts.create({
      userId: user.email,
      requestBody: { message: { raw: await createRawMessage(to, subject, htmlBody, drive, fileId) } },
    });
    console.log(res.data);
    
    • In this modification, the file of fileId is not Google Docs files (Documents, Spreadsheets, Slides and so on). Please be careful about this.
    • When this script is run, the file content is downloaded and the downloaded file content is used as an attachment file. And, a draft email is created.