Files
wpywmail-client/server/mime.js
T

615 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* MIME 解析与组装(RFC 5322 / 2047 / 2231 / 2045-2049)。
*
* 这一层决定了「中文对不对」。生产环境的教训:
* - 头部必须能解 RFC 2047 编码字(否则主题显示成 =?utf-8?B?...?=)
* - 头部若被写成裸 UTF-8,必须按 UTF-8 优先解(按 Latin-1 解会得到 [æµè¯])
* - 正文要按声明的 charset 解,且要能回退到 GBK/Big5(老邮件)
* - 组装时要主动用 RFC 2047 编码非 ASCII 头部,不能把裸中文写进 Subject
*/
const { TextDecoder } = require('node:util');
const crypto = require('node:crypto');
const CRLF = '\r\n';
// ─────────────────────────────────────────────────────────── 字符集
const DECODERS = new Map();
function decoderFor(charset) {
const key = String(charset || 'utf-8').trim().toLowerCase().replace(/^["']|["']$/g, '');
if (DECODERS.has(key)) return DECODERS.get(key);
const aliases = {
'utf8': 'utf-8',
'utf-8': 'utf-8',
'gb2312': 'gbk',
'gb18030': 'gbk',
'gbk': 'gbk',
'big5': 'big5',
'big5-hkscs': 'big5',
'shift_jis': 'shift_jis',
'shift-jis': 'shift_jis',
'sjis': 'shift_jis',
'euc-kr': 'euc-kr',
'ks_c_5601-1987': 'euc-kr',
'latin1': 'windows-1252',
'iso-8859-1': 'windows-1252',
'us-ascii': 'utf-8',
'ascii': 'utf-8',
'windows-1252': 'windows-1252',
};
const label = aliases[key] || key;
let dec = null;
try {
dec = new TextDecoder(label, { fatal: false });
} catch {
dec = null;
}
DECODERS.set(key, dec);
return dec;
}
/** 按 charset 解码字节;失败则依次尝试 utf-8 / gbk */
function decodeCharset(buffer, charset) {
if (!buffer || !buffer.length) return '';
const dec = decoderFor(charset);
if (dec) {
try {
return dec.decode(buffer);
} catch { /* 落到下面的回退 */ }
}
try {
return new TextDecoder('utf-8', { fatal: false }).decode(buffer);
} catch {
return buffer.toString('latin1');
}
}
// ─────────────────────────────────────────────────────────── 传输编码
/** quoted-printable → 原始字节(含软换行合并) */
function decodeQuotedPrintable(buffer) {
const text = buffer.toString('latin1');
const out = [];
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (c === '=') {
const n1 = text[i + 1];
const n2 = text[i + 2];
if (n1 === '\r' && n2 === '\n') { i += 2; continue; }
if (n1 === '\n') { i += 1; continue; }
if (/[0-9A-Fa-f]{2}/.test((n1 || '') + (n2 || ''))) {
out.push(parseInt(n1 + n2, 16));
i += 2;
continue;
}
}
out.push(c.charCodeAt(0) & 0xff);
}
return Buffer.from(out);
}
function decodeTransfer(buffer, encoding) {
const enc = String(encoding || '').trim().toLowerCase();
if (enc === 'base64') {
const clean = buffer.toString('latin1').replace(/[^A-Za-z0-9+/=]/g, '');
try { return Buffer.from(clean, 'base64'); } catch { return buffer; }
}
if (enc === 'quoted-printable') return decodeQuotedPrintable(buffer);
return buffer;
}
// ─────────────────────────────────────────────────────────── RFC 2047
function decodeOneWord(charset, encoding, text) {
let bytes;
if (encoding.toUpperCase() === 'B') {
try { bytes = Buffer.from(text.replace(/\s+/g, ''), 'base64'); } catch { return text; }
} else {
// Q:下划线代表空格,=XX 代表字节
const bytesArr = [];
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (c === '_') { bytesArr.push(0x20); continue; }
if (c === '=' && /[0-9A-Fa-f]{2}/.test(text.substr(i + 1, 2))) {
bytesArr.push(parseInt(text.substr(i + 1, 2), 16));
i += 2;
continue;
}
bytesArr.push(c.charCodeAt(0) & 0xff);
}
bytes = Buffer.from(bytesArr);
}
return decodeCharset(bytes, charset);
}
/** 解码头部里的 RFC 2047 编码字;相邻编码字之间的空白会被丢弃(RFC 2047 规定) */
function decodeWords(input) {
const s = String(input == null ? '' : input);
if (!s.includes('=?')) return s;
const re = /=\?([^?\s]+)\?([BbQq])\?([^?]*)\?=/g;
let out = '';
let last = 0;
let m;
let prevWasWord = false;
while ((m = re.exec(s)) !== null) {
let between = s.slice(last, m.index);
if (prevWasWord && /^\s*$/.test(between)) between = '';
out += between + decodeOneWord(m[1], m[2], m[3]);
prevWasWord = true;
last = m.index + m[0].length;
}
out += s.slice(last);
return out.replace(/[\r\n]+/g, ' ').trim();
}
/** 把可能是裸 UTF-8 的头部值还原成可读文本 */
function smartDecodeHeader(rawValue) {
const s = String(rawValue == null ? '' : rawValue);
if (s.includes('=?')) return decodeWords(s);
// 全是 ASCII 直接返回
if (!/[\u0080-\u00ff]/.test(s)) return s.trim();
// 含高位字节:优先按 UTF-8 解,失败再按 Latin-1
const bytes = Buffer.from(s, 'latin1');
const utf8 = decodeCharset(bytes, 'utf-8');
if (!utf8.includes('\ufffd')) return utf8.trim();
return s.trim();
}
/** RFC 2231(filename*=UTF-8''...)与普通 filename 参数 */
function decodeParameter(value) {
if (value == null) return '';
const v = String(value).trim();
const m = /^([^']*)'([^']*)'(.*)$/s.exec(v);
if (m) {
const charset = m[1] || 'utf-8';
const encoded = m[3];
// 关键:直接把 %XX 还原成**字节**,不要先 decodeURIComponent 成字符串。
// 先转字符串再用 Buffer.from(str,'binary'/'latin1') 会把多字节字符按单字节截断,
// 中文文件名会变成一串 U+FFFD(这是在真机上踩到的坑)。
const bytes = [];
for (let i = 0; i < encoded.length; i++) {
const ch = encoded[i];
if (ch === '%' && /^[0-9A-Fa-f]{2}$/.test(encoded.substr(i + 1, 2))) {
bytes.push(parseInt(encoded.substr(i + 1, 2), 16));
i += 2;
} else {
bytes.push(ch.charCodeAt(0) & 0xff);
}
}
return decodeCharset(Buffer.from(bytes), charset);
}
return smartDecodeHeader(v.replace(/^"|"$/g, ''));
}
// ─────────────────────────────────────────────────────────── 头部
/** 拆出头部与正文(按字节,保持忠实) */
function splitHeadBody(buffer) {
const idx = buffer.indexOf('\r\n\r\n');
if (idx >= 0) return { head: buffer.subarray(0, idx), body: buffer.subarray(idx + 4) };
const idx2 = buffer.indexOf('\n\n');
if (idx2 >= 0) return { head: buffer.subarray(0, idx2), body: buffer.subarray(idx2 + 2) };
return { head: buffer, body: Buffer.alloc(0) };
}
/** 解析头部为 [{name, value}](折行拼接) */
function parseHeaders(headBuffer) {
const text = headBuffer.toString('latin1');
const lines = text.split(/\r?\n/);
const headers = [];
let cur = null;
for (const line of lines) {
if (/^[ \t]/.test(line) && cur) {
cur.value += CRLF + line;
continue;
}
const i = line.indexOf(':');
if (i <= 0) { cur = null; continue; }
cur = { name: line.slice(0, i).trim(), value: line.slice(i + 1) };
headers.push(cur);
}
return headers;
}
/** Content-Type 解析:mime 类型 + 参数表 */
function parseContentType(value) {
const s = String(value || 'text/plain');
const semi = s.indexOf(';');
const mime = (semi < 0 ? s : s.slice(0, semi)).trim().toLowerCase() || 'text/plain';
const params = {};
if (semi >= 0) {
const rest = s.slice(semi + 1);
// 参数可能带引号并含分号,用状态机切分
const chunks = [];
let buf = '';
let inQuote = false;
for (const ch of rest) {
if (ch === '"') { inQuote = !inQuote; buf += ch; continue; }
if (ch === ';' && !inQuote) { chunks.push(buf); buf = ''; continue; }
buf += ch;
}
if (buf) chunks.push(buf);
for (const chunk of chunks) {
const eq = chunk.indexOf('=');
if (eq < 0) continue;
const k = chunk.slice(0, eq).trim().toLowerCase();
const v = chunk.slice(eq + 1).trim();
params[k] = v;
}
}
// RFC 2231 的分段参数:filename*0*=utf-8''%E9%AA%8C; filename*1*=... → 合并成 filename*
const keys = Object.keys(params);
const groups = new Map();
for (const k of keys) {
const m = /^([a-z0-9_-]+)\*(\d+)(\*?)$/.exec(k);
if (!m) continue;
if (!groups.has(m[1])) groups.set(m[1], []);
groups.get(m[1]).push({ idx: Number(m[2]), encoded: m[3] === '*', key: k });
}
for (const [base, parts] of groups) {
if (parts.length < 2 && !String(parts[0].key).endsWith('*')) continue;
parts.sort((a, b) => a.idx - b.idx);
let joined = '';
let allEncoded = true;
for (const p of parts) {
const raw = params[p.key].replace(/^"|"$/g, '');
joined += raw;
if (!p.encoded) allEncoded = false;
}
params[base + (allEncoded ? '*' : '')] = joined;
for (const p of parts) delete params[p.key];
}
return { mime, params };
}
/** 从 Content-Disposition 里取文件名 */
function filenameFrom(headers) {
const disp = getHeader(headers, 'content-disposition');
if (disp) {
const { params } = parseContentType(disp);
if (params['filename*']) return decodeParameter(params['filename*']);
if (params.filename) return decodeParameter(params.filename);
}
const ct = parseContentType(getHeader(headers, 'content-type'));
if (ct.params['name*']) return decodeParameter(ct.params['name*']);
if (ct.params.name) return decodeParameter(ct.params.name);
return '';
}
function getHeader(headers, name) {
const lower = name.toLowerCase();
const hit = headers.find((h) => h.name.toLowerCase() === lower);
return hit ? hit.value : null;
}
/** 地址列表解析:`"张三" <[email protected]>, [email protected]` */
function parseAddressList(rawValue) {
const s = smartDecodeHeader(rawValue || '');
if (!s.trim()) return [];
const parts = [];
let buf = '';
let inQuote = false;
let inAngle = false;
for (const ch of s) {
if (ch === '"') inQuote = !inQuote;
if (ch === '<' && !inQuote) inAngle = true;
if (ch === '>' && !inQuote) inAngle = false;
if (ch === ',' && !inQuote && !inAngle) { parts.push(buf); buf = ''; continue; }
buf += ch;
}
if (buf.trim()) parts.push(buf);
return parts.map((p) => {
const t = p.trim();
const m = /^(.*?)<([^>]+)>\s*$/.exec(t);
if (m) {
const name = m[1].trim().replace(/^"|"$/g, '').trim();
return { name, address: m[2].trim() };
}
return { name: '', address: t.replace(/^"|"$/g, '') };
}).filter((a) => a.address);
}
// ─────────────────────────────────────────────────────────── 正文解析
/** 粗略地从 HTML 提取纯文本(没有 text/plain 部分时的回退) */
function htmlToText(html) {
return String(html)
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/(p|div|li|tr|h[1-6])>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/gi, ' ')
.replace(/&lt;/gi, '<').replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"').replace(/&#39;/gi, "'")
.replace(/&amp;/gi, '&')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
/**
* 递归解析一个 MIME 实体。
* @returns {{headers, mime, params, text, html, attachments, children}}
*/
function parseEntity(headBuffer, bodyBuffer, depth = 0) {
const headers = parseHeaders(headBuffer);
const ct = parseContentType(getHeader(headers, 'content-type'));
const transfer = (getHeader(headers, 'content-transfer-encoding') || '').trim();
const node = {
headers, mime: ct.mime, params: ct.params,
text: null, html: null, attachments: [], children: [],
};
if (ct.mime.startsWith('multipart/') && (ct.params.boundary || ct.params.boundary === '')) {
const boundary = String(ct.params.boundary || '').replace(/^"|"$/g, '');
const parts = splitMultipart(bodyBuffer, boundary);
for (const p of parts) {
if (depth > 8) break;
const child = parseEntity(p.head, p.body, depth + 1);
node.children.push(child);
if (child.text != null && node.text == null) node.text = child.text;
if (child.html != null && node.html == null) node.html = child.html;
node.attachments.push(...child.attachments);
}
// 有些坑爹邮件把文本塞在 multipart 的 preamble 里
return node;
}
const decoded = decodeTransfer(bodyBuffer, transfer);
const charset = ct.params.charset;
const disposition = (parseContentType(getHeader(headers, 'content-disposition')).mime || '');
const filename = filenameFrom(headers);
const isAttachment =
disposition === 'attachment' ||
(!!filename && !/^text\/(plain|html)$/i.test(ct.mime)) ||
/^(image|application|audio|video)\//i.test(ct.mime) && !!filename;
if (!isAttachment && ct.mime === 'text/plain') {
node.text = decodeCharset(decoded, charset).replace(/\r\n/g, '\n');
return node;
}
if (!isAttachment && ct.mime === 'text/html') {
node.html = decodeCharset(decoded, charset).replace(/\r\n/g, '\n');
return node;
}
if (ct.mime === 'message/rfc822') {
const inner = parseEntityFromBuffer(decoded, depth + 1);
node.children.push(inner);
if (inner.text != null && node.text == null) node.text = inner.text;
if (inner.html != null && node.html == null) node.html = inner.html;
node.attachments.push(...inner.attachments);
return node;
}
// 其余一律当附件(含无文件名的内联图)
node.attachments.push({
filename: filename || '(未命名)',
contentType: ct.mime,
contentId: (getHeader(headers, 'content-id') || '').replace(/^<|>$/g, '') || null,
inline: disposition === 'inline',
size: decoded.length,
content: decoded,
});
return node;
}
function parseEntityFromBuffer(buffer, depth = 0) {
const { head, body } = splitHeadBody(buffer);
return parseEntity(head, body, depth);
}
/** 按 boundary 切分 multipart 正文 */
function splitMultipart(bodyBuffer, boundary) {
const delim = Buffer.from('--' + boundary);
const parts = [];
let start = bodyBuffer.indexOf(delim);
if (start < 0) return parts;
start += delim.length;
// 跳过紧随其后的 CRLF
if (bodyBuffer[start] === 0x0d && bodyBuffer[start + 1] === 0x0a) start += 2;
else if (bodyBuffer[start] === 0x0a) start += 1;
for (;;) {
const next = bodyBuffer.indexOf(delim, start);
if (next < 0) break;
let end = next;
// 去掉分隔符前的 CRLF
if (end >= 2 && bodyBuffer[end - 2] === 0x0d && bodyBuffer[end - 1] === 0x0a) end -= 2;
else if (end >= 1 && bodyBuffer[end - 1] === 0x0a) end -= 1;
const chunk = bodyBuffer.subarray(start, end);
const { head, body } = splitHeadBody(chunk);
parts.push({ head, body });
// 判断是否为结束分隔符 "--boundary--"
const afterDelim = next + delim.length;
if (bodyBuffer[afterDelim] === 0x2d && bodyBuffer[afterDelim + 1] === 0x2d) break;
start = afterDelim;
if (bodyBuffer[start] === 0x0d && bodyBuffer[start + 1] === 0x0a) start += 2;
else if (bodyBuffer[start] === 0x0a) start += 1;
}
return parts;
}
/** 顶级解析:给出客户端 UI 需要的全部字段 */
function parseMessage(rawBuffer) {
const buffer = Buffer.isBuffer(rawBuffer) ? rawBuffer : Buffer.from(rawBuffer);
const root = parseEntityFromBuffer(buffer, 0);
const headers = root.headers;
const subject = smartDecodeHeader(getHeader(headers, 'subject') || '');
const from = parseAddressList(getHeader(headers, 'from'));
const to = parseAddressList(getHeader(headers, 'to'));
const cc = parseAddressList(getHeader(headers, 'cc'));
const replyTo = parseAddressList(getHeader(headers, 'reply-to'));
const text = root.text != null ? root.text : (root.html ? htmlToText(root.html) : '');
return {
headers,
headerMap: headers.reduce((m, h) => { m[h.name.toLowerCase()] = h.value; return m; }, {}),
subject,
from, to, cc, replyTo,
date: getHeader(headers, 'date') ? smartDecodeHeader(getHeader(headers, 'date')) : null,
messageId: (getHeader(headers, 'message-id') || '').trim() || null,
inReplyTo: (getHeader(headers, 'in-reply-to') || '').trim() || null,
references: (getHeader(headers, 'references') || '').trim() || null,
text,
html: root.html || null,
attachments: root.attachments,
size: buffer.length,
};
}
// ─────────────────────────────────────────────────────────── 组装(发信)
/** 非 ASCII 头部值 → RFC 2047 编码字(按 45 字节分块,保证整词 ≤75 字符) */
function encodeHeaderValue(value) {
const s = String(value == null ? '' : value).replace(/[\r\n]+/g, ' ');
// eslint-disable-next-line no-control-regex
if (/^[\x20-\x7e]*$/.test(s)) return s;
const bytes = Buffer.from(s, 'utf8');
const chunks = [];
let cur = [];
let curLen = 0;
for (const ch of s) {
const b = Buffer.byteLength(ch, 'utf8');
if (curLen + b > 45) { chunks.push(Buffer.from(cur.join(''), 'utf8')); cur = []; curLen = 0; }
cur.push(ch);
curLen += b;
}
if (cur.length) chunks.push(Buffer.from(cur.join(''), 'utf8'));
return chunks.map((b) => `=?UTF-8?B?${b.toString('base64')}?=`).join(CRLF + ' ');
}
/** 组装地址头 */
function formatAddress(addr) {
const name = addr.name ? encodeHeaderValue(addr.name) : '';
if (!name) return addr.address;
const needsQuote = !/^[\x20-\x7e]*$/.test(name) || /[",;:<>@\\()\[\]]/.test(name);
return `${needsQuote && !name.startsWith('=?') ? `"${name.replace(/"/g, '\\"')}"` : name} <${addr.address}>`;
}
function wrap76(base64) {
const lines = [];
for (let i = 0; i < base64.length; i += 76) lines.push(base64.slice(i, i + 76));
return lines.join(CRLF);
}
function generateMessageId(domain) {
const rnd = crypto.randomBytes(12).toString('hex');
return `<${Date.now()}.${rnd}@${domain || 'localhost'}>`;
}
function rfc2822Date(d = new Date()) {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const pad = (n) => String(n).padStart(2, '0');
const off = -d.getTimezoneOffset();
const sign = off >= 0 ? '+' : '-';
const abs = Math.abs(off);
return `${days[d.getDay()]}, ${pad(d.getDate())} ${months[d.getMonth()]} ${d.getFullYear()} ` +
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())} ` +
`${sign}${pad(Math.floor(abs / 60))}${pad(abs % 60)}`;
}
/**
* 组装一封可发送的 MIME 报文(返回 UTF-8 Buffer,CRLF 行尾)。
* @param {object} msg {from:{name,address}, to:[], cc:[], subject, text, html,
* attachments:[{filename,contentType,content}], inReplyTo,
* references, domain, date}
*/
function buildMessage(msg) {
const domain = msg.domain || (msg.from && msg.from.address ? String(msg.from.address).split('@')[1] : 'localhost');
const headers = [];
headers.push(['From', formatAddress(msg.from)]);
if (msg.to && msg.to.length) headers.push(['To', msg.to.map(formatAddress).join(', ')]);
if (msg.cc && msg.cc.length) headers.push(['Cc', msg.cc.map(formatAddress).join(', ')]);
headers.push(['Subject', encodeHeaderValue(msg.subject || '')]);
headers.push(['Date', msg.date || rfc2822Date()]);
headers.push(['Message-ID', msg.messageId || generateMessageId(domain)]);
if (msg.inReplyTo) headers.push(['In-Reply-To', msg.inReplyTo]);
if (msg.references) headers.push(['References', msg.references]);
headers.push(['MIME-Version', '1.0']);
const attachments = msg.attachments || [];
const useHtml = !!msg.html;
const textPart = (body, mime, charset = 'UTF-8') => {
const isAscii = /^[\x09\x0a\x0d\x20-\x7e]*$/.test(body);
const enc = isAscii ? '7bit' : 'base64';
const payload = isAscii ? body.replace(/\r?\n/g, CRLF) : wrap76(Buffer.from(body, 'utf8').toString('base64'));
return [
`Content-Type: ${mime}; charset=${charset}`,
`Content-Transfer-Encoding: ${enc}`,
'',
payload,
].join(CRLF);
};
let bodyText;
if (attachments.length === 0 && !useHtml) {
headers.push(['Content-Type', 'text/plain; charset=UTF-8']);
const isAscii = /^[\x09\x0a\x0d\x20-\x7e]*$/.test(msg.text || '');
headers.push(['Content-Transfer-Encoding', isAscii ? '7bit' : 'base64']);
bodyText = isAscii
? (msg.text || '').replace(/\r?\n/g, CRLF)
: wrap76(Buffer.from(msg.text || '', 'utf8').toString('base64'));
} else if (attachments.length === 0 && useHtml) {
const alt = 'alt-' + crypto.randomBytes(10).toString('hex');
headers.push(['Content-Type', `multipart/alternative; boundary="${alt}"`]);
bodyText = [
`--${alt}`, textPart(msg.text || '', 'text/plain'), '',
`--${alt}`, textPart(msg.html, 'text/html'), '',
`--${alt}--`, '',
].join(CRLF);
} else {
const mixed = 'mix-' + crypto.randomBytes(10).toString('hex');
headers.push(['Content-Type', `multipart/mixed; boundary="${mixed}"`]);
const chunks = [];
if (useHtml) {
const alt = 'alt-' + crypto.randomBytes(10).toString('hex');
chunks.push(`--${mixed}`, `Content-Type: multipart/alternative; boundary="${alt}"`, '');
chunks.push(`--${alt}`, textPart(msg.text || '', 'text/plain'), '');
chunks.push(`--${alt}`, textPart(msg.html, 'text/html'), '');
chunks.push(`--${alt}--`, '');
} else {
chunks.push(`--${mixed}`, textPart(msg.text || '', 'text/plain'), '');
}
for (const att of attachments) {
const content = Buffer.isBuffer(att.content) ? att.content : Buffer.from(att.content || '', 'base64');
const name = encodeHeaderValue(att.filename || 'attachment');
chunks.push(`--${mixed}`);
chunks.push(`Content-Type: ${att.contentType || 'application/octet-stream'}; name="${name}"`);
chunks.push('Content-Transfer-Encoding: base64');
chunks.push(`Content-Disposition: attachment; filename="${name}"`);
chunks.push('');
chunks.push(wrap76(content.toString('base64')));
chunks.push('');
}
chunks.push(`--${mixed}--`, '');
bodyText = chunks.join(CRLF);
}
const head = headers.map(([k, v]) => `${k}: ${v}`).join(CRLF);
return Buffer.from(head + CRLF + CRLF + bodyText, 'utf8');
}
module.exports = {
parseMessage,
buildMessage,
parseHeaders,
parseAddressList,
parseContentType,
decodeWords,
decodeParameter,
smartDecodeHeader,
decodeCharset,
decodeQuotedPrintable,
decodeTransfer,
encodeHeaderValue,
htmlToText,
splitMultipart,
filenameFrom,
getHeader,
rfc2822Date,
generateMessageId,
};