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

Open Beginner friendly
#2,758 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
python
Domain
backend

Research direction

Start in app/email_utils.py with encode_text() and trace its call from add_header(). Reproduce a quoted-printable HTML message with a forwarding banner, then verify that re-encoded soft line breaks use RFC 2045-compliant CRLF separators and that the forwarded body remains readable in strict clients such as Outlook.

Written by the indexing model from the issue text.

Description

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.

Dominant language
Python
Stars
7k
Forks
650
Avg merge
18h 53m
Merged PRs (30d)
9

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from simple-login/app

All issues in simple-login/app

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.