2024-02-05 06:03:53 +08:00
|
|
|
import { decodeEncodedWords, BDecode, BEncode, QPDecode, decodeText } from 'Mime/Encoding';
|
|
|
|
import { addressparser } from 'Mime/Address';
|
2022-02-10 22:37:05 +08:00
|
|
|
|
|
|
|
export function ParseMime(text)
|
|
|
|
{
|
|
|
|
class MimePart
|
|
|
|
{
|
2022-08-31 22:03:22 +08:00
|
|
|
/*
|
|
|
|
constructor() {
|
|
|
|
this.id = 0;
|
|
|
|
this.start = 0;
|
|
|
|
this.end = 0;
|
|
|
|
this.parts = [];
|
|
|
|
this.bodyStart = 0;
|
|
|
|
this.bodyEnd = 0;
|
|
|
|
this.boundary = '';
|
|
|
|
this.bodyText = '';
|
2024-02-05 06:03:53 +08:00
|
|
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6
|
|
|
|
this.headers = {
|
|
|
|
// Required
|
|
|
|
date = null,
|
|
|
|
from = [], // mailbox-list
|
|
|
|
// Optional
|
|
|
|
sender = [], // MUST occur with multi-address
|
|
|
|
'reply-to' = [], // address-list
|
|
|
|
to = [], // address-list
|
|
|
|
cc = [], // address-list
|
|
|
|
bcc = [], // address-list
|
|
|
|
'message-id' = '', // msg-id SHOULD be present
|
|
|
|
'in-reply-to' = '', // 1*msg-id SHOULD occur in some replies
|
|
|
|
references = '', // 1*msg-id SHOULD occur in some replies
|
|
|
|
subject = '', // unstructured
|
|
|
|
// Optional unlimited
|
|
|
|
comments = [], // unstructured
|
|
|
|
keywords = [], // phrase *("," phrase)
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6.6
|
|
|
|
trace = [],
|
|
|
|
'resent-date' = [],
|
|
|
|
'resent-from' = [],
|
|
|
|
'resent-sender' = [],
|
|
|
|
'resent-to' = [],
|
|
|
|
'resent-cc' = [],
|
|
|
|
'resent-bcc' = [],
|
|
|
|
'resent-msg-id' = [],
|
|
|
|
// optional others outside RFC2822
|
|
|
|
'mime-version' = '',
|
|
|
|
'content-transfer-encoding' = '',
|
|
|
|
'content-type' = '',
|
|
|
|
'delivered-to' = '', // angle-addr
|
|
|
|
'return-path' = '', // angle-addr
|
|
|
|
'received' = [],
|
|
|
|
'authentication-results' = '', // dkim, spf, dmarc
|
|
|
|
'dkim-signature' = '',
|
|
|
|
'x-rspamd-queue-id' = '',
|
|
|
|
'x-rspamd-action' = '',
|
|
|
|
'x-spamd-bar' = '',
|
|
|
|
'x-rspamd-server' = '',
|
|
|
|
'x-spamd-result' = '',
|
|
|
|
'x-remote-address' = '',
|
|
|
|
};
|
2022-08-31 22:03:22 +08:00
|
|
|
}
|
|
|
|
*/
|
|
|
|
|
2022-02-10 22:37:05 +08:00
|
|
|
header(name) {
|
2022-09-23 15:53:29 +08:00
|
|
|
return this.headers?.[name];
|
2022-02-10 22:37:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
headerValue(name) {
|
2022-08-31 04:27:07 +08:00
|
|
|
return this.header(name)?.value;
|
2022-02-10 22:37:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
get raw() {
|
|
|
|
return text.slice(this.start, this.end);
|
|
|
|
}
|
|
|
|
|
|
|
|
get bodyRaw() {
|
2022-02-11 18:01:07 +08:00
|
|
|
return text.slice(this.bodyStart, this.bodyEnd);
|
2022-02-10 22:37:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
get body() {
|
|
|
|
let body = this.bodyRaw,
|
2022-11-15 17:38:43 +08:00
|
|
|
charset = this.header('content-type')?.params.charset,
|
2022-02-10 22:37:05 +08:00
|
|
|
encoding = this.headerValue('content-transfer-encoding');
|
|
|
|
if ('quoted-printable' == encoding) {
|
2022-11-15 17:38:43 +08:00
|
|
|
body = QPDecode(body);
|
2022-02-10 22:37:05 +08:00
|
|
|
} else if ('base64' == encoding) {
|
2024-02-05 06:03:53 +08:00
|
|
|
body = BDecode(body.replace(/\r?\n/g, ''));
|
2022-08-31 22:03:22 +08:00
|
|
|
}
|
2022-11-15 17:38:43 +08:00
|
|
|
return decodeText(charset, body);
|
2022-02-10 22:37:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
get dataUrl() {
|
|
|
|
let body = this.bodyRaw,
|
|
|
|
encoding = this.headerValue('content-transfer-encoding');
|
|
|
|
if ('base64' == encoding) {
|
|
|
|
body = body.replace(/\r?\n/g, '');
|
|
|
|
} else {
|
|
|
|
if ('quoted-printable' == encoding) {
|
2022-11-15 17:38:43 +08:00
|
|
|
body = QPDecode(body);
|
2022-02-10 22:37:05 +08:00
|
|
|
}
|
2024-02-05 06:03:53 +08:00
|
|
|
body = BEncode(body);
|
2022-02-10 22:37:05 +08:00
|
|
|
}
|
|
|
|
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
|
|
|
}
|
|
|
|
|
|
|
|
forEach(fn) {
|
|
|
|
fn(this);
|
2022-08-31 22:03:22 +08:00
|
|
|
this.parts.forEach(part => part.forEach(fn));
|
2022-02-10 22:37:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
getByContentType(type) {
|
|
|
|
if (type == this.headerValue('content-type')) {
|
|
|
|
return this;
|
|
|
|
}
|
2022-08-31 22:03:22 +08:00
|
|
|
let i = 0, p = this.parts, part;
|
|
|
|
for (i; i < p.length; ++i) {
|
|
|
|
if ((part = p[i].getByContentType(type))) {
|
|
|
|
return part;
|
2022-02-10 22:37:05 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-05 06:03:53 +08:00
|
|
|
// mailbox-list or address-list
|
|
|
|
const lists = ['from','reply-to','to','cc','bcc'];
|
|
|
|
|
2022-02-10 22:37:05 +08:00
|
|
|
const ParsePart = (mimePart, start_pos = 0, id = '') =>
|
|
|
|
{
|
2022-08-31 04:27:07 +08:00
|
|
|
let part = new MimePart,
|
2022-09-23 15:53:29 +08:00
|
|
|
head = mimePart.match(/^[\s\S]+?\r?\n\r?\n/)?.[0],
|
2022-08-31 04:27:07 +08:00
|
|
|
headers = {};
|
2022-02-10 22:37:05 +08:00
|
|
|
if (id) {
|
|
|
|
part.id = id;
|
|
|
|
part.start = start_pos;
|
|
|
|
part.end = start_pos + mimePart.length;
|
|
|
|
}
|
2022-08-31 22:03:22 +08:00
|
|
|
part.parts = [];
|
2022-02-10 22:37:05 +08:00
|
|
|
|
|
|
|
// get headers
|
|
|
|
if (head) {
|
|
|
|
head.replace(/\r?\n\s+/g, ' ').split(/\r?\n/).forEach(header => {
|
|
|
|
let match = header.match(/^([^:]+):\s*([^;]+)/),
|
|
|
|
params = {};
|
2022-09-23 15:53:29 +08:00
|
|
|
if (match) {
|
|
|
|
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
|
|
|
params[param[1].trim().toLowerCase()] = param[2].trim()
|
|
|
|
);
|
2024-02-05 06:03:53 +08:00
|
|
|
let field = match[1].trim().toLowerCase();
|
|
|
|
if (lists.includes(field)) {
|
|
|
|
match[2] = addressparser(match[2]);
|
|
|
|
} else if ('keywords' === field) {
|
|
|
|
match[2] = match[2].split(',').forEach(entry => decodeEncodedWords(entry.trim()));
|
|
|
|
match[2] = (headers[field]?.value || []).concat(match[2]);
|
|
|
|
} else {
|
|
|
|
match[2] = decodeEncodedWords(match[2].trim());
|
|
|
|
if ('comments' === field) {
|
|
|
|
match[2] = (headers[field]?.value || []).push(match[2]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
headers[field] = {
|
2022-11-14 20:49:43 +08:00
|
|
|
value: match[2],
|
2022-09-23 15:53:29 +08:00
|
|
|
params: params
|
|
|
|
};
|
|
|
|
}
|
2022-02-10 22:37:05 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
// get body
|
2022-02-11 18:01:07 +08:00
|
|
|
part.bodyStart = start_pos + head.length;
|
|
|
|
part.bodyEnd = start_pos + mimePart.length;
|
2022-02-10 22:37:05 +08:00
|
|
|
|
2022-02-11 18:01:07 +08:00
|
|
|
// get child parts
|
2022-09-23 15:53:29 +08:00
|
|
|
let boundary = headers['content-type']?.params.boundary;
|
2022-02-10 22:37:05 +08:00
|
|
|
if (boundary) {
|
|
|
|
part.boundary = boundary;
|
|
|
|
let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'),
|
|
|
|
body = mimePart.slice(head.length),
|
|
|
|
bodies = body.split(regex),
|
2022-02-11 18:01:07 +08:00
|
|
|
pos = part.bodyStart;
|
2022-02-10 22:37:05 +08:00
|
|
|
[...body.matchAll(regex)].forEach(([boundary], index) => {
|
|
|
|
if (!index) {
|
2022-02-11 18:01:07 +08:00
|
|
|
// Mostly something like: "This is a multi-part message in MIME format."
|
|
|
|
part.bodyText = bodies[0];
|
2022-02-10 22:37:05 +08:00
|
|
|
}
|
2022-02-11 18:01:07 +08:00
|
|
|
// Not the end?
|
|
|
|
if ('--' != boundary.trim().slice(-2)) {
|
|
|
|
pos += bodies[index].length + boundary.length;
|
|
|
|
part.parts.push(ParsePart(bodies[1+index], pos, ((id ? id + '.' : '') + (1+index))));
|
2022-02-10 22:37:05 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-09-23 15:53:29 +08:00
|
|
|
part.headers = headers;
|
|
|
|
}
|
2022-08-31 04:27:07 +08:00
|
|
|
|
2022-02-10 22:37:05 +08:00
|
|
|
return part;
|
|
|
|
};
|
|
|
|
|
|
|
|
return ParsePart(text);
|
|
|
|
}
|