Skip to content

Bug: forwarded emails with quoted-printable body are corrupted in strict MIME clients (Outlook) #2758

Description

@Flyingfufu

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

  1. Set up a self-hosted SimpleLogin instance (tested with the current Docker image).
  2. Create an alias with the default forwarding banner enabled.
  3. 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).
  4. Open the forwarded email in Microsoft Outlook (desktop, Windows).
  5. 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:

  1. Reads the encoding: encoding = get_encoding(msg) → EmailEncoding.QUOTED
  2. Gets the raw QP payload: payload = msg.get_payload()
  3. Decodes it: decode_text(payload, encoding) → quopri.decodestring(...) ✓
  4. Prepends the banner text
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions