Skip to content

Fix SSL error and improve authentication failure#495

Merged
sylvinus merged 4 commits into
mainfrom
sdem/imap_ssl_fix_and_auth_failure_enhance
Jan 24, 2026
Merged

Fix SSL error and improve authentication failure#495
sylvinus merged 4 commits into
mainfrom
sdem/imap_ssl_fix_and_auth_failure_enhance

Conversation

@sdemagny
Copy link
Copy Markdown
Contributor

@sdemagny sdemagny commented Jan 20, 2026

Fix https://sentry.incubateur.anct.gouv.fr/organizations/sentry/issues/14719/events/f5f190afa2544d0db3904ffd50896845/
Improve authentication failure handling

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened IMAP import connection security checks with clearer, actionable error messages for SSL/STARTTLS and port configurations.
    • Improved handling of STARTTLS negotiation and explicit failures when server capabilities or TLS upgrades are not supported.
    • Improved authentication failure handling with more informative messages and reliable cleanup on errors; ensures UTF‑8 is set on established connections.
  • Tests

    • Added comprehensive IMAP connection tests covering SSL, STARTTLS, unencrypted flows, authentication failures, and cleanup.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add STARTTLS negotiation for port 143 with use_ssl=True
- Create IMAPSecurityError exception for security violations
- Refuse to continue unencrypted when SSL is required
- Check server STARTTLS capability before attempting upgrade
- Fail fast to prevent credential leakage on security downgrade

Connection behavior:
- Port 993 + use_ssl=True → SSL direct
- Port 143 + use_ssl=True → STARTTLS (mandatory)
- Port 143 + use_ssl=False → unencrypted (explicit choice)

Fixes: SSL WRONG_VERSION_NUMBER error on port 143
Backend improvements:
- Catch SSL handshake failures during IMAP4_SSL connection
- Provide clear error message suggesting port 143 with STARTTLS
- Add authentication failure logging with username context
- Wrap login() in try-except to log auth errors before propagating

Frontend improvements:
- Detect "IMAP authentication failed" message in error responses
- Display user-friendly credential error message in UI

This helps users understand connection failures and authentication
issues with clearer, actionable error messages.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Adds a public IMAPSecurityError and explicit STARTTLS handling to IMAP connection logic, improves connection/cleanup flows and UTF-8 handling, extends frontend import auth-error detection, and adds comprehensive IMAP connection unit tests.

Changes

Cohort / File(s) Summary
Backend IMAP security & connection logic
src/backend/core/services/importer/imap.py
Adds IMAPSecurityError; adds STARTTLS negotiation when use_ssl with port 143; verifies STARTTLS capability; distinguishes SSL-direct vs STARTTLS paths; captures SSL handshake and STARTTLS failures and raises IMAPSecurityError; ensures UTF-8 on connection and attempts logout cleanup on failures.
Backend IMAP unit tests
src/backend/core/tests/importer/test_imap_connection.py
New tests covering SSL-direct success/failure, STARTTLS capability/negotiation success and failure (including empty/None caps), unencrypted mode, and auth-failure cleanup; mocks imaplib.IMAP4/IMAP4_SSL.
Frontend import error handling
src/frontend/src/features/controlled-modals/message-importer/step-loader.tsx
Expands authentication error matching to include both "AUTHENTICATIONFAILED" and "IMAP authentication failed" and consolidates displayed auth error message.
Minor test formatting
src/backend/core/tests/api/test_admin_maildomains_message_template.py
Non-functional formatting change: split test function signature across multiple lines (no logic change).

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant IMAPClient as IMAP Client
    participant IMAPServer as IMAP Server

    App->>IMAPClient: connect(host, port, use_ssl)
    IMAPClient->>IMAPServer: TCP connect / greeting

    alt use_ssl == true and port == 143 (STARTTLS path)
        rect rgba(100, 150, 200, 0.5)
        IMAPClient->>IMAPServer: CAPABILITY
        IMAPServer-->>IMAPClient: capabilities
        IMAPClient->>IMAPServer: STARTTLS
        IMAPServer-->>IMAPClient: OK / NO
        IMAPClient->>IMAPServer: TLS handshake (if OK)
        IMAPServer-->>IMAPClient: TLS established
        end
    else SSL-direct (SSL on connect)
        rect rgba(200, 100, 100, 0.5)
        IMAPClient->>IMAPServer: SSL socket handshake
        IMAPServer-->>IMAPClient: handshake OK / failure
        end
    end

    rect rgba(100, 200, 150, 0.5)
    IMAPClient->>IMAPServer: LOGIN username/password
    IMAPServer-->>IMAPClient: OK / NO
    end

    alt security or handshake failure
        IMAPClient->>App: raise IMAPSecurityError (logout attempted)
    else auth failure
        IMAPClient->>App: propagate auth error (logout attempted)
    else success
        IMAPClient->>App: return authenticated connection (UTF-8 enforced)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I hopped to check the mail tonight,
STARTTLS glowed in soft moonlight,
Handshakes snug, the tunnels tight,
If errors pop, I tidy flight,
Safe hops and logs — my code's delight.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix SSL error and improve authentication failure' partially relates to the changeset. It accurately references SSL/IMAP security improvements and authentication failure handling, which are central to the backend changes. However, it omits significant aspects: STARTTLS support addition, the new IMAPSecurityError exception, and the frontend UI enhancement for clearer error messaging.
Docstring Coverage ✅ Passed Docstring coverage is 81.25% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@sdemagny sdemagny marked this pull request as ready for review January 20, 2026 15:11
@sdemagny sdemagny requested a review from sylvinus January 20, 2026 16:07
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/backend/core/services/importer/imap.py`:
- Around line 128-133: The current exception handler logs the PII-containing
username (self.username) when self.connection.login raises imaplib.IMAP4.error;
remove the username from the log and instead log the mail server/port (e.g., use
self.host and self.port or connection.server attributes) along with the caught
exception object (e) in the logger.error call inside the except block that wraps
self.connection.login, and ensure no references to self.username or other PII
remain in that error message.
- Around line 101-121: The capability() and starttls() calls can raise
exceptions and currently are not exception-wrapped, so if they raise (e.g.,
imaplib.IMAP4.error or ssl.SSLError) the connection may never be logged out;
wrap the capability() call and the starttls() call in try/except blocks that on
any Exception call self.connection.logout() and re-raise an IMAPSecurityError
(or the original exception) with a descriptive message; also when parsing
capabilities, aggregate all response lines from data instead of only data[0]
(join/decode all items) to robustly detect "STARTTLS"; mirror the existing
login() cleanup pattern (use the same exception handling and logout behavior)
and reference the same symbols: self.connection.capability(),
self.connection.starttls(), self.connection.logout(), and IMAPSecurityError.

Comment on lines +101 to +121
typ, data = self.connection.capability()
capabilities = data[0].decode().upper() if data and data[0] else ""
if typ != "OK" or "STARTTLS" not in capabilities:
error_msg = (
f"Server {self.server}:{self.port} does not support STARTTLS. "
"Encrypted connection required."
)
logger.error(error_msg)
self.connection.logout()
raise IMAPSecurityError(error_msg)

# Attempt STARTTLS
status, response = self.connection.starttls()
if status != "OK":
error_msg = (
f"STARTTLS failed for {self.server}:{self.port}: {response}. "
"Encrypted connection required."
)
logger.error(error_msg)
self.connection.logout()
raise IMAPSecurityError(error_msg)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/backend/core/services/importer/imap.py | head -130 | tail -50

Repository: suitenumerique/messages

Length of output: 2950


🏁 Script executed:

wc -l src/backend/core/services/importer/imap.py

Repository: suitenumerique/messages

Length of output: 113


🌐 Web query:

imaplib.IMAP4 capability() return value structure Python documentation

💡 Result:

In imaplib, most IMAP4 commands (including CAPABILITY) return a 2‑tuple:

  • typ: usually 'OK' or 'NO'
  • data: a Python list of response items, where each item is typically bytes (sometimes a (header, literal) tuple for commands that return literals)【1】.

So, in practice, IMAP4.capability() returns something shaped like:

typ, data = M.capability()
# typ: 'OK' / 'NO'
# data: [b'IMAP4rev1 ...']   # a single bytes line with space-separated capabilities

To turn it into a list of capability tokens, you typically parse the first element, e.g. data[0].split() (after decoding if you want str).

Also note imaplib stores parsed capability tokens in the instance variable M.capabilities (used e.g. for AUTH=... mechanisms)【1】.

Sources: Python imaplib docs (response tuple format; capabilities attribute usage)【1】.

[1] https://docs.python.org/3/library/imaplib.html


🌐 Web query:

imaplib.IMAP4 starttls() exception handling Python

💡 Result:

imaplib.IMAP4.starttls() can fail in two main ways: imaplib-level protocol checks (raising IMAP4.error / IMAP4.abort) and TLS handshake/socket errors (raising ssl.SSLError, OSError, etc.).

What starttls() raises (in practice)

From the stdlib docs, the relevant exception hierarchy is: IMAP4.error (base), with IMAP4.abort as a subclass for server/protocol-level abort conditions. [1]

From the imaplib implementation, starttls() specifically raises: [2]

  • IMAP4.error('SSL support missing') if Python was built without SSL support.
  • IMAP4.abort('TLS session already established') if you call it twice.
  • IMAP4.abort('TLS not supported by server') if the server doesn’t advertise STARTTLS in capabilities.
  • IMAP4.error("Couldn't establish TLS session") if the server responds non-OK to the STARTTLS command.
  • Additionally, the actual TLS wrap/handshake (ssl_context.wrap_socket(...)) may raise ssl.SSLError or OSError during negotiation. [2]

A typical exception-handling pattern

import imaplib, ssl

M = imaplib.IMAP4(host, 143)

try:
    M.starttls(ssl.create_default_context())
except imaplib.IMAP4.abort as e:
    # e.g. "TLS not supported by server" / "TLS session already established"
    raise
except imaplib.IMAP4.error as e:
    # e.g. "SSL support missing" / "Couldn't establish TLS session"
    raise
except (ssl.SSLError, OSError) as e:
    # handshake / socket problems
    raise

If you require TLS, you can also pre-check b"STARTTLS" in M.capabilities (or just rely on the IMAP4.abort('TLS not supported by server') you’ll get). [2]

Sources: Python imaplib docs for exception types [1]; imaplib.starttls() implementation showing the exact raised conditions [2].


🏁 Script executed:

cat -n src/backend/core/services/importer/imap.py | head -145 | tail -70

Repository: suitenumerique/messages

Length of output: 3822


🏁 Script executed:

rg "def.*__init__|def.*connect" src/backend/core/services/importer/imap.py -A 2

Repository: suitenumerique/messages

Length of output: 456


Wrap capability() and starttls() in try-except to ensure connection cleanup.

Both capability() and starttls() can raise unexpected exceptions (e.g., imaplib.IMAP4.error, ssl.SSLError). If either raises an exception not caught by the current explicit status checks, the connection won't be logged out, leaving it open.

Also, for robustness, aggregate all capability lines instead of checking only data[0], in case a server returns multiple response items.

The pattern matches the login() handling at line 128, which properly wraps the call with exception cleanup.

♻️ Suggested fix
-                    typ, data = self.connection.capability()
-                    capabilities = data[0].decode().upper() if data and data[0] else ""
+                    try:
+                        typ, data = self.connection.capability()
+                        capabilities = " ".join(
+                            part.decode().upper()
+                            for part in (data or [])
+                            if isinstance(part, (bytes, bytearray))
+                        )
+                    except Exception:
+                        self.connection.logout()
+                        raise
@@
-                    status, response = self.connection.starttls()
+                    try:
+                        status, response = self.connection.starttls()
+                    except Exception:
+                        self.connection.logout()
+                        raise
🤖 Prompt for AI Agents
In `@src/backend/core/services/importer/imap.py` around lines 101 - 121, The
capability() and starttls() calls can raise exceptions and currently are not
exception-wrapped, so if they raise (e.g., imaplib.IMAP4.error or ssl.SSLError)
the connection may never be logged out; wrap the capability() call and the
starttls() call in try/except blocks that on any Exception call
self.connection.logout() and re-raise an IMAPSecurityError (or the original
exception) with a descriptive message; also when parsing capabilities, aggregate
all response lines from data instead of only data[0] (join/decode all items) to
robustly detect "STARTTLS"; mirror the existing login() cleanup pattern (use the
same exception handling and logout behavior) and reference the same symbols:
self.connection.capability(), self.connection.starttls(),
self.connection.logout(), and IMAPSecurityError.

Comment thread src/backend/core/services/importer/imap.py Outdated
@sylvinus sylvinus merged commit 20538a4 into main Jan 24, 2026
13 checks passed
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.

2 participants