Pārlūkot izejas kodu

fix: an attachment with an accented filename is no longer silently discarded

A 41 KB PDF named "Sárkányok párzási és étkezési szokásaik.pdf" arrived, was
detected, and produced no reply and no error. The mail client had sent the name
as an RFC 2047 encoded word folded across two lines, as the standard says to for
anything outside ASCII:

  Content-Disposition: attachment;
   filename="=?UTF-8?Q?S=C3=A1rk=C3=A1nyok_p=C3=A1rz=C3=A1si_=C3=A9s_=C3=A9tkez?=
   =?UTF-8?Q?=C3=A9si_szok=C3=A1saik=2Epdf?="

Every header regex here reads its value with a class that stops at \r or \n, so
the name was captured only as far as the fold - a string ending in "?=" rather
than ".pdf". The MIME type filter passed and the extension filter rejected it.

Nothing was broken enough to report. The parser returned a name it had really
read, the filter correctly rejected a name that is not a PDF, and the workflow
correctly took its no-attachments branch and completed. Three reasonable
decisions and the mail went unanswered in silence.

This is not new and not from the move to containers: any attachment whose name
is not plain ASCII has always been lost this way. Headers are now unfolded
before anything reads them, and encoded words are decoded (both Q and B).

Confirmed against the message itself, fetched in peek mode so it stayed
untouched: with the filters off the part was already there as application/pdf,
42372 bytes, with the truncated name. The regression test uses a synthetic
message of the same shape so it does not depend on a mailbox, and keeps the
extension filter switched on, which is what actually fails without the decoding.

Also fixes a real bug introduced with the containers: the image built its user
with useradd --system, so the runner ran as uid 999 while the bind-mounted data
directory belongs to uid 1000 on the host, and it could not write attachments at
all. It is uid 1000 now. That was not the cause here - the node warns once and
keeps the bytes in memory - but it would have surfaced as soon as this PDF got
any further.
fszontagh 3 nedēļas atpakaļ
vecāks
revīzija
5756a799bd

+ 48 - 3
nodes/imap/imap-extract-attachments.js

@@ -121,6 +121,50 @@ const outputs = [
 /**
  * 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) {
     const attachments = [];
 
@@ -149,7 +193,8 @@ function parseMimeContent(rawEmail) {
 
         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));
 
         // Check if this is an attachment
@@ -171,9 +216,9 @@ function parseMimeContent(rawEmail) {
                 filename = filenameStarMatch[1];
             }
         } else if (filenameMatch) {
-            filename = filenameMatch[1];
+            filename = decodeEncodedWords(filenameMatch[1]);
         } else if (nameMatch) {
-            filename = nameMatch[1];
+            filename = decodeEncodedWords(nameMatch[1]);
         }
 
         // Determine if this is an attachment (has filename or Content-Disposition: attachment)

+ 7 - 1
packaging/Dockerfile.build

@@ -127,7 +127,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates
 # Without tzdata every zone falls back to UTC and a workflow set for 02:00
 # Europe/Budapest fires at the wrong hour, silently.
 
-RUN useradd --system --create-home --home-dir /var/lib/smartbotic --shell /usr/sbin/nologin smartbotic
+# uid/gid 1000 on purpose: the data directory is bind-mounted from the host,
+# where it belongs to the first real user. A --system account gets 999, and the
+# container then cannot write the volume - imap-extract-attachments logs one
+# warning, keeps the bytes in memory and carries on, so the failure is invisible.
+RUN groupadd --gid 1000 smartbotic \
+    && useradd --uid 1000 --gid 1000 --create-home --home-dir /var/lib/smartbotic \
+       --shell /usr/sbin/nologin smartbotic
 
 COPY --from=builder /build/build/smartbotic-webserver /usr/bin/
 COPY --from=builder /build/build/smartbotic-runner    /usr/bin/

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 23 - 0
tests/nodes/imap-extract-folded-encoded-filename.json


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels