-
Notifications
You must be signed in to change notification settings - Fork 256
Description
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
-
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
-
Register a new user account
-
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:
- Stoat sends emails as
Content-Type: text/plain - SMTP applies
Content-Transfer-Encoding: quoted-printable - Quoted-printable has a 76-character line limit
- Long URLs get wrapped with
=as soft line breaks - 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:
- Copy the text before the
=:https://chat.example.com/login/reset/utdObONblhEMT69f - Copy the next line:
XYVtMybuz7bDZTRf - 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:
lettresupports HTML emails viaMessage::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:
- Create HTML email templates
- Update the email sending code to use
multipart/alternativewith both HTML and plain text versions - Ensure links are properly formatted with full URLs
Please let me know if you need any additional information or testing assistance.