I am using GMAIL api to send email from the nodejs api. I am rendering the raw body using the following utility function
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, '_');
The code i have used to send the email is
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
),
},
});
The emojis are being rendered in the body but not working in subject. See the pic below
Left one is from Gmail in Google Chrome on Desktop and right one is from Gmail App in Mobile
Your utility may benefit from several improvements (we will get to the emoji problem):
\r\n
).Content-Transfer-Encoding
header, set it to 7bit
is easiest, but may not be generic enough (quoted-printable is likely the better option).Now to the emoji problem:
You need to make sure the subject is correctly encoded separately from the body to be able to pass the emoji. According to RFC13420, you can use either Base64 or quoted-printable encoding on a subject to create an encoded-word, described as:
"=" "?" charset "?" encoding "?" encoded-text "?" "="
Where encoding is either Q
for quoted-printable and B
for Base64 encoding.
Note that the resulting encoded subject string must not be longer than 76 chars in length, which reserves 75 chars for the string and 1 for the separator (to use multiple words, separate them with space or newline [CRLF as well]).
So, set your charset to utf-8
, encoding to Q
, encode the actual subject with something like below1, and you are half way done:
/**
* @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}?=`;
}
}
Now, the fun part. I was working on a GAS project that had to leverage the Gmail API directly (after all, that is what the client library does under the hood). Even with the correct encoding, an attempt to pass something like a "Beep! \u{1F697}"
resulted in an incorrectly parsed subject.
Turns out you need to leverage fromCodePoint
operating on byte array or buffer from the original string. This snippet should suffice (don't forget to apply only to multibyte chars):
const escape = (u: string) => String.fromCodePoint(...Buffer.from(u));
0 This is the initial RFC, it would be more appropriate to refer to RFC 2047. Also, see RFC 2231 for including locale info in the header (and some more obscure extensions).
1 If the char falls into a range of printable US-ASCII, it can be left as-is, but due to an extensive set of rules, I recommend sticking to 48-57
(numbers), 65-90
(uppercase) and 97-122
(lowercase) ranges.