From db663d56a4c4a8f7787eff3b8d743ba1ae1b6f70 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 12 Sep 2026 18:55:24 +0200 Subject: [PATCH] fix: update offlineimap STARTTLS patch to upstream version --- home/patches/offlineimap3.patch | 178 ++++++++++++++++++++++++++++++-- 1 file changed, 167 insertions(+), 11 deletions(-) diff --git a/home/patches/offlineimap3.patch b/home/patches/offlineimap3.patch index 4127b74e45341197931266ea3cf5f2b19e0f36bb..872e97e0de53c1b0827f83fcfb33ca8a2bec5b02 100644 --- a/home/patches/offlineimap3.patch +++ b/home/patches/offlineimap3.patch @@ -1,11 +1,167 @@ ---- a/offlineimap/localeval.py 2025-05-04 12:19:37.739860684 +0200 -+++ b/offlineimap/localeval.py 2025-05-04 12:26:07.838113251 +0200 -@@ -26,6 +26,8 @@ - self.namespace = {} - - if path is not None: -+ import importlib.machinery -+ - # FIXME: limit opening files owned by current user with rights set - # to fixed mode 644. - foo = open(path, 'r') +From 1c4f9b4f05de575cd99ac5bad68c6e0bbad25374 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Rodolfo=20Garc=C3=ADa=20Pe=C3=B1as=20=28kix=29?= + +Date: Wed, 13 May 2026 18:35:54 +0200 +Subject: [PATCH] Enhance STARTTLS handling with fallback for non-standard IMAP + servers + +- Implement tolerance for non-standard IMAP servers that do not provide reliable CAPABILITY after STARTTLS. +- Introduce configuration option 'allow_nonstandard_capabilities' to enable fallback using pre-TLS capabilities. +--- + offlineimap/imapserver.py | 110 +++++++++++++++++++++++++++------ + offlineimap/repository/IMAP.py | 7 +++ + 2 files changed, 98 insertions(+), 19 deletions(-) + +diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py +index fd866867..b29b5d6c 100644 +--- a/offlineimap/imapserver.py ++++ b/offlineimap/imapserver.py +@@ -363,32 +363,95 @@ def __gsshandler(self, token): + return None + + def __start_tls(self, imapobj): +- if 'STARTTLS' in imapobj.capabilities and not self.usessl: +- self.ui.debug('imap', 'Using STARTTLS connection') +- # imaplib2.starttls() unconditionally calls _get_capabilities() right +- # after the TLS handshake (before setting _tls_established=True). +- # Servers like Protonmail Bridge do not respond to CAPABILITY at +- # that stage, causing a 60-second hang. We bypass it by temporarily +- # substituting a no-op; imaplib2 still sets _tls_established=True +- # normally, and acquireconnection() already guards its own +- # post-login CAPABILITY refresh against that flag (see below). +- _orig_get_cap = imapobj._get_capabilities +- imapobj._get_capabilities = lambda: None ++ """Upgrade connection to TLS if STARTTLS is configured. ++ ++ - Uses pre-TLS capabilities only to decide whether to perform STARTTLS. ++ - After STARTTLS, forces a CAPABILITY command and replaces internal ++ capabilities with the post-TLS response. ++ - If no reliable post-TLS CAPABILITY is available: ++ - In strict mode (allow_nonstandard_capabilities = no): abort. ++ - In tolerant mode (allow_nonstandard_capabilities = yes): ++ reuse pre-TLS capabilities, removing LOGINDISABLED. ++ """ ++ ++ # If the repository does not want STARTTLS, do nothing. ++ if not self.starttls or self.usessl: ++ return ++ ++ # Pre-TLS capabilities (from initial banner / CAPABILITY). ++ caps_pre = set(getattr(imapobj, '_offlineimap_capabilities_pre_tls', ++ getattr(imapobj, 'capabilities', []))) ++ ++ # If the server does not advertise STARTTLS, respect that. ++ if 'STARTTLS' not in caps_pre: ++ return ++ ++ # Execute STARTTLS. ++ self.ui.debug('imap', 'Using STARTTLS connection') ++ ++ # Disable _get_capabilities() during starttls() to prevent imaplib2 ++ # from issuing CAPABILITY before we can do it ourselves. ++ _orig_get_cap = imapobj._get_capabilities ++ imapobj._get_capabilities = lambda: None ++ try: + try: + imapobj.starttls() + except imapobj.error as e: + err = "Failed to start TLS connection: %s" % str(e) + raise OfflineImapError(err, OfflineImapError.ERROR.REPO, + exc_info()[2]) +- finally: +- # Always restore the original method, even on success. +- imapobj._get_capabilities = _orig_get_cap +- elif self.starttls and not self.usessl: ++ finally: ++ # Always restore the original method, even on error. ++ imapobj._get_capabilities = _orig_get_cap ++ ++ # At this point, the socket is encrypted. Now we must refresh CAPABILITY. ++ caps_post = None ++ try: ++ typ, data = imapobj.capability() ++ if typ == 'OK' and data: ++ # data is a list of bytes, e.g. [b'IMAP4rev1 IDLE AUTH=PLAIN'] ++ line = data[0] ++ if isinstance(line, bytes): ++ line = line.decode('ascii', 'ignore') ++ caps_post = set(line.upper().split()) ++ except Exception: ++ caps_post = None ++ ++ if caps_post is not None: ++ # Standard path: completely replace capabilities ++ imapobj.capabilities = caps_post ++ imapobj._offlineimap_capabilities_post_tls = caps_post ++ return ++ ++ # If we reach here, we do not have reliable post-TLS capabilities. ++ # Decide based on repository configuration. ++ allow_nonstandard = getattr(self.repos, ++ 'allow_nonstandard_capabilities', False) ++ ++ if not allow_nonstandard: ++ # Strict mode: abort with clear error. + raise OfflineImapError( +- "STARTTLS requested but server does not advertise STARTTLS capability. " +- "Aborting to prevent sending password in plaintext (possible MITM attack). " +- "If you REALLY want an insecure connection, set 'starttls = no'.", +- OfflineImapError.ERROR.REPO) ++ "Server did not provide valid CAPABILITY after STARTTLS; " ++ "set 'allow_nonstandard_capabilities = yes' in repository " ++ "configuration to enable a non-standard fallback.", ++ OfflineImapError.ERROR.REPO ++ ) ++ ++ # Tolerant mode: best-effort using caps_pre. ++ caps_fallback = set(caps_pre) ++ if 'LOGINDISABLED' in caps_fallback: ++ # We assume that after STARTTLS, LOGINDISABLED no longer applies, ++ # so we remove it to allow LOGIN/AUTH configured by the user. ++ caps_fallback.remove('LOGINDISABLED') ++ ++ imapobj.capabilities = caps_fallback ++ imapobj._offlineimap_capabilities_post_tls = caps_fallback ++ ++ self.ui.warn( ++ "Server did not provide CAPABILITY after STARTTLS; " ++ "falling back to pre-TLS capabilities without LOGINDISABLED " ++ "due to allow_nonstandard_capabilities = yes." ++ ) + + # All __authn_* procedures are helpers that do authentication. + # They are class methods that take one parameter, IMAP object. +@@ -605,6 +668,15 @@ def acquireconnection(self): + self.lastowner[imapobj] = curThread.ident + self.connectionlock.release() + ++ # Store the pre-TLS capabilities (from banner and initial CAPABILITY). ++ # This will be used only to decide STARTTLS and, in case of non-standard ++ # fallback, to reconstruct an approximate post-TLS capability list. ++ try: ++ caps_pre = set(getattr(imapobj, 'capabilities', [])) ++ except Exception: ++ caps_pre = set() ++ imapobj._offlineimap_capabilities_pre_tls = caps_pre ++ + # Verify that the connection is still alive before returning it + # to the caller. If not, clean up and recursively call + # acquireconnection() to get a new one. +diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py +index b1afe861..db64dae1 100644 +--- a/offlineimap/repository/IMAP.py ++++ b/offlineimap/repository/IMAP.py +@@ -38,6 +38,13 @@ def __init__(self, reposname, account): + self.idlefolders = None + BaseRepository.__init__(self, reposname, account) + # self.ui is being set by the BaseRepository ++ ++ # Allow tolerance for non-standard IMAP servers that do not provide ++ # reliable CAPABILITY after STARTTLS. Default is strict (False). ++ # If set to True, heuristics using pre-TLS capabilities are applied ++ # as a fallback. ++ self.allow_nonstandard_capabilities = self.getconfboolean( ++ 'allow_nonstandard_capabilities', False) + self._host = None + # Must be set before calling imapserver.IMAPServer(self) + self.oauth2_request_url = None