|
1 | 1 | """IMAP4 client. |
2 | 2 |
|
3 | | -Based on RFC 2060. |
| 3 | +Based on RFC 2060 and updated for RFC 9051. |
4 | 4 |
|
5 | 5 | Public class: IMAP4 |
6 | 6 | Public variable: Debug |
|
40 | 40 | Debug = 0 |
41 | 41 | IMAP4_PORT = 143 |
42 | 42 | IMAP4_SSL_PORT = 993 |
43 | | -AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first |
| 43 | +AllowedVersions = ('IMAP4REV2', 'IMAP4REV1', 'IMAP4') # Most recent first |
| 44 | +IMAP4REV2_BUILTIN_COMMANDS = ( |
| 45 | + 'NAMESPACE', 'UNSELECT', 'UIDPLUS', 'ESEARCH', 'SEARCHRES', |
| 46 | + 'ENABLE', 'IDLE', 'SASL-IR', 'LIST-EXTENDED', 'LIST-STATUS', |
| 47 | + 'MOVE', 'LITERAL+') # RFC 9051 Appendix E.2 |
44 | 48 |
|
45 | 49 | # Maximal line length when calling readline(). This is to prevent |
46 | 50 | # reading arbitrary length lines. RFC 3501 and 2060 (IMAP 4rev1) |
@@ -144,7 +148,7 @@ class IMAP4: |
144 | 148 | If timeout is not given or is None, |
145 | 149 | the global default socket timeout is used |
146 | 150 |
|
147 | | - All IMAP4rev1 commands are supported by methods of the same |
| 151 | + All IMAP4rev2 and IMAP4rev1 commands are supported by methods of the same |
148 | 152 | name (in lowercase). |
149 | 153 |
|
150 | 154 | All arguments to commands are converted to strings, except for |
@@ -198,6 +202,7 @@ def __init__(self, host='', port=IMAP4_PORT, timeout=None): |
198 | 202 | self.is_readonly = False # READ-ONLY desired state |
199 | 203 | self.tagnum = 0 |
200 | 204 | self._tls_established = False |
| 205 | + self.PROTOCOL_VERSION = None |
201 | 206 | self._mode_ascii() |
202 | 207 | self._readbuf = [] |
203 | 208 |
|
@@ -260,14 +265,59 @@ def _connect(self): |
260 | 265 | if self.debug >= 3: |
261 | 266 | self._mesg('CAPABILITIES: %r' % (self.capabilities,)) |
262 | 267 |
|
| 268 | + self._set_protocol_version() |
| 269 | + |
| 270 | + |
| 271 | + def _set_protocol_version(self): |
| 272 | + |
| 273 | + if self._should_use_rev1_mode_for_dual_support(): |
| 274 | + self._activate_rev1_mode() |
| 275 | + return |
| 276 | + |
263 | 277 | for version in AllowedVersions: |
264 | 278 | if not version in self.capabilities: |
265 | 279 | continue |
266 | | - self.PROTOCOL_VERSION = version |
| 280 | + if version == 'IMAP4REV2': |
| 281 | + self._activate_rev2_mode() |
| 282 | + else: |
| 283 | + self.PROTOCOL_VERSION = version |
267 | 284 | return |
268 | 285 |
|
269 | 286 | raise self.error('server not IMAP4 compliant') |
270 | 287 |
|
| 288 | + def _supports_rev2(self): |
| 289 | + return 'IMAP4REV2' in self.capabilities |
| 290 | + |
| 291 | + def _supports_dual_rev1_rev2(self): |
| 292 | + return self._supports_rev2() and 'IMAP4REV1' in self.capabilities |
| 293 | + |
| 294 | + def _is_using_rev2(self): |
| 295 | + return self.PROTOCOL_VERSION == 'IMAP4REV2' |
| 296 | + |
| 297 | + def _is_capability_available(self, capability): |
| 298 | + capability = capability.upper() |
| 299 | + if self._is_using_rev2() and capability in IMAP4REV2_BUILTIN_COMMANDS: |
| 300 | + return True |
| 301 | + return capability in self.capabilities |
| 302 | + |
| 303 | + def _should_use_rev1_mode_for_dual_support(self): |
| 304 | + return self._supports_dual_rev1_rev2() |
| 305 | + |
| 306 | + def _activate_rev1_mode(self): |
| 307 | + self.PROTOCOL_VERSION = 'IMAP4REV1' |
| 308 | + self._mode_ascii() |
| 309 | + |
| 310 | + def _activate_rev2_mode(self): |
| 311 | + self.PROTOCOL_VERSION = 'IMAP4REV2' |
| 312 | + self._mode_utf8() |
| 313 | + |
| 314 | + def _handle_enable_success(self, capability): |
| 315 | + capability = capability.upper() |
| 316 | + if 'UTF8=ACCEPT' in capability: |
| 317 | + self._mode_utf8() |
| 318 | + if 'IMAP4REV2' in capability: |
| 319 | + self._activate_rev2_mode() |
| 320 | + |
271 | 321 |
|
272 | 322 | def __getattr__(self, attr): |
273 | 323 | # Allow UPPERCASE variants of IMAP4 command methods. |
@@ -603,11 +653,11 @@ def enable(self, capability): |
603 | 653 |
|
604 | 654 | (typ, [data]) = <instance>.enable(capability) |
605 | 655 | """ |
606 | | - if 'ENABLE' not in self.capabilities: |
| 656 | + if not self._is_capability_available('ENABLE'): |
607 | 657 | raise IMAP4.error("Server does not support ENABLE") |
608 | 658 | typ, data = self._simple_command('ENABLE', capability) |
609 | | - if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper(): |
610 | | - self._mode_utf8() |
| 659 | + if typ == 'OK': |
| 660 | + self._handle_enable_success(capability) |
611 | 661 | return typ, data |
612 | 662 |
|
613 | 663 | def expunge(self): |
@@ -838,7 +888,7 @@ def search(self, charset, *criteria): |
838 | 888 | """ |
839 | 889 | name = 'SEARCH' |
840 | 890 | if charset: |
841 | | - if self.utf8_enabled: |
| 891 | + if self.utf8_enabled and not self._is_using_rev2(): |
842 | 892 | raise IMAP4.error("Non-None charset not valid in UTF8 mode") |
843 | 893 | typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria) |
844 | 894 | else: |
@@ -936,6 +986,7 @@ def starttls(self, ssl_context=None): |
936 | 986 | self._imaplib_file = self.sock.makefile('rb') |
937 | 987 | self._tls_established = True |
938 | 988 | self._get_capabilities() |
| 989 | + self._set_protocol_version() |
939 | 990 | else: |
940 | 991 | raise self.error("Couldn't establish TLS session") |
941 | 992 | return self._untagged_response(typ, dat, name) |
@@ -1439,7 +1490,7 @@ class Idler: |
1439 | 1490 | """ |
1440 | 1491 |
|
1441 | 1492 | def __init__(self, imap, duration=None): |
1442 | | - if 'IDLE' not in imap.capabilities: |
| 1493 | + if not imap._is_capability_available('IDLE'): |
1443 | 1494 | raise imap.error("Server does not support IMAP4 IDLE") |
1444 | 1495 | if duration is not None and not imap.sock: |
1445 | 1496 | # IMAP4_stream pipes don't support timeouts |
|
0 commit comments