codekingpro/portable-devtools
114k
1"""IMAP4 client.2 3Based on RFC 2060.4 5Public class: IMAP46Public variable: Debug7Public functions: Internaldate2tuple8 Int2AP9 ParseFlags10 Time2Internaldate11"""12 13# Author: Piers Lauder <piers@cs.su.oz.au> December 1997.14#15# Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.16# String method conversion by ESR, February 2001.17# GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.18# IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.19# GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.20# PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002.21# GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.22# IDLE contributed by Forest <forestix@nom.one> August 2024.23 24__version__ = "2.60"25 26import binascii, errno, random, re, socket, subprocess, sys, time, calendar27from datetime import datetime, timezone, timedelta28from io import DEFAULT_BUFFER_SIZE29 30try:31 import ssl32 HAVE_SSL = True33except ImportError:34 HAVE_SSL = False35 36__all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple",37 "Int2AP", "ParseFlags", "Time2Internaldate"]38 39# Globals40 41CRLF = b'\r\n'42Debug = 043IMAP4_PORT = 14344IMAP4_SSL_PORT = 99345AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first46 47# Maximal line length when calling readline(). This is to prevent48# reading arbitrary length lines. RFC 3501 and 2060 (IMAP 4rev1)49# don't specify a line length. RFC 2683 suggests limiting client50# command lines to 1000 octets and that servers should be prepared51# to accept command lines up to 8000 octets, so we used to use 10K here.52# In the modern world (eg: gmail) the response to, for example, a53# search command can be quite large, so we now use 1M.54_MAXLINE = 100000055 56 57# Commands58 59Commands = {60 # name valid states61 'APPEND': ('AUTH', 'SELECTED'),62 'AUTHENTICATE': ('NONAUTH',),63 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),64 'CHECK': ('SELECTED',),65 'CLOSE': ('SELECTED',),66 'COPY': ('SELECTED',),67 'CREATE': ('AUTH', 'SELECTED'),68 'DELETE': ('AUTH', 'SELECTED'),69 'DELETEACL': ('AUTH', 'SELECTED'),70 'ENABLE': ('AUTH', ),71 'EXAMINE': ('AUTH', 'SELECTED'),72 'EXPUNGE': ('SELECTED',),73 'FETCH': ('SELECTED',),74 'GETACL': ('AUTH', 'SELECTED'),75 'GETANNOTATION':('AUTH', 'SELECTED'),76 'GETQUOTA': ('AUTH', 'SELECTED'),77 'GETQUOTAROOT': ('AUTH', 'SELECTED'),78 'IDLE': ('AUTH', 'SELECTED'),79 'MYRIGHTS': ('AUTH', 'SELECTED'),80 'LIST': ('AUTH', 'SELECTED'),81 'LOGIN': ('NONAUTH',),82 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),83 'LSUB': ('AUTH', 'SELECTED'),84 'MOVE': ('SELECTED',),85 'NAMESPACE': ('AUTH', 'SELECTED'),86 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),87 'PARTIAL': ('SELECTED',), # NB: obsolete88 'PROXYAUTH': ('AUTH',),89 'RENAME': ('AUTH', 'SELECTED'),90 'SEARCH': ('SELECTED',),91 'SELECT': ('AUTH', 'SELECTED'),92 'SETACL': ('AUTH', 'SELECTED'),93 'SETANNOTATION':('AUTH', 'SELECTED'),94 'SETQUOTA': ('AUTH', 'SELECTED'),95 'SORT': ('SELECTED',),96 'STARTTLS': ('NONAUTH',),97 'STATUS': ('AUTH', 'SELECTED'),98 'STORE': ('SELECTED',),99 'SUBSCRIBE': ('AUTH', 'SELECTED'),100 'THREAD': ('SELECTED',),101 'UID': ('SELECTED',),102 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),103 'UNSELECT': ('SELECTED',),104 }105 106# Patterns to match server responses107 108Continuation = re.compile(br'\+( (?P<data>.*))?')109Flags = re.compile(br'.*FLAGS \((?P<flags>[^\)]*)\)')110InternalDate = re.compile(br'.*INTERNALDATE "'111 br'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'112 br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'113 br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'114 br'"')115# Literal is no longer used; kept for backward compatibility.116Literal = re.compile(br'.*{(?P<size>\d+)}$', re.ASCII)117MapCRLF = re.compile(br'\r\n|\r|\n')118# We no longer exclude the ']' character from the data portion of the response119# code, even though it violates the RFC. Popular IMAP servers such as Gmail120# allow flags with ']', and there are programs (including imaplib!) that can121# produce them. The problem with this is if the 'text' portion of the response122# includes a ']' we'll parse the response wrong (which is the point of the RFC123# restriction). However, that seems less likely to be a problem in practice124# than being unable to correctly parse flags that include ']' chars, which125# was reported as a real-world problem in issue #21815.126Response_code = re.compile(br'\[(?P<type>[A-Z-]+)( (?P<data>.*))?\]')127Untagged_response = re.compile(br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')128# Untagged_status is no longer used; kept for backward compatibility129Untagged_status = re.compile(130 br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?', re.ASCII)131# We compile these in _mode_xxx.132_Literal = br'.*{(?P<size>\d+)}$'133_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'134 135 136 137class IMAP4:138 139 r"""IMAP4 client class.140 141 Instantiate with: IMAP4([host[, port[, timeout=None]]])142 143 host - host's name (default: localhost);144 port - port number (default: standard IMAP4 port).145 timeout - socket timeout (default: None)146 If timeout is not given or is None,147 the global default socket timeout is used148 149 All IMAP4rev1 commands are supported by methods of the same150 name (in lowercase).151 152 All arguments to commands are converted to strings, except for153 AUTHENTICATE, and the last argument to APPEND which is passed as154 an IMAP4 literal. If necessary (the string contains any155 non-printing characters or white-space and isn't enclosed with156 either parentheses or double quotes) each string is quoted.157 However, the 'password' argument to the LOGIN command is always158 quoted. If you want to avoid having an argument string quoted159 (eg: the 'flags' argument to STORE) then enclose the string in160 parentheses (eg: "(\Deleted)").161 162 Each command returns a tuple: (type, [data, ...]) where 'type'163 is usually 'OK' or 'NO', and 'data' is either the text from the164 tagged response, or untagged results from command. Each 'data'165 is either a string, or a tuple. If a tuple, then the first part166 is the header of the response, and the second part contains167 the data (ie: 'literal' value).168 169 Errors raise the exception class <instance>.error("<reason>").170 IMAP4 server errors raise <instance>.abort("<reason>"),171 which is a sub-class of 'error'. Mailbox status changes172 from READ-WRITE to READ-ONLY raise the exception class173 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.174 175 "error" exceptions imply a program error.176 "abort" exceptions imply the connection should be reset, and177 the command re-tried.178 "readonly" exceptions imply the command should be re-tried.179 180 Note: to use this module, you must read the RFCs pertaining to the181 IMAP4 protocol, as the semantics of the arguments to each IMAP4182 command are left to the invoker, not to mention the results. Also,183 most IMAP servers implement a sub-set of the commands available here.184 """185 186 class error(Exception): pass # Logical errors - debug required187 class abort(error): pass # Service errors - close and retry188 class readonly(abort): pass # Mailbox status changed to READ-ONLY189 class _responsetimeout(TimeoutError): pass # No response during IDLE190 191 def __init__(self, host='', port=IMAP4_PORT, timeout=None):192 self.debug = Debug193 self.state = 'LOGOUT'194 self.literal = None # A literal argument to a command195 self.tagged_commands = {} # Tagged commands awaiting response196 self.untagged_responses = {} # {typ: [data, ...], ...}197 self.continuation_response = '' # Last continuation response198 self._idle_responses = [] # Response queue for idle iteration199 self._idle_capture = False # Whether to queue responses for idle200 self.is_readonly = False # READ-ONLY desired state201 self.tagnum = 0202 self._tls_established = False203 self._mode_ascii()204 self._readbuf = []205 206 # Open socket to server.207 208 self.open(host, port, timeout)209 210 try:211 self._connect()212 except Exception:213 try:214 self.shutdown()215 except OSError:216 pass217 raise218 219 def _mode_ascii(self):220 self.utf8_enabled = False221 self._encoding = 'ascii'222 self.Literal = re.compile(_Literal, re.ASCII)223 self.Untagged_status = re.compile(_Untagged_status, re.ASCII)224 225 226 def _mode_utf8(self):227 self.utf8_enabled = True228 self._encoding = 'utf-8'229 self.Literal = re.compile(_Literal)230 self.Untagged_status = re.compile(_Untagged_status)231 232 233 def _connect(self):234 # Create unique tag for this session,235 # and compile tagged response matcher.236 237 self.tagpre = Int2AP(random.randint(4096, 65535))238 self.tagre = re.compile(br'(?P<tag>'239 + self.tagpre240 + br'\d+) (?P<type>[A-Z]+) (?P<data>.*)', re.ASCII)241 242 # Get server welcome message,243 # request and store CAPABILITY response.244 245 if __debug__:246 self._cmd_log_len = 10247 self._cmd_log_idx = 0248 self._cmd_log = {} # Last '_cmd_log_len' interactions249 if self.debug >= 1:250 self._mesg('imaplib version %s' % __version__)251 self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)252 253 self.welcome = self._get_response()254 if 'PREAUTH' in self.untagged_responses:255 self.state = 'AUTH'256 elif 'OK' in self.untagged_responses:257 self.state = 'NONAUTH'258 else:259 raise self.error(self.welcome)260 261 self._get_capabilities()262 if __debug__:263 if self.debug >= 3:264 self._mesg('CAPABILITIES: %r' % (self.capabilities,))265 266 for version in AllowedVersions:267 if not version in self.capabilities:268 continue269 self.PROTOCOL_VERSION = version270 return271 272 raise self.error('server not IMAP4 compliant')273 274 275 def __getattr__(self, attr):276 # Allow UPPERCASE variants of IMAP4 command methods.277 if attr in Commands:278 return getattr(self, attr.lower())279 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)280 281 def __enter__(self):282 return self283 284 def __exit__(self, *args):285 if self.state == "LOGOUT":286 return287 288 try:289 self.logout()290 except OSError:291 pass292 293 294 # Overridable methods295 296 297 def _create_socket(self, timeout):298 # Default value of IMAP4.host is '', but socket.getaddrinfo()299 # (which is used by socket.create_connection()) expects None300 # as a default value for host.301 if timeout is not None and not timeout:302 raise ValueError('Non-blocking socket (timeout=0) is not supported')303 host = None if not self.host else self.host304 sys.audit("imaplib.open", self, self.host, self.port)305 address = (host, self.port)306 if timeout is not None:307 return socket.create_connection(address, timeout)308 return socket.create_connection(address)309 310 def open(self, host='', port=IMAP4_PORT, timeout=None):311 """Setup connection to remote server on "host:port"312 (default: localhost:standard IMAP4 port).313 This connection will be used by the routines:314 read, readline, send, shutdown.315 """316 self.host = host317 self.port = port318 self.sock = self._create_socket(timeout)319 self._file = self.sock.makefile('rb')320 321 322 @property323 def file(self):324 # The old 'file' attribute is no longer used now that we do our own325 # read() and readline() buffering, with which it conflicts.326 # As an undocumented interface, it should never have been accessed by327 # external code, and therefore does not warrant deprecation.328 # Nevertheless, we provide this property for now, to avoid suddenly329 # breaking any code in the wild that might have been using it in a330 # harmless way.331 import warnings332 warnings.warn(333 'IMAP4.file is unsupported, can cause errors, and may be removed.',334 RuntimeWarning,335 stacklevel=2)336 return self._file337 338 339 def read(self, size):340 """Read 'size' bytes from remote."""341 # We need buffered read() to continue working after socket timeouts,342 # since we use them during IDLE. Unfortunately, the standard library's343 # SocketIO implementation makes this impossible, by setting a permanent344 # error condition instead of letting the caller decide how to handle a345 # timeout. We therefore implement our own buffered read().346 # https://github.com/python/cpython/issues/51571347 #348 # Reading in chunks instead of delegating to a single349 # BufferedReader.read() call also means we avoid its preallocation350 # of an unreasonably large memory block if a malicious server claims351 # it will send a huge literal without actually sending one.352 # https://github.com/python/cpython/issues/119511353 354 parts = []355 356 while size > 0:357 358 if len(parts) < len(self._readbuf):359 buf = self._readbuf[len(parts)]360 else:361 try:362 buf = self.sock.recv(DEFAULT_BUFFER_SIZE)363 except ConnectionError:364 break365 if not buf:366 break367 self._readbuf.append(buf)368 369 if len(buf) >= size:370 parts.append(buf[:size])371 self._readbuf = [buf[size:]] + self._readbuf[len(parts):]372 break373 parts.append(buf)374 size -= len(buf)375 376 return b''.join(parts)377 378 379 def readline(self):380 """Read line from remote."""381 # The comment in read() explains why we implement our own readline().382 383 LF = b'\n'384 parts = []385 length = 0386 387 while length < _MAXLINE:388 389 if len(parts) < len(self._readbuf):390 buf = self._readbuf[len(parts)]391 else:392 try:393 buf = self.sock.recv(DEFAULT_BUFFER_SIZE)394 except ConnectionError:395 break396 if not buf:397 break398 self._readbuf.append(buf)399 400 pos = buf.find(LF)401 if pos != -1:402 pos += 1403 parts.append(buf[:pos])404 self._readbuf = [buf[pos:]] + self._readbuf[len(parts):]405 break406 parts.append(buf)407 length += len(buf)408 409 line = b''.join(parts)410 if len(line) > _MAXLINE:411 raise self.error("got more than %d bytes" % _MAXLINE)412 return line413 414 415 def send(self, data):416 """Send data to remote."""417 sys.audit("imaplib.send", self, data)418 self.sock.sendall(data)419 420 421 def shutdown(self):422 """Close I/O established in "open"."""423 self._file.close()424 try:425 self.sock.shutdown(socket.SHUT_RDWR)426 except OSError as exc:427 # The server might already have closed the connection.428 # On Windows, this may result in WSAEINVAL (error 10022):429 # An invalid operation was attempted.430 if (exc.errno != errno.ENOTCONN431 and getattr(exc, 'winerror', 0) != 10022):432 raise433 finally:434 self.sock.close()435 436 437 def socket(self):438 """Return socket instance used to connect to IMAP4 server.439 440 socket = <instance>.socket()441 """442 return self.sock443 444 445 446 # Utility methods447 448 449 def recent(self):450 """Return most recent 'RECENT' responses if any exist,451 else prompt server for an update using the 'NOOP' command.452 453 (typ, [data]) = <instance>.recent()454 455 'data' is None if no new messages,456 else list of RECENT responses, most recent last.457 """458 name = 'RECENT'459 typ, dat = self._untagged_response('OK', [None], name)460 if dat[-1]:461 return typ, dat462 typ, dat = self.noop() # Prod server for response463 return self._untagged_response(typ, dat, name)464 465 466 def response(self, code):467 """Return data for response 'code' if received, or None.468 469 Old value for response 'code' is cleared.470 471 (code, [data]) = <instance>.response(code)472 """473 return self._untagged_response(code, [None], code.upper())474 475 476 477 # IMAP4 commands478 479 480 def append(self, mailbox, flags, date_time, message):481 """Append message to named mailbox.482 483 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)484 485 All args except 'message' can be None.486 """487 name = 'APPEND'488 if not mailbox:489 mailbox = 'INBOX'490 if flags:491 if (flags[0],flags[-1]) != ('(',')'):492 flags = '(%s)' % flags493 else:494 flags = None495 if date_time:496 date_time = Time2Internaldate(date_time)497 else:498 date_time = None499 literal = MapCRLF.sub(CRLF, message)500 self.literal = literal501 return self._simple_command(name, mailbox, flags, date_time)502 503 504 def authenticate(self, mechanism, authobject):505 """Authenticate command - requires response processing.506 507 'mechanism' specifies which authentication mechanism is to508 be used - it must appear in <instance>.capabilities in the509 form AUTH=<mechanism>.510 511 'authobject' must be a callable object:512 513 data = authobject(response)514 515 It will be called to process server continuation responses; the516 response argument it is passed will be a bytes. It should return bytes517 data that will be base64 encoded and sent to the server. It should518 return None if the client abort response '*' should be sent instead.519 """520 mech = mechanism.upper()521 # XXX: shouldn't this code be removed, not commented out?522 #cap = 'AUTH=%s' % mech523 #if not cap in self.capabilities: # Let the server decide!524 # raise self.error("Server doesn't allow %s authentication." % mech)525 self.literal = _Authenticator(authobject).process526 typ, dat = self._simple_command('AUTHENTICATE', mech)527 if typ != 'OK':528 raise self.error(dat[-1].decode('utf-8', 'replace'))529 self.state = 'AUTH'530 return typ, dat531 532 533 def capability(self):534 """(typ, [data]) = <instance>.capability()535 Fetch capabilities list from server."""536 537 name = 'CAPABILITY'538 typ, dat = self._simple_command(name)539 return self._untagged_response(typ, dat, name)540 541 542 def check(self):543 """Checkpoint mailbox on server.544 545 (typ, [data]) = <instance>.check()546 """547 return self._simple_command('CHECK')548 549 550 def close(self):551 """Close currently selected mailbox.552 553 Deleted messages are removed from writable mailbox.554 This is the recommended command before 'LOGOUT'.555 556 (typ, [data]) = <instance>.close()557 """558 try:559 typ, dat = self._simple_command('CLOSE')560 finally:561 self.state = 'AUTH'562 return typ, dat563 564 565 def copy(self, message_set, new_mailbox):566 """Copy 'message_set' messages onto end of 'new_mailbox'.567 568 (typ, [data]) = <instance>.copy(message_set, new_mailbox)569 """570 return self._simple_command('COPY', message_set, new_mailbox)571 572 573 def create(self, mailbox):574 """Create new mailbox.575 576 (typ, [data]) = <instance>.create(mailbox)577 """578 return self._simple_command('CREATE', mailbox)579 580 581 def delete(self, mailbox):582 """Delete old mailbox.583 584 (typ, [data]) = <instance>.delete(mailbox)585 """586 return self._simple_command('DELETE', mailbox)587 588 def deleteacl(self, mailbox, who):589 """Delete the ACLs (remove any rights) set for who on mailbox.590 591 (typ, [data]) = <instance>.deleteacl(mailbox, who)592 """593 return self._simple_command('DELETEACL', mailbox, who)594 595 def enable(self, capability):596 """Send an RFC5161 enable string to the server.597 598 (typ, [data]) = <instance>.enable(capability)599 """600 if 'ENABLE' not in self.capabilities:601 raise IMAP4.error("Server does not support ENABLE")602 typ, data = self._simple_command('ENABLE', capability)603 if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper():604 self._mode_utf8()605 return typ, data606 607 def expunge(self):608 """Permanently remove deleted items from selected mailbox.609 610 Generates 'EXPUNGE' response for each deleted message.611 612 (typ, [data]) = <instance>.expunge()613 614 'data' is list of 'EXPUNGE'd message numbers in order received.615 """616 name = 'EXPUNGE'617 typ, dat = self._simple_command(name)618 return self._untagged_response(typ, dat, name)619 620 621 def fetch(self, message_set, message_parts):622 """Fetch (parts of) messages.623 624 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)625 626 'message_parts' should be a string of selected parts627 enclosed in parentheses, eg: "(UID BODY[TEXT])".628 629 'data' are tuples of message part envelope and data.630 """631 name = 'FETCH'632 typ, dat = self._simple_command(name, message_set, message_parts)633 return self._untagged_response(typ, dat, name)634 635 636 def getacl(self, mailbox):637 """Get the ACLs for a mailbox.638 639 (typ, [data]) = <instance>.getacl(mailbox)640 """641 typ, dat = self._simple_command('GETACL', mailbox)642 return self._untagged_response(typ, dat, 'ACL')643 644 645 def getannotation(self, mailbox, entry, attribute):646 """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)647 Retrieve ANNOTATIONs."""648 649 typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)650 return self._untagged_response(typ, dat, 'ANNOTATION')651 652 653 def getquota(self, root):654 """Get the quota root's resource usage and limits.655 656 Part of the IMAP4 QUOTA extension defined in rfc2087.657 658 (typ, [data]) = <instance>.getquota(root)659 """660 typ, dat = self._simple_command('GETQUOTA', root)661 return self._untagged_response(typ, dat, 'QUOTA')662 663 664 def getquotaroot(self, mailbox):665 """Get the list of quota roots for the named mailbox.666 667 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)668 """669 typ, dat = self._simple_command('GETQUOTAROOT', mailbox)670 typ, quota = self._untagged_response(typ, dat, 'QUOTA')671 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')672 return typ, [quotaroot, quota]673 674 675 def idle(self, duration=None):676 """Return an iterable IDLE context manager producing untagged responses.677 If the argument is not None, limit iteration to 'duration' seconds.678 679 with M.idle(duration=29 * 60) as idler:680 for typ, data in idler:681 print(typ, data)682 683 Note: 'duration' requires a socket connection (not IMAP4_stream).684 """685 return Idler(self, duration)686 687 688 def list(self, directory='""', pattern='*'):689 """List mailbox names in directory matching pattern.690 691 (typ, [data]) = <instance>.list(directory='""', pattern='*')692 693 'data' is list of LIST responses.694 """695 name = 'LIST'696 typ, dat = self._simple_command(name, directory, pattern)697 return self._untagged_response(typ, dat, name)698 699 700 def login(self, user, password):701 """Identify client using plaintext password.702 703 (typ, [data]) = <instance>.login(user, password)704 705 NB: 'password' will be quoted.706 """707 typ, dat = self._simple_command('LOGIN', user, self._quote(password))708 if typ != 'OK':709 raise self.error(dat[-1])710 self.state = 'AUTH'711 return typ, dat712 713 714 def login_cram_md5(self, user, password):715 """ Force use of CRAM-MD5 authentication.716 717 (typ, [data]) = <instance>.login_cram_md5(user, password)718 """719 self.user, self.password = user, password720 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)721 722 723 def _CRAM_MD5_AUTH(self, challenge):724 """ Authobject to use with CRAM-MD5 authentication. """725 import hmac726 727 if isinstance(self.password, str):728 password = self.password.encode('utf-8')729 else:730 password = self.password731 732 try:733 authcode = hmac.HMAC(password, challenge, 'md5')734 except ValueError: # HMAC-MD5 is not available735 raise self.error("CRAM-MD5 authentication is not supported")736 return f"{self.user} {authcode.hexdigest()}"737 738 739 def logout(self):740 """Shutdown connection to server.741 742 (typ, [data]) = <instance>.logout()743 744 Returns server 'BYE' response.745 """746 self.state = 'LOGOUT'747 typ, dat = self._simple_command('LOGOUT')748 self.shutdown()749 return typ, dat750 751 752 def lsub(self, directory='""', pattern='*'):753 """List 'subscribed' mailbox names in directory matching pattern.754 755 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')756 757 'data' are tuples of message part envelope and data.758 """759 name = 'LSUB'760 typ, dat = self._simple_command(name, directory, pattern)761 return self._untagged_response(typ, dat, name)762 763 def myrights(self, mailbox):764 """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).765 766 (typ, [data]) = <instance>.myrights(mailbox)767 """768 typ,dat = self._simple_command('MYRIGHTS', mailbox)769 return self._untagged_response(typ, dat, 'MYRIGHTS')770 771 def namespace(self):772 """ Returns IMAP namespaces ala rfc2342773 774 (typ, [data, ...]) = <instance>.namespace()775 """776 name = 'NAMESPACE'777 typ, dat = self._simple_command(name)778 return self._untagged_response(typ, dat, name)779 780 781 def noop(self):782 """Send NOOP command.783 784 (typ, [data]) = <instance>.noop()785 """786 if __debug__:787 if self.debug >= 3:788 self._dump_ur(self.untagged_responses)789 return self._simple_command('NOOP')790 791 792 def partial(self, message_num, message_part, start, length):793 """Fetch truncated part of a message.794 795 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)796 797 'data' is tuple of message part envelope and data.798 """799 name = 'PARTIAL'800 typ, dat = self._simple_command(name, message_num, message_part, start, length)801 return self._untagged_response(typ, dat, 'FETCH')802 803 804 def proxyauth(self, user):805 """Assume authentication as "user".806 807 Allows an authorised administrator to proxy into any user's808 mailbox.809 810 (typ, [data]) = <instance>.proxyauth(user)811 """812 813 name = 'PROXYAUTH'814 return self._simple_command('PROXYAUTH', user)815 816 817 def rename(self, oldmailbox, newmailbox):818 """Rename old mailbox name to new.819 820 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)821 """822 return self._simple_command('RENAME', oldmailbox, newmailbox)823 824 825 def search(self, charset, *criteria):826 """Search mailbox for matching messages.827 828 (typ, [data]) = <instance>.search(charset, criterion, ...)829 830 'data' is space separated list of matching message numbers.831 If UTF8 is enabled, charset MUST be None.832 """833 name = 'SEARCH'834 if charset:835 if self.utf8_enabled:836 raise IMAP4.error("Non-None charset not valid in UTF8 mode")837 typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)838 else:839 typ, dat = self._simple_command(name, *criteria)840 return self._untagged_response(typ, dat, name)841 842 843 def select(self, mailbox='INBOX', readonly=False):844 """Select a mailbox.845 846 Flush all untagged responses.847 848 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)849 850 'data' is count of messages in mailbox ('EXISTS' response).851 852 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so853 other responses should be obtained via <instance>.response('FLAGS') etc.854 """855 self.untagged_responses = {} # Flush old responses.856 self.is_readonly = readonly857 if readonly:858 name = 'EXAMINE'859 else:860 name = 'SELECT'861 typ, dat = self._simple_command(name, mailbox)862 if typ != 'OK':863 self.state = 'AUTH' # Might have been 'SELECTED'864 return typ, dat865 self.state = 'SELECTED'866 if 'READ-ONLY' in self.untagged_responses \867 and not readonly:868 if __debug__:869 if self.debug >= 1:870 self._dump_ur(self.untagged_responses)871 raise self.readonly('%s is not writable' % mailbox)872 return typ, self.untagged_responses.get('EXISTS', [None])873 874 875 def setacl(self, mailbox, who, what):876 """Set a mailbox acl.877 878 (typ, [data]) = <instance>.setacl(mailbox, who, what)879 """880 return self._simple_command('SETACL', mailbox, who, what)881 882 883 def setannotation(self, *args):884 """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)885 Set ANNOTATIONs."""886 887 typ, dat = self._simple_command('SETANNOTATION', *args)888 return self._untagged_response(typ, dat, 'ANNOTATION')889 890 891 def setquota(self, root, limits):892 """Set the quota root's resource limits.893 894 (typ, [data]) = <instance>.setquota(root, limits)895 """896 typ, dat = self._simple_command('SETQUOTA', root, limits)897 return self._untagged_response(typ, dat, 'QUOTA')898 899 900 def sort(self, sort_criteria, charset, *search_criteria):901 """IMAP4rev1 extension SORT command.902 903 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)904 """905 name = 'SORT'906 #if not name in self.capabilities: # Let the server decide!907 # raise self.error('unimplemented extension command: %s' % name)908 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):909 sort_criteria = '(%s)' % sort_criteria910 typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)911 return self._untagged_response(typ, dat, name)912 913 914 def starttls(self, ssl_context=None):915 name = 'STARTTLS'916 if not HAVE_SSL:917 raise self.error('SSL support missing')918 if self._tls_established:919 raise self.abort('TLS session already established')920 if name not in self.capabilities:921 raise self.abort('TLS not supported by server')922 # Generate a default SSL context if none was passed.923 if ssl_context is None:924 ssl_context = ssl._create_stdlib_context()925 typ, dat = self._simple_command(name)926 if typ == 'OK':927 self.sock = ssl_context.wrap_socket(self.sock,928 server_hostname=self.host)929 self._file = self.sock.makefile('rb')930 self._tls_established = True931 self._get_capabilities()932 else:933 raise self.error("Couldn't establish TLS session")934 return self._untagged_response(typ, dat, name)935 936 937 def status(self, mailbox, names):938 """Request named status conditions for mailbox.939 940 (typ, [data]) = <instance>.status(mailbox, names)941 """942 name = 'STATUS'943 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!944 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)945 typ, dat = self._simple_command(name, mailbox, names)946 return self._untagged_response(typ, dat, name)947 948 949 def store(self, message_set, command, flags):950 """Alters flag dispositions for messages in mailbox.951 952 (typ, [data]) = <instance>.store(message_set, command, flags)953 """954 if (flags[0],flags[-1]) != ('(',')'):955 flags = '(%s)' % flags # Avoid quoting the flags956 typ, dat = self._simple_command('STORE', message_set, command, flags)957 return self._untagged_response(typ, dat, 'FETCH')958 959 960 def subscribe(self, mailbox):961 """Subscribe to new mailbox.962 963 (typ, [data]) = <instance>.subscribe(mailbox)964 """965 return self._simple_command('SUBSCRIBE', mailbox)966 967 968 def thread(self, threading_algorithm, charset, *search_criteria):969 """IMAPrev1 extension THREAD command.970 971 (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)972 """973 name = 'THREAD'974 typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)975 return self._untagged_response(typ, dat, name)976 977 978 def uid(self, command, *args):979 """Execute "command arg ..." with messages identified by UID,980 rather than message number.981 982 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)983 984 Returns response appropriate to 'command'.985 """986 command = command.upper()987 if not command in Commands:988 raise self.error("Unknown IMAP4 UID command: %s" % command)989 if self.state not in Commands[command]:990 raise self.error("command %s illegal in state %s, "991 "only allowed in states %s" %992 (command, self.state,993 ', '.join(Commands[command])))994 name = 'UID'995 typ, dat = self._simple_command(name, command, *args)996 if command in ('SEARCH', 'SORT', 'THREAD'):997 name = command998 else:999 name = 'FETCH'1000 return self._untagged_response(typ, dat, name)1001 1002 1003 def unsubscribe(self, mailbox):1004 """Unsubscribe from old mailbox.1005 1006 (typ, [data]) = <instance>.unsubscribe(mailbox)1007 """1008 return self._simple_command('UNSUBSCRIBE', mailbox)1009 1010 1011 def unselect(self):1012 """Free server's resources associated with the selected mailbox1013 and returns the server to the authenticated state.1014 This command performs the same actions as CLOSE, except1015 that no messages are permanently removed from the currently1016 selected mailbox.1017 1018 (typ, [data]) = <instance>.unselect()1019 """1020 try:1021 typ, data = self._simple_command('UNSELECT')1022 finally:1023 self.state = 'AUTH'1024 return typ, data1025 1026 1027 def xatom(self, name, *args):1028 """Allow simple extension commands1029 notified by server in CAPABILITY response.1030 1031 Assumes command is legal in current state.1032 1033 (typ, [data]) = <instance>.xatom(name, arg, ...)1034 1035 Returns response appropriate to extension command 'name'.1036 """1037 name = name.upper()1038 #if not name in self.capabilities: # Let the server decide!1039 # raise self.error('unknown extension command: %s' % name)1040 if not name in Commands:1041 Commands[name] = (self.state,)1042 return self._simple_command(name, *args)1043 1044 1045 1046 # Private methods1047 1048 1049 def _append_untagged(self, typ, dat):1050 if dat is None:1051 dat = b''1052 1053 # During idle, queue untagged responses for delivery via iteration1054 if self._idle_capture:1055 # Responses containing literal strings are passed to us one data1056 # fragment at a time, while others arrive in a single call.1057 if (not self._idle_responses or1058 isinstance(self._idle_responses[-1][1][-1], bytes)):1059 # We are not continuing a fragmented response; start a new one1060 self._idle_responses.append((typ, [dat]))1061 else:1062 # We are continuing a fragmented response; append the fragment1063 response = self._idle_responses[-1]1064 assert response[0] == typ1065 response[1].append(dat)1066 if __debug__ and self.debug >= 5:1067 self._mesg(f'idle: queue untagged {typ} {dat!r}')1068 return1069 1070 ur = self.untagged_responses1071 if __debug__:1072 if self.debug >= 5:1073 self._mesg('untagged_responses[%s] %s += ["%r"]' %1074 (typ, len(ur.get(typ,'')), dat))1075 if typ in ur:1076 ur[typ].append(dat)1077 else:1078 ur[typ] = [dat]1079 1080 1081 def _check_bye(self):1082 bye = self.untagged_responses.get('BYE')1083 if bye:1084 raise self.abort(bye[-1].decode(self._encoding, 'replace'))1085 1086 1087 def _command(self, name, *args):1088 1089 if self.state not in Commands[name]:1090 self.literal = None1091 raise self.error("command %s illegal in state %s, "1092 "only allowed in states %s" %1093 (name, self.state,1094 ', '.join(Commands[name])))1095 1096 for typ in ('OK', 'NO', 'BAD'):1097 if typ in self.untagged_responses:1098 del self.untagged_responses[typ]1099 1100 if 'READ-ONLY' in self.untagged_responses \1101 and not self.is_readonly:1102 raise self.readonly('mailbox status changed to READ-ONLY')1103 1104 tag = self._new_tag()1105 name = bytes(name, self._encoding)1106 data = tag + b' ' + name1107 for arg in args:1108 if arg is None: continue1109 if isinstance(arg, str):1110 arg = bytes(arg, self._encoding)1111 data = data + b' ' + arg1112 1113 literal = self.literal1114 if literal is not None:1115 self.literal = None1116 if type(literal) is type(self._command):1117 literator = literal1118 else:1119 literator = None1120 if self.utf8_enabled:1121 data = data + bytes(' UTF8 (~{%s}' % len(literal), self._encoding)1122 literal = literal + b')'1123 else:1124 data = data + bytes(' {%s}' % len(literal), self._encoding)1125 1126 if __debug__:1127 if self.debug >= 4:1128 self._mesg('> %r' % data)1129 else:1130 self._log('> %r' % data)1131 1132 try:1133 self.send(data + CRLF)1134 except OSError as val:1135 raise self.abort('socket error: %s' % val)1136 1137 if literal is None:1138 return tag1139 1140 while 1:1141 # Wait for continuation response1142 1143 while self._get_response():1144 if self.tagged_commands[tag]: # BAD/NO?1145 return tag1146 1147 # Send literal1148 1149 if literator:1150 literal = literator(self.continuation_response)1151 1152 if __debug__:1153 if self.debug >= 4:1154 self._mesg('write literal size %s' % len(literal))1155 1156 try:1157 self.send(literal)1158 self.send(CRLF)1159 except OSError as val:1160 raise self.abort('socket error: %s' % val)1161 1162 if not literator:1163 break1164 1165 return tag1166 1167 1168 def _command_complete(self, name, tag):1169 logout = (name == 'LOGOUT')1170 # BYE is expected after LOGOUT1171 if not logout:1172 self._check_bye()1173 try:1174 typ, data = self._get_tagged_response(tag, expect_bye=logout)1175 except self.abort as val:1176 raise self.abort('command: %s => %s' % (name, val))1177 except self.error as val:1178 raise self.error('command: %s => %s' % (name, val))1179 if not logout:1180 self._check_bye()1181 if typ == 'BAD':1182 raise self.error('%s command error: %s %s' % (name, typ, data))1183 return typ, data1184 1185 1186 def _get_capabilities(self):1187 typ, dat = self.capability()1188 if dat == [None]:1189 raise self.error('no CAPABILITY response from server')1190 dat = str(dat[-1], self._encoding)1191 dat = dat.upper()1192 self.capabilities = tuple(dat.split())1193 1194 1195 def _get_response(self, start_timeout=False):1196 1197 # Read response and store.1198 #1199 # Returns None for continuation responses,1200 # otherwise first response line received.