~ruther/guix-config

db663d56a4c4a8f7787eff3b8d743ba1ae1b6f70 — Rutherther 9 days ago 5039f8b
fix: update offlineimap STARTTLS patch to upstream version
1 files changed, 167 insertions(+), 11 deletions(-)

M home/patches/offlineimap3.patch
M home/patches/offlineimap3.patch => home/patches/offlineimap3.patch +167 -11
@@ 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?=
 <kix@kix.es>
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