Skip to content

Plain text emails break verification/reset URLs due to quoted-printable line wrapping #244

@psbanka

Description

@psbanka

Plain text emails break verification/reset URLs due to quoted-printable line wrapping

👋 Hi, folks. I encountered this issue when trying to stand up a self-hosted stoat server. I hope this bug-report is helpful.

Description

Email verification and password reset emails sent by Stoat contain broken URLs due to quoted-printable encoding line wrapping. This makes it impossible for users to click the links without manually reconstructing the URLs.

Environment

  • Stoat version: v0.11.1
  • Email transport: SMTP with TLS (Postfix)

Steps to Reproduce

  1. Configure Stoat with email verification enabled:

    [features]
    email_verification = true
    
    [api.smtp]
    host = "mail.example.com"
    port = 465
    username = "stoat@example.com"
    password = "..."
    from_address = "noreply@example.com"
    use_tls = true
  2. Register a new user account

  3. Check the verification email received

Expected Behavior

Email should contain a clickable URL that users can click to verify their account or reset their password.

Actual Behavior

The email contains a plain text URL that is broken across multiple lines with quoted-printable soft line breaks:

Please navigate to: https://chat.example.com/login/reset/utdObONblhEMT69f=
XYVtMybuz7bDZTRf

The = at the end of the line is a quoted-printable continuation marker, not part of the URL. Email clients display this as-is, making the link unclickable.

Full Email Example

You requested a password reset, if you did not perform this action you can =
safely ignore this email.

Please navigate to: https://chat.example.com/login/reset/utdObONblhEMT69f=
XYVtMybuz7bDZTRf

This email is intended for user@example.com

This email has no association with Stoat or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html

Technical Details

The issue occurs because:

  1. Stoat sends emails as Content-Type: text/plain
  2. SMTP applies Content-Transfer-Encoding: quoted-printable
  3. Quoted-printable has a 76-character line limit
  4. Long URLs get wrapped with = as soft line breaks
  5. Email clients don't automatically unwrap these for URLs

URL breakdown:

  • Base URL: https://chat.example.com/login/reset/ (46 chars)
  • Token: 32 characters
  • Total: 78 characters (exceeds 76-char limit)

Impact

Critical usability issue:

  • New users cannot verify their email addresses without manually reconstructing URLs
  • Users cannot reset passwords without technical knowledge
  • Makes the platform effectively unusable for non-technical users

Workaround (for users)

Users must manually:

  1. Copy the text before the =: https://chat.example.com/login/reset/utdObONblhEMT69f
  2. Copy the next line: XYVtMybuz7bDZTRf
  3. Remove the = and join: https://chat.example.com/login/reset/utdObONblhEMT69fXYVtMybuz7bDZTRf

This is not acceptable UX for most users.

Proposed Solutions

Option 1: HTML Emails (Recommended)

Send emails as text/html or multipart/alternative with both HTML and plain text:

<html>
<body>
<p>You requested a password reset, if you did not perform this action you can safely ignore this email.</p>

<p><a href="https://chat.example.com/login/reset/utdObONblhEMT69fXYVtMybuz7bDZTRf">Reset your password</a></p>

<p>Or copy this link: https://chat.example.com/login/reset/utdObONblhEMT69fXYVtMybuz7bDZTRf</p>

<p>This email is intended for user@example.com</p>
</body>
</html>

Pros:

  • Clickable links
  • Better UX
  • No line wrapping issues
  • Standard practice for email services

Cons:

  • Slightly more complex templates
  • Need HTML email library

Option 2: Base64 Transfer Encoding

Force Content-Transfer-Encoding: base64 instead of quoted-printable to avoid line wrapping in the content.

Pros:

  • No line breaks in content
  • Works with plain text

Cons:

  • Entire email is base64-encoded (less readable in raw form)
  • Links still not clickable in plain text

Option 3: Shorter Tokens

Use shorter verification tokens (e.g., 20 characters instead of 32) to keep URLs under 76 characters.

Pros:

  • Simple fix
  • Works with current plain text emails

Cons:

  • Reduces security (fewer bits of entropy)
  • Only helps if total URL stays under 76 chars
  • Doesn't scale for longer domain names

Recommendation

Implement Option 1 (HTML emails). This is the industry standard for any service that sends links via email. Modern email clients expect HTML, and it solves the problem completely.

Example libraries that support HTML emails:

  • Rust: lettre supports HTML emails via Message::builder().singlepart(...)
  • Most SMTP libraries have HTML email support

Additional Context

This issue affects all self-hosted instances with domain names longer than ~30 characters. Given that verification and password reset are core security features, this significantly impacts the usability of self-hosted Stoat installations.


For Stoat Maintainers

If you'd like help implementing HTML email support, I'd be happy to contribute a PR. The main changes would be:

  1. Create HTML email templates
  2. Update the email sending code to use multipart/alternative with both HTML and plain text versions
  3. Ensure links are properly formatted with full URLs

Please let me know if you need any additional information or testing assistance.

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