表情符号未在 gmail 主题中呈现

Emoji is not rendered in the subject of gmail

我正在使用 GMAIL api 从 nodejs api 发送电子邮件。我正在使用以下实用函数

渲染原始 body
message += '[DEFAULT EMOJI ]'
  
const str = [
    'Content-Type: text/html; charset="UTF-8"\n',
    'MIME-Version: 1.0\n',
    'Content-Transfer-Encoding: 7bit\n',
    'to: ',
    to,
    '\n',
    'from: ',
    from.name,
    ' <',
    from.address,
    '>',
    '\n',
    'subject: ',
    subject + '[DEFAULT EMOJI ]',
    '\n\n',
    message
  ].join('');

  return Buffer.alloc(str.length, str).toString('base64').replace(/\+/g, '-').replace(/\//g, '_');

我用来发送电子邮件的代码是

const r = await gmail.users.messages.send({
    auth,
    userId: "me",
    requestBody: {
        raw: makeEmailBody(
            thread.send_to,
            {
                address: user.from_email,
                name: user.from_name,
            },
            campaign.subject,
            campaign.template,
            thread.id
        ),
    },
});

表情符号正在 body 中呈现,但在主题中不起作用。见下图

左边一个来自桌面 Google Chrome 中的 Gmail,右边一个来自移动

中的 Gmail 应用程序

您的实用程序可能会受益于多项改进(我们将讨论表情符号问题):

  1. 首先,使其成为 RFC822 compliant by separating lines with CRLF (\r\n).
  2. 小心 Content-Transfer-Encoding header,将其设置为 7bit 最简单,但可能不够通用(quoted-printable 可能是更好的选择)。

现在是表情符号问题:

您需要确保主题已正确编码 与 body 分开 才能传递表情符号。根据 RFC13420,您可以对主题使用 Base64 或 quoted-printable 编码来创建 encoded-word,描述为:

"=" "?" charset "?" encoding "?" encoded-text "?" "="

其中 quoted-printable 编码为 Q,Base64 编码编码为 B

请注意,生成的编码主题字符串的长度不得超过 76 个字符,这会为字符串保留 75 个字符,并且1 作为分隔符(要使用多个单词,用 space 或换行符 [CRLF 以及] 分隔它们)。

所以,将你的字符集设置为 utf-8,编码为 Q,用下面的内容对实际主题进行编码 1,你就成功了一半完成:

/**
 * @summary RFC 1342 header encoding
 * @see {@link https://www.rfc-editor.org/rfc/rfc1342}
 */
class HeaderEncoder {

  /**
   * @summary encode using Q encoding
   */
  static quotedPrintable(str: string, encoding = "utf-8") {
    let encoded = "";

    for (const char of str) {
      const cp = char.codePointAt(0);
      encoded += `=${cp.toString(16)}`;
    }

    return `=?${encoding}?Q?${encoded}?=`;
  }
}

现在,有趣的部分。我正在从事一个必须直接利用 Gmail API 的 GAS 项目(毕竟,这就是客户端库在幕后所做的事情)。即使使用正确的编码,尝试传递类似 "Beep! \u{1F697}" 的内容也会导致错误解析的主题。

事实证明,您需要利用 fromCodePoint 对原始字符串的字节数组或缓冲区进行操作。这个片段应该足够了(不要忘记只应用于多字节字符):

const escape = (u: string) => String.fromCodePoint(...Buffer.from(u));

0 这是最初的 RFC,更适合参考 RFC 2047. Also, see RFC 2231 在 header 中包含语言环境信息(以及更多晦涩的扩展名)。
1如果char落在printable US-ASCII的范围内,可以保留as-is,但由于规则比较多,我建议坚持48-57(数字)、65-90(大写)和 97-122(小写)范围。