Skip to content

fix(imap): verify IMAP TLS certificate and report Process Now errors - #37

Open
askmoai wants to merge 2 commits into
LeonMusCoden:masterfrom
askmoai:fix/imap-tls-verification
Open

askmoai wants to merge 2 commits into
LeonMusCoden:masterfrom
askmoai:fix/imap-tls-verification

Conversation

@askmoai

@askmoai askmoai commented Nov 2, 2026

Copy link
Copy Markdown

Problem

LetterFeed connects to the IMAP server with imaplib.IMAP4_SSL(server) without an ssl_context. In that case Python uses ssl._create_unverified_context(), so neither the certificate chain nor the hostname is checked. Anyone who can intercept the connection between LetterFeed and the mail server (e.g. on the network path or via DNS) can present any certificate and read the IMAP username and password.

Related: when fetching fails ("Process Now"), the UI still reports success. Connection, TLS and login errors only show up in the backend log.

Changes

  1. fix(imap): verify IMAP server TLS certificate and hostname
    • New connect_imap() in app/core/imap.py, used by the connection test, the folder list and email processing. It passes ssl.create_default_context(), which verifies the certificate against the system CA store and checks the hostname.
    • A certificate error is reported as a readable message, e.g. TLS certificate verification failed for imap.example.com: self-signed certificate. If the server uses a self-signed certificate, set LETTERFEED_IMAP_TLS_INSECURE=true.
    • Opt-out for servers with a self-signed certificate: LETTERFEED_IMAP_TLS_INSECURE=true (default false, logs a warning when enabled). Documented in .env.example and the README.
  2. fix: show IMAP processing errors when using Process Now
    • process_emails() still processes all folders, but afterwards raises EmailProcessingError with the collected errors (connection/TLS/login failure, folder that cannot be selected, errors while processing a folder). Repeated identical errors are reported once.
    • POST /imap/process returns these as 400 with the message in detail, the same way POST /imap/test reports a failed connection test. The existing frontend error handling already shows detail as an error toast, so no frontend change is needed.
    • The scheduled job logs the error as a warning.
    • Messages contain the server's error text and never the IMAP password.

Behavior change

Users whose IMAP server has a self-signed certificate, an expired certificate, or a certificate for a different hostname (e.g. configured by IP address) will see the connection fail after upgrading. They should configure the hostname the certificate was issued for, or set LETTERFEED_IMAP_TLS_INSECURE=true on the backend.

Testing

  • Backend: ruff check app, ruff format --check app and pytest pass (79 tests, 8 new: verifying context, opt-out, certificate error message, connection/login/folder errors in process_emails, 400 from /imap/process, scheduler warning).
  • Manual tests with Docker builds containing these changes:
    • imap.gmail.com and outlook.office365.com (connection only, no login): TLS connection succeeds with verification enabled;
    • a TLS endpoint with a self-signed certificate and imap.gmail.com addressed by IP: rejected with the certificate error (before the change both were accepted silently); with LETTERFEED_IMAP_TLS_INSECURE=true the TLS handshake succeeds again;
    • GreenMail IMAP server behind a TLS proxy with a certificate from a trusted test CA: connection test and Process Now succeed and emails are fetched;
    • same server behind a proxy with a self-signed certificate, an unknown host and a wrong password: Process Now now shows Could not connect to IMAP server: ... as an error toast instead of the success message; switching back to the valid server fetches emails again.

imaplib.IMAP4_SSL() without an ssl_context falls back to an unverified
context, so any certificate is accepted and a man-in-the-middle can read
the IMAP credentials.

All IMAP connections (connection test, folder list, email processing) now
go through connect_imap(), which uses ssl.create_default_context() and
therefore verifies the certificate chain and the hostname. A certificate
error is reported with a readable message.

Servers with a self-signed certificate can opt out explicitly with
LETTERFEED_IMAP_TLS_INSECURE=true.
Connection, login and mailbox selection errors were only logged, and
POST /imap/process always answered with success, so "Process Now" in the
UI reported success even when nothing could be fetched.

process_emails() still processes all folders, but now raises
EmailProcessingError with the collected error messages afterwards. The
endpoint returns it as a 400 response with the message in "detail", the
same way the connection test does, and the frontend shows it as an error
toast. The scheduled job logs the error as a warning.
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.

1 participant