Reply to a thread (mail) with attachment being sent as a new mail in sender account - gmail api

佐手、 提交于 2020-01-06 08:31:10

问题


I am trying to send reply to a thread (mail) with attachment using gmail api. But it is being sent as a new mail in senders account, but in receiver account it is seen in same thread. This mail should be in same thread for both sender and receiver sides as well.

I am using node js and request-promise module for achieving this. Here is my piece of code. Please have a look and let me know where I am wrong.

let from = req.body.email;
    let mailString = '';
    for (let i in req.files) {
      mailString += '--boundary_mail1\n'
      mailString += `Content-Type: ${req.files[i].mimetype}\n`
      mailString += `Content-Disposition: attachment; filename="${req.files[i].filename}"\n`
      mailString += "Content-Transfer-Encoding: base64\n\n"
      mailString += `${fs.readFileSync(req.files[i].path).toString('base64')}` + '\n'
    }
    let raw = [
      'MIME-Version: 1.0\n',
      "to: ", reply.mailContent.to, "\n",
      "from: ", from, "\n",
      "threadId: ", reply.mailContent.id, "\n",
      "References: ", reply.mailContent.references, "\n",
      "In-Reply-To: ", reply.mailContent.messageId, "\n",
      "subject: ", reply.mailContent.subject, "\n",
      "Content-Type: multipart/mixed; boundary=boundary_mail1\n\n",

      "--boundary_mail1\n",
      "Content-Type: multipart/alternative; boundary=boundary_mail2\n\n",

      "--boundary_mail2\n",
      "Content-Type: text/html; charset=UTF-8\n",
      "Content-Transfer-Encoding: quoted-printable\n\n",
      req.body.message = req.body.message ? req.body.message : '', "\n\n",
      "--boundary_mail2--\n",
      mailString,
      '--boundary_mail1--',      
    ].join('');
    const id = 'me';
    let options = {
      url: "https://www.googleapis.com/upload/gmail/v1/users/" + id + "/messages/send?uploadType=multipart",
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${user.options.access_token}`,
        'Content-Type': 'message/rfc822'
      },
      body: raw
    };
    await request(options).then(async b => {
        return res.apiOk();
      }).catch(err => {
        return res.apiError(err);
      })
    }).catch(err => {
      return res.apiError("Something went wrong!");
    })

回答1:


It's a bit complicated to explain how should it be.

I had the same problem, after a while, I ended up this js-function to create a Gmail-Message-Body. It works fine now.

Lets dig inside:

Gmail Body Maker

function createJson(threadId) {
  const thread = threadId ? `"threadId": "${threadId}"\r\n` : ''

  return [
    'Content-Type: application/json; charset="UTF-8"\r\n\r\n',
    `{\r\n${thread}}`
  ].join('')
}

function createHeaders(headers) {
  if ( !headers || headers.length === 0 )
    return ''

  const result = []

  for (const h in headers) {
    if (headers.hasOwnProperty(h)) {
      result.push(`${h}: ${headers[h]}\r\n`)
    }
  }

  return result.join('')
}

function createPlain(text) {
  return [
    'Content-Type: text/plain; charset="UTF-8"\r\n',
    'MIME-Version: 1.0\r\n',
    'Content-Transfer-Encoding: 7bit\r\n\r\n',
    text
  ].join('')
}

function createHtml(html) {
  return [
    'Content-Type: text/html; charset="UTF-8"\r\n',
    'MIME-Version: 1.0\r\n',
    'Content-Transfer-Encoding: 7bit\r\n\r\n',
    html
  ].join('')
}

function createAlternative(text, html) {
  return [
    'Content-Type: multipart/alternative; boundary="foo"\r\n\r\n',

    '--foo\r\n',
    createPlain(text),
    '\r\n\r\n',

    '--foo\r\n',
    createHtml(html),
    '\r\n\r\n',

    '--foo--',
  ].join('')
}

function createBody(text, html) {
  if (text && html)
    return createAlternative(text, html)

  if (text)
    return createPlain(text)

  if (html)
    return createHtml(html)

  return ''
}

function createAttachments(attachments) {
  if ( !attachments || attachments.length === 0 )
    return ''

  const result = []

  for (let i = 0; i < attachments.length; i++) {
    const att     = attachments[i]
    const attName = att.filename ? `; filename="${att.filename}"` : ''

    // Content-Type: image/jpeg
    // MIME-Version: 1.0
    // Content-ID: <20180619202303.24365.655.img@domain>
    // Content-Transfer-Encoding: base64
    // Content-Disposition: inline

    result.push('--foo_bar\r\n')
    result.push(`Content-Type: ${att.type}\r\n`)
    result.push('MIME-Version: 1.0\r\n')
    if (att.contentId) result.push(`Content-ID: <${att.contentId}>\r\n`)
    result.push('Content-Transfer-Encoding: base64\r\n')
    result.push(`Content-Disposition: attachment${attName}\r\n\r\n`)
    result.push(`${att.content}\r\n\r\n`)
  }

  return result.join('')
}

function gmailBodyMaker(params) {
  const json        = createJson(params.threadId)
  const headers     = createHeaders(params.headers)
  const body        = createBody(params.text, params.html)
  const attachments = createAttachments(params.attachments)

  return [
    '--foo_bar_baz\r\n',
    `${json}\r\n\r\n`,

    '--foo_bar_baz\r\n',
    'Content-Type: message/rfc822\r\n\r\n',

    'Content-Type: multipart/mixed; boundary="foo_bar"\r\n',
    `${headers}\r\n`,

    '--foo_bar\r\n',
    `${body}\r\n\r\n`,

    attachments,

    '--foo_bar--\r\n\r\n',

    '--foo_bar_baz--',
  ].join('')
}

Create Email Object

const createEmailObject = async function (params) {
  const attachments = []

  for (const att of params.attachments) {
    const imageData = await request.get({
      url: att.link,
      encoding: 'binary'
    })

    const base64 = Buffer.from(imageData, 'binary').toString('base64')
    const size   = base64.length // in bytes

    if ( size > 5242880)
      throw Error.BadRequest('File-size could not be greater than 4-MB!')

    attachments.push({
      'filename': att.filename,
      'contentId': att.contentId || null, // <img src="cid:some-image-cid" alt="img" />
      'content': base64,
      'encoding': 'base64',
      'type': att.type
    })
  }

  const toRecipients  = params.header.to ? params.header.to.map(record => (record.address)) : []
  const ccRecipients  = params.header.cc ? params.header.cc.map(record => (record.address)) : []
  const bccRecipients = params.header.bcc ? params.header.bcc.map(record => (record.address)) : []

  return {
    headers: {
      From: params.header.from,
      To: toRecipients.join(','),
      Cc: ccRecipients.join(','),
      Bcc: bccRecipients.join(','),
      'In-Reply-To': params.header['In-Reply-To'],

      Subject: params.header.subject
    },

    threadId: params.threadId,
    attachments: attachments,

    text: params.body.text,
    html: params.body.html
  }
}

Email Senders


// Highly recommend to use this method
Google.prototype.sendMultipartMessage = async function (email) {
  // File Size Limit: Up To 35 MB
  const authHeader = await this.oAuth2Client.getRequestHeaders()

  const arr   = authHeader.Authorization.split('Bearer ')
  const token = arr[1]

  const responseString = await request.post({
    url: 'https://www.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=multipart',
    headers: {
      'Authorization': `OAuth ${token}`,
      'Content-Type': 'multipart/related; boundary="foo_bar_baz"'
    },
    body: gmailBodyMaker(email)
  })

  return JSON.parse(responseString)
}

// Alternative Method
Google.prototype.sendMessage = async function (email) {
  const message = [
    'Content-Type: text/plain; charset="UTF-8"',
    'MIME-Version: 1.0',
    'Content-Transfer-Encoding: 7bit',
    `To: ${email.to}`,
    `Subject: ${email.subject}`,
    '', // Extra new line required
    `${email.htmlBody}`
  ].join('\n')

  const raw = Buffer.from(message).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')

  const response = await this.gmail.users.messages.send({
    auth: this.oAuth2Client,
    userId: this.gmailAddress,
    resource: {
      threadId: email.threadId,
      raw: raw
    }
  })

  return response
}

// Alternative Method
Google.prototype.sendMessageWithAttachment = async function (email) {
  // File Size Limit: 5 MB

  const message = new MailComposer({
    to: email.to,
    subject: email.subject,
    text: email.textBody,
    html: email.htmlBody,
    textEncoding: 'base64',
    attachments: email.attachments
  })

  const builtMessage = await message.compile().build()
  const raw          = Buffer.from(builtMessage).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')    

  const response = await this.gmail.users.messages.send({
    auth: this.oAuth2Client,
    userId: this.gmailAddress,
    uploadType: 'multipart',
    resource: {
      threadId: email.threadId,
      raw: raw
    }
  })

  return response
}

Main Script

const params = {
  'header': {
    'subject': 'your-subject',
    'from': 'name <email@domain.com> ',
    'to': [{'address': 'email_2@domain.com', 'name': 'name'}],
    'cc': [{'address': 'email_3@domain.com', 'name': 'name'}],
    'bcc': [{'address': 'email_4@domain.com', 'name': 'name'}],
    'In-Reply-To': `<any_valid_internet_message_id_goes_here>` || null
  },

  'threadId': 'any_valid_thread_id_goes_here' || null,

  'attachments': [
    {
      'filename': 'filename.jpg',
      'link': 'https://some-where.com/my_image.jpg',
      'type': 'image/jpeg',
      'isInline': true,
      'contentId': 'any_valid_content_id'
    }
  ],

  'body': {
    'text': 'text',
    'html': 'html'
  }
}

// Solution 1
const body   = await createEmailObject(params)
const result = await google.sendMultipartMessage(body)

const sentMessageId = result.id


来源:https://stackoverflow.com/questions/54555904/reply-to-a-thread-mail-with-attachment-being-sent-as-a-new-mail-in-sender-acco

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!