Bug: forwarded emails with quoted-printable body are corrupted in strict MIME clients (Outlook)
Prerequisites
- I have searched open and closed issues to make sure this bug has not been reported yet.
Bug description
When an alias receives an HTML email encoded as quoted-printable (QP), and the alias has a forwarding banner enabled, SimpleLogin re-encodes the message body using Python's quopri.encodestring(). This function produces soft line breaks as =\n (Unix LF) instead of the =\r\n (CRLF) required by RFC 2045 §6.7 rule 5.
The resulting non-standard QP body is accepted by lenient clients (Apple Mail / iOS Mail) but causes strict clients such as Outlook (which uses a Word-based MIME renderer) to display raw = characters in the middle of every word, at ~76-character intervals, making the email completely unreadable.
Steps to reproduce
- Set up a self-hosted SimpleLogin instance (tested with the current Docker image).
- Create an alias with the default forwarding banner enabled.
- Send an HTML email to the alias from any provider that uses Content-Transfer-Encoding: quoted-printable (e.g. Google / Gmail notifications, Amazon SES newsletters).
- Open the forwarded email in Microsoft Outlook (desktop, Windows).
- Observe corrupted body: "=vance" instead of "avance", "yo=tube" instead of "youtube", "Conditions ='utilisation" instead of "Conditions d'utilisation", etc. The corruption occurs at exactly every ~75–76 characters. Expected behavior
The forwarded email body should be readable in all RFC-compliant email clients, including Microsoft Outlook.
Actual behavior
The body is corrupted with stray = characters at regular intervals (~76 chars). The email is readable on iOS Mail (lenient QP decoder) but broken in Outlook (strict RFC 2045 decoder).
Example (Google account notification email, forwarded through alias):
CORRUPTED (received in Outlook): Tous les deux ans environ, nous mettons à jour nos Conditions ='utilisation. Nous tenions à vous informer à l'=vance que la prochaine mise à jour aura lieu le 30 juill=t 2026.
EXPECTED: Tous les deux ans environ, nous mettons à jour nos Conditions d'utilisation. Nous tenions à vous informer à l'avance que la prochaine mise à jour aura lieu le 30 juillet 2026. Root cause (code analysis)
The bug is in app/email_utils.py, function add_header(), which is called every time SimpleLogin prepends a forwarding banner to a message. For text/html and text/plain parts, it:
- Reads the encoding: encoding = get_encoding(msg) → EmailEncoding.QUOTED
- Gets the raw QP payload: payload = msg.get_payload()
- Decodes it: decode_text(payload, encoding) → quopri.decodestring(...) ✓
- Prepends the banner text
- Re-encodes: encode_text(new_payload, encoding)
Step 5 calls:
def encode_text(text, encoding): if encoding == EmailEncoding.QUOTED: encoded = quopri.encodestring(text.encode("utf-8")) # ← BUG HERE return str(encoded, "utf-8")
Python's quopri.encodestring() uses b'\n' as the soft-line-break separator. RFC 2045 §6.7 rule 5 mandates b'=\r\n'. The resulting =\n in the body is technically malformed QP. iOS Mail tolerates it; Outlook does not.
Proposed fix
In app/email_utils.py, function encode_text(), replace the soft line breaks after encoding:
def encode_text(text: str, encoding: EmailEncoding = EmailEncoding.NO) -> str: if encoding == EmailEncoding.QUOTED: encoded = quopri.encodestring(text.encode("utf-8")) # Fix: RFC 2045 §6.7 requires =\r\n for soft line breaks, not =\n encoded = encoded.replace(b"=\n", b"=\r\n") return str(encoded, "utf-8") ...
Alternatively, switching the re-encoding to base64 for HTML parts would be more robust and avoid this class of issue entirely (base64 is unambiguous and universally supported), but would require updating the Content-Transfer-Encoding header accordingly.
Environment
- SimpleLogin: self-hosted (latest Docker image, simplelogin/app)
- Mail server: Stalwart SMTP
- Affected email client: Microsoft Outlook (desktop, Windows)
- Unaffected client: Apple Mail / iOS Mail (lenient QP decoder)
- Affected encodings: Content-Transfer-Encoding: quoted-printable
- Trigger: alias with forwarding banner enabled (add_header() is called)
- NOT triggered: alias without banner / direct relay without body modification Additional notes
The corruption reproduces with emails from completely independent senders (Google account notifications and Amazon SES newsletters), which rules out a sender-side bug. The common factor is the alias forwarding pipeline.
Source: simple-login/app