Skip to content

feat(providers): add one-click List-Unsubscribe to mail messages - #61

Merged
joaovitoriasilva merged 2 commits into
mainfrom
feat/issue_56
Nov 7, 2026
Merged

joaovitoriasilva merged 2 commits into
mainfrom
feat/issue_56

Conversation

@joaovitoriasilva

Copy link
Copy Markdown
Member

What does this PR do?

Adds MailMessage.list_unsubscribe: str | None = None. When set, build_mime emits:

List-Unsubscribe: <https://...>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

This is the RFC 8058 one-click unsubscribe that Gmail and Yahoo require of bulk senders.

  • Validation: only an absolute https URL with a host, printable ASCII, no spaces, <, > or ,, at most 978 characters. Anything else is a ValueError that does not echo the value.
  • The header is written verbatim, on one line. policy.SMTP wraps at 78 characters, and a long value with no spaces is turned into RFC 2047 encoded-words (=?utf-8?q?=3Chttps=3A//...?=), which a mailbox provider cannot read as a URL. List-Unsubscribe therefore uses a small header class (_VerbatimHeader) registered on a HeaderRegistry of JASIL's own. Every other header folds exactly as before.
  • 978 characters is what lets List-Unsubscribe: <url> fit in RFC 5322's 998-character line limit (MAX_UNSUBSCRIBE_URL_LENGTH, computed from that).
  • Docs: a "One-click unsubscribe" subsection under MailProvider, and the threat model's header-injection paragraph.

Additive: a new MailMessage field with a default.

Related issue

Closes #56

Type of change

  • Bug fix
  • New feature (exception during freeze; link prior discussion/acknowledgment)
  • Documentation
  • Refactor / code quality
  • Translation
  • Other (describe below)

Checklist

  • This PR is focused on a single concern
  • An issue was opened and acknowledged before writing this code (if not a small fix)
  • If this PR introduces a new feature during freeze, it was discussed and explicitly acknowledged first
  • The PR is under ~300 lines changed, excluding lock files, migrations, and generated code
  • Tests have been added or updated where applicable
  • Documentation has been updated where applicable
  • Commit messages follow Conventional Commits format

Notes for the reviewer

  • Rejected alternative: raising max_line_length on the whole MIME policy to 998. I tested it: base64 body lines became 996 characters (RFC 2045 allows 76), and a non-ASCII subject became a 988-character encoded-word (RFC 2047 allows 75). It would make normal mail non-compliant.
  • URL limit: the issue proposed 2048 characters; it is 978 here, because the header must fit in one 998-character line.
  • HeaderRegistry: the default one is a class attribute shared by every EmailPolicy in the process, so _VerbatimHeader is registered on a separate instance and the host's own email parsing is unaffected.
  • Not enforced: the RFC 8058 DKIM requirement (the relay's job) and that the endpoint accepts an unauthenticated POST. Both are documented. mailto: is out of scope.
  • Conflicts: this PR will conflict with feat(providers): let a mail message choose its sender #60 in MailMessage / __hash__ and with the others in CHANGELOG.md. I'll rebase as they merge.

Checks run: ruff, ruff format, lint-imports, mypy, pytest (3662 passed, 139 skipped, 97.80% coverage), check_module_coverage.py, check_docs_api.py.

🤖 Generated with Claude Code

MailMessage.list_unsubscribe emits List-Unsubscribe and
List-Unsubscribe-Post (RFC 8058). The SMTP email policy folds a long
token-only header value into RFC 2047 encoded-words, which a mailbox
provider cannot read as a URL, so List-Unsubscribe is written by a
header class of its own that never folds, and the URL is capped so the
line fits in 998 characters. Raising the whole policy's line length
instead would break RFC 2045 (base64/quoted-printable body lines) and
RFC 2047 (encoded-word length) limits.

Closes #56

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 29 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 865be93d-33ef-43ed-b599-47a7d9c016dc
Commits

Reviewing files that changed from the base of the PR and between f2b4c19 and 0d0c803.

Files selected for processing: 6

  • CHANGELOG.md
  • docs/providers-and-backends.md
  • docs/security/threat-model.md
  • jasil/_core/mail_message.py
  • jasil/providers.py
  • tests/test_mail.py
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

# Conflicts:
#	CHANGELOG.md
#	docs/providers-and-backends.md
#	jasil/providers.py
#	tests/test_mail.py
@joaovitoriasilva
joaovitoriasilva merged commit d5eb141 into main Nov 7, 2026
20 checks passed
@joaovitoriasilva
joaovitoriasilva deleted the feat/issue_56 branch November 7, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: One-click List-Unsubscribe on MailMessage (RFC 8058)

1 participant