|
@@ -121,6 +121,50 @@ const outputs = [
|
|
|
/**
|
|
/**
|
|
|
* Parse MIME multipart content and extract attachments
|
|
* Parse MIME multipart content and extract attachments
|
|
|
*/
|
|
*/
|
|
|
|
|
+// A header value may be folded across lines: RFC 5322 continues one wherever a
|
|
|
|
|
+// line begins with whitespace. Every regex below reads a value with a class
|
|
|
|
|
+// that stops at \r or \n, so a folded value used to be captured only as far as
|
|
|
|
|
+// the first line - which for a long filename means losing the end of it,
|
|
|
|
|
+// including the extension.
|
|
|
|
|
+function unfoldHeaders(text) {
|
|
|
|
|
+ return String(text || '').replace(/\r?\n[ \t]+/g, ' ');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// A filename with anything outside ASCII arrives as an RFC 2047 encoded word:
|
|
|
|
|
+// =?UTF-8?Q?S=C3=A1rk=C3=A1nyok_p=C3=A1rz=C3=A1si...?=
|
|
|
|
|
+// Undecoded, that string does not end in ".pdf", so an extension filter drops
|
|
|
|
|
+// the attachment and the workflow proceeds as though the mail had none. Any
|
|
|
|
|
+// attachment with an accented name has always been lost this way.
|
|
|
|
|
+function decodeEncodedWords(text) {
|
|
|
|
|
+ var value = String(text || '');
|
|
|
|
|
+ if (value.indexOf('=?') === -1) {
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+ // Adjacent encoded words are joined without the whitespace between them,
|
|
|
|
|
+ // which is what the standard says to do when a long value was split.
|
|
|
|
|
+ value = value.replace(/\?=\s+=\?/g, '?==?');
|
|
|
|
|
+ return value.replace(/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g, function (all, charset, kind, payload) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (kind === 'B' || kind === 'b') {
|
|
|
|
|
+ return smartbotic.utils.base64Decode(payload);
|
|
|
|
|
+ }
|
|
|
|
|
+ // Q encoding: underscores are spaces, =XX is a byte.
|
|
|
|
|
+ var text = payload.replace(/_/g, ' ');
|
|
|
|
|
+ var bytes = text.replace(/=([0-9A-Fa-f]{2})/g, function (m, hex) {
|
|
|
|
|
+ return String.fromCharCode(parseInt(hex, 16));
|
|
|
|
|
+ });
|
|
|
|
|
+ // The bytes are UTF-8; decodeURIComponent turns them into characters.
|
|
|
|
|
+ try {
|
|
|
|
|
+ return decodeURIComponent(escape(bytes));
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return bytes;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return all;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function parseMimeContent(rawEmail) {
|
|
function parseMimeContent(rawEmail) {
|
|
|
const attachments = [];
|
|
const attachments = [];
|
|
|
|
|
|
|
@@ -149,7 +193,8 @@ function parseMimeContent(rawEmail) {
|
|
|
|
|
|
|
|
if (actualHeaderEnd === -1) continue;
|
|
if (actualHeaderEnd === -1) continue;
|
|
|
|
|
|
|
|
- const headers = part.substring(0, actualHeaderEnd);
|
|
|
|
|
|
|
+ // Unfolded before anything reads a value out of it - see unfoldHeaders.
|
|
|
|
|
+ const headers = unfoldHeaders(part.substring(0, actualHeaderEnd));
|
|
|
const body = part.substring(actualHeaderEnd + (headerEndIndex !== -1 ? 4 : 2));
|
|
const body = part.substring(actualHeaderEnd + (headerEndIndex !== -1 ? 4 : 2));
|
|
|
|
|
|
|
|
// Check if this is an attachment
|
|
// Check if this is an attachment
|
|
@@ -171,9 +216,9 @@ function parseMimeContent(rawEmail) {
|
|
|
filename = filenameStarMatch[1];
|
|
filename = filenameStarMatch[1];
|
|
|
}
|
|
}
|
|
|
} else if (filenameMatch) {
|
|
} else if (filenameMatch) {
|
|
|
- filename = filenameMatch[1];
|
|
|
|
|
|
|
+ filename = decodeEncodedWords(filenameMatch[1]);
|
|
|
} else if (nameMatch) {
|
|
} else if (nameMatch) {
|
|
|
- filename = nameMatch[1];
|
|
|
|
|
|
|
+ filename = decodeEncodedWords(nameMatch[1]);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Determine if this is an attachment (has filename or Content-Disposition: attachment)
|
|
// Determine if this is an attachment (has filename or Content-Disposition: attachment)
|