Prerequisites
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.
Prerequisites
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
Content-Transfer-Encoding: quoted-printable
(e.g. Google / Gmail notifications, Amazon SES newsletters).
"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:
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
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.