hydroxide icon indicating copy to clipboard operation
hydroxide copied to clipboard

Retrieved encrypted messages have invalid-formatted extraneous multipart/mixed header

Open GalaxyShard opened this issue 3 months ago • 0 comments

Mime-Version: 1.0
Message-Id: <>
To: "" <>
From: ""
 <>
Subject: ...
Date: Fri, 12 Sep 2025 12:00:00 +0000
Content-Type: multipart/mixed;
 boundary=ed844fd9ac5375a0ab9

--ed844fd9ac5375a0ab9
Content-Type: multipart/mixed;
 boundary=b06d2d0f93a6bae9d0916f35da90fe53fff776f64b84919f12879e0e00b3;
 charset=utf-8

Content-Type: multipart/mixed; boundary="O04e4NugXPgiR";
 protected-headers="v1"
Subject: Re: subject
From: ""
 <>
To: ___ <>
Message-ID: <>
References: <>
In-Reply-To: <>

--O04e4NugXPgiR
[...]

This is the start of an email (email addresses removed and viewing the email in raw form) that Thunderbird recieved from hydroxide. On line 12 a seemingly random multipart/mixed header is added without any terminator; either it shouldn't be there at all, or there should be a --b06d2d0f93a6bae9d0916f35da90fe53fff776f64b84919f12879e0e00b3 on the line before the next set of headers and there should be a matching --b06d2d0f93a6bae9d0916f35da90fe53fff776f64b84919f12879e0e00b3-- near the end of the message.

It appears the issue is in imap/message.go. The extraneous multipart/mixed header is created by the call to CreatePart and inlineHeader on line 266, the missing initial terminator should (as far as I can tell) be added right before the mbox.inlineBody/io.Copy call a couple lines of code down, and the missing final terminator should be added during/right before pw.Close().

I wrote two quick patches which solve the issue, although I'm not sure if it solves the root problem.

Patch for https://github.com/emersion/go-message:

diff --git a/writer.go b/writer.go
index 6a80da2..d97cf1f 100644
--- a/writer.go
+++ b/writer.go
@@ -100,6 +100,13 @@ func (w *Writer) Close() error {
        return nil
 }
 
+func (w *Writer) OutputBoundary() {
+       fmt.Fprintf(w.w, "--%s\r\n", w.mw.Boundary());
+}
+func (w *Writer) OutputClosingBoundary() {
+       fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.mw.Boundary());
+}
+
 // CreatePart returns a Writer to a new part in this multipart entity. If this
 // entity is not multipart, it fails. The body of the part should be written to
 // the returned io.WriteCloser.

Patch for this repository:

diff --git a/imap/message.go b/imap/message.go
index 5051259..661b05b 100644
--- a/imap/message.go
+++ b/imap/message.go
@@ -266,6 +266,7 @@ func (mbox *mailbox) fetchBodySection(msg *protonmail.Message, section *imap.Bod
                        if err != nil {
                                return nil, err
                        }
+                       pw.OutputBoundary();
                        pr, err := mbox.inlineBody(msg)
                        if err != nil {
                                return nil, err
@@ -273,6 +274,7 @@ func (mbox *mailbox) fetchBodySection(msg *protonmail.Message, section *imap.Bod
                        if _, err := io.Copy(pw, pr); err != nil {
                                return nil, err
                        }
+                       pw.OutputClosingBoundary();
                        pw.Close()
 
                        for _, att := range msg.Attachments {

GalaxyShard avatar Sep 12 '25 23:09 GalaxyShard