~ruther/guix-config

ref: a178510f63e4e391479c8eee6cb87e6c062861f5 guix-config/home/patches/offlineimap3.patch -rw-r--r-- 7.9 KiB
a178510f — Rutherther chore: update 8 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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