fix(mime): fall back gracefully on malformed base64 instead of aborting the parse

ober

3148912301087c3644f02db97de6d2943325ca2a

diff --git a/jerboa-mail/encoding.ss b/jerboa-mail/encoding.ss
index d68084f..893e710 100644
--- a/jerboa-mail/encoding.ss
+++ b/jerboa-mail/encoding.ss
@@ -68,7 +68,15 @@
                         "base64 payload"
                         s
                         mail-max-transfer-encoded-chars)
-    (u8vector->utf8-string (base64-string->u8vector (strip-ascii-whitespace s))))
+    (let ([stripped (strip-ascii-whitespace s)])
+      ;; base64-string->u8vector raises on malformed input (an invalid
+      ;; character or a length that is 1 mod 4). A hostile mail must not abort
+      ;; the whole parse -- which would also discard every sibling part of a
+      ;; multipart message -- so fall back to the undecoded payload, mirroring
+      ;; the lossy fallbacks already used for quoted-printable and UTF-8,
+      ;; instead of propagating the error.
+      (guard (_ [#t stripped])
+        (u8vector->utf8-string (base64-string->u8vector stripped)))))
 
   (define (hex-value ch)
     (cond
diff --git a/test/test-all.ss b/test/test-all.ss
index 5b7f897..1f2a5df 100644
--- a/test/test-all.ss
+++ b/test/test-all.ss
@@ -89,6 +89,34 @@
 (check "base64 invalid UTF-8 returns a string"
        (string? (mail-base64-decode-string "w0E=")))
 
+;; Malformed base64 (bad char / bad length) must fall back to the undecoded
+;; payload instead of raising, mirroring the QP and UTF-8 lossy fallbacks.
+(check "base64 malformed chars fall back instead of raising"
+       (string? (mail-base64-decode-string "!!!!not-base64!!!!")))
+
+(check "base64 malformed length falls back instead of raising"
+       (string? (mail-base64-decode-string "A")))
+
+;; A malformed base64 body must not abort the whole parse: the message still
+;; parses and the undecoded payload is retained as the body.
+(let* ([raw "Content-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\n@@@@"]
+       [msg (mail-parse-message raw)])
+  (check "parse survives malformed base64 body"
+         (string=? "@@@@" (mail-best-text-body msg))))
+
+;; A single malformed base64 part must not discard its sibling parts.
+(let* ([raw (string-append
+              "Content-Type: multipart/mixed; boundary=\"b\"\r\n\r\n"
+              "--b\r\nContent-Type: text/plain\r\n\r\ngood part\r\n"
+              "--b\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: base64\r\n\r\n@@@@\r\n"
+              "--b--\r\n")]
+       [msg (mail-parse-message raw)])
+  (check "multipart keeps all parts despite one malformed base64 part"
+         (= 2 (length (mail-message-parts msg))))
+  (check "multipart good sibling part still readable"
+         (string=? "good part"
+                   (mail-message-body (car (mail-message-parts msg))))))
+
 (check "encoded-word drops whitespace between adjacent words"
        (string=? "HelloWorld"
                  (mail-decode-encoded-words "=?UTF-8?Q?Hello?= =?UTF-8?Q?World?=")))