1"""IMAP4 client. 2 3Based on RFC 2060. 4 5Public class: IMAP4 6Public variable: Debug 7Public functions: Internaldate2tuple 8 Int2AP 9 ParseFlags 10 Time2Internaldate 11""" 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 23__version__ = "2.58" 24 25import binascii, errno, random, re, socket, subprocess, sys, time, calendar 26from datetime import datetime, timezone, timedelta 27from io import DEFAULT_BUFFER_SIZE 28 29try: 30 import ssl 31 HAVE_SSL = True 32except ImportError: 33 HAVE_SSL = False 34 35__all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple", 36 "Int2AP", "ParseFlags", "Time2Internaldate"] 37 38# Globals 39 40CRLF = b'\r\n' 41Debug = 0 42IMAP4_PORT = 143 43IMAP4_SSL_PORT = 993 44AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first 45 46# Maximal line length when calling readline(). This is to prevent 47# reading arbitrary length lines. RFC 3501 and 2060 (IMAP 4rev1) 48# don't specify a line length. RFC 2683 suggests limiting client 49# command lines to 1000 octets and that servers should be prepared 50# to accept command lines up to 8000 octets, so we used to use 10K here. 51# In the modern world (eg: gmail) the response to, for example, a 52# search command can be quite large, so we now use 1M. 53_MAXLINE = 1000000 54 55 56# Commands 57 58Commands = { 59 # name valid states 60 'APPEND': ('AUTH', 'SELECTED'), 61 'AUTHENTICATE': ('NONAUTH',), 62 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 63 'CHECK': ('SELECTED',), 64 'CLOSE': ('SELECTED',), 65 'COPY': ('SELECTED',), 66 'CREATE': ('AUTH', 'SELECTED'), 67 'DELETE': ('AUTH', 'SELECTED'), 68 'DELETEACL': ('AUTH', 'SELECTED'), 69 'ENABLE': ('AUTH', ), 70 'EXAMINE': ('AUTH', 'SELECTED'), 71 'EXPUNGE': ('SELECTED',), 72 'FETCH': ('SELECTED',), 73 'GETACL': ('AUTH', 'SELECTED'), 74 'GETANNOTATION':('AUTH', 'SELECTED'), 75 'GETQUOTA': ('AUTH', 'SELECTED'), 76 'GETQUOTAROOT': ('AUTH', 'SELECTED'), 77 'MYRIGHTS': ('AUTH', 'SELECTED'), 78 'LIST': ('AUTH', 'SELECTED'), 79 'LOGIN': ('NONAUTH',), 80 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 81 'LSUB': ('AUTH', 'SELECTED'), 82 'MOVE': ('SELECTED',), 83 'NAMESPACE': ('AUTH', 'SELECTED'), 84 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 85 'PARTIAL': ('SELECTED',), # NB: obsolete 86 'PROXYAUTH': ('AUTH',), 87 'RENAME': ('AUTH', 'SELECTED'), 88 'SEARCH': ('SELECTED',), 89 'SELECT': ('AUTH', 'SELECTED'), 90 'SETACL': ('AUTH', 'SELECTED'), 91 'SETANNOTATION':('AUTH', 'SELECTED'), 92 'SETQUOTA': ('AUTH', 'SELECTED'), 93 'SORT': ('SELECTED',), 94 'STARTTLS': ('NONAUTH',), 95 'STATUS': ('AUTH', 'SELECTED'), 96 'STORE': ('SELECTED',), 97 'SUBSCRIBE': ('AUTH', 'SELECTED'), 98 'THREAD': ('SELECTED',), 99 'UID': ('SELECTED',), 100 'UNSUBSCRIBE': ('AUTH', 'SELECTED'), 101 'UNSELECT': ('SELECTED',), 102 } 103 104# Patterns to match server responses 105 106Continuation = re.compile(br'\+( (?P<data>.*))?') 107Flags = re.compile(br'.*FLAGS \((?P<flags>[^\)]*)\)') 108InternalDate = re.compile(br'.*INTERNALDATE "' 109 br'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])' 110 br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])' 111 br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])' 112 br'"') 113# Literal is no longer used; kept for backward compatibility. 114Literal = re.compile(br'.*{(?P<size>\d+)}$', re.ASCII) 115MapCRLF = re.compile(br'\r\n|\r|\n') 116# We no longer exclude the ']' character from the data portion of the response 117# code, even though it violates the RFC. Popular IMAP servers such as Gmail 118# allow flags with ']', and there are programs (including imaplib!) that can 119# produce them. The problem with this is if the 'text' portion of the response 120# includes a ']' we'll parse the response wrong (which is the point of the RFC 121# restriction). However, that seems less likely to be a problem in practice 122# than being unable to correctly parse flags that include ']' chars, which 123# was reported as a real-world problem in issue #21815. 124Response_code = re.compile(br'\[(?P<type>[A-Z-]+)( (?P<data>.*))?\]') 125Untagged_response = re.compile(br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?') 126# Untagged_status is no longer used; kept for backward compatibility 127Untagged_status = re.compile( 128 br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?', re.ASCII) 129# We compile these in _mode_xxx. 130_Literal = br'.*{(?P<size>\d+)}$' 131_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?' 132 133 134 135class IMAP4: 136 137 r"""IMAP4 client class. 138 139 Instantiate with: IMAP4([host[, port[, timeout=None]]]) 140 141 host - host's name (default: localhost); 142 port - port number (default: standard IMAP4 port). 143 timeout - socket timeout (default: None) 144 If timeout is not given or is None, 145 the global default socket timeout is used 146 147 All IMAP4rev1 commands are supported by methods of the same 148 name (in lower-case). 149 150 All arguments to commands are converted to strings, except for 151 AUTHENTICATE, and the last argument to APPEND which is passed as 152 an IMAP4 literal. If necessary (the string contains any 153 non-printing characters or white-space and isn't enclosed with 154 either parentheses or double quotes) each string is quoted. 155 However, the 'password' argument to the LOGIN command is always 156 quoted. If you want to avoid having an argument string quoted 157 (eg: the 'flags' argument to STORE) then enclose the string in 158 parentheses (eg: "(\Deleted)"). 159 160 Each command returns a tuple: (type, [data, ...]) where 'type' 161 is usually 'OK' or 'NO', and 'data' is either the text from the 162 tagged response, or untagged results from command. Each 'data' 163 is either a string, or a tuple. If a tuple, then the first part 164 is the header of the response, and the second part contains 165 the data (ie: 'literal' value). 166 167 Errors raise the exception class <instance>.error("<reason>"). 168 IMAP4 server errors raise <instance>.abort("<reason>"), 169 which is a sub-class of 'error'. Mailbox status changes 170 from READ-WRITE to READ-ONLY raise the exception class 171 <instance>.readonly("<reason>"), which is a sub-class of 'abort'. 172 173 "error" exceptions imply a program error. 174 "abort" exceptions imply the connection should be reset, and 175 the command re-tried. 176 "readonly" exceptions imply the command should be re-tried. 177 178 Note: to use this module, you must read the RFCs pertaining to the 179 IMAP4 protocol, as the semantics of the arguments to each IMAP4 180 command are left to the invoker, not to mention the results. Also, 181 most IMAP servers implement a sub-set of the commands available here. 182 """ 183 184 class error(Exception): pass # Logical errors - debug required 185 class abort(error): pass # Service errors - close and retry 186 class readonly(abort): pass # Mailbox status changed to READ-ONLY 187 188 def __init__(self, host='', port=IMAP4_PORT, timeout=None): 189 self.debug = Debug 190 self.state = 'LOGOUT' 191 self.literal = None # A literal argument to a command 192 self.tagged_commands = {} # Tagged commands awaiting response 193 self.untagged_responses = {} # {typ: [data, ...], ...} 194 self.continuation_response = '' # Last continuation response 195 self.is_readonly = False # READ-ONLY desired state 196 self.tagnum = 0 197 self._tls_established = False 198 self._mode_ascii() 199 200 # Open socket to server. 201 202 self.open(host, port, timeout) 203 204 try: 205 self._connect() 206 except Exception: 207 try: 208 self.shutdown() 209 except OSError: 210 pass 211 raise 212 213 def _mode_ascii(self): 214 self.utf8_enabled = False 215 self._encoding = 'ascii' 216 self.Literal = re.compile(_Literal, re.ASCII) 217 self.Untagged_status = re.compile(_Untagged_status, re.ASCII) 218 219 220 def _mode_utf8(self): 221 self.utf8_enabled = True 222 self._encoding = 'utf-8' 223 self.Literal = re.compile(_Literal) 224 self.Untagged_status = re.compile(_Untagged_status) 225 226 227 def _connect(self): 228 # Create unique tag for this session, 229 # and compile tagged response matcher. 230 231 self.tagpre = Int2AP(random.randint(4096, 65535)) 232 self.tagre = re.compile(br'(?P<tag>' 233 + self.tagpre 234 + br'\d+) (?P<type>[A-Z]+) (?P<data>.*)', re.ASCII) 235 236 # Get server welcome message, 237 # request and store CAPABILITY response. 238 239 if __debug__: 240 self._cmd_log_len = 10 241 self._cmd_log_idx = 0 242 self._cmd_log = {} # Last `_cmd_log_len' interactions 243 if self.debug >= 1: 244 self._mesg('imaplib version %s' % __version__) 245 self._mesg('new IMAP4 connection, tag=%s' % self.tagpre) 246 247 self.welcome = self._get_response() 248 if 'PREAUTH' in self.untagged_responses: 249 self.state = 'AUTH' 250 elif 'OK' in self.untagged_responses: 251 self.state = 'NONAUTH' 252 else: 253 raise self.error(self.welcome) 254 255 self._get_capabilities() 256 if __debug__: 257 if self.debug >= 3: 258 self._mesg('CAPABILITIES: %r' % (self.capabilities,)) 259 260 for version in AllowedVersions: 261 if not version in self.capabilities: 262 continue 263 self.PROTOCOL_VERSION = version 264 return 265 266 raise self.error('server not IMAP4 compliant') 267 268 269 def __getattr__(self, attr): 270 # Allow UPPERCASE variants of IMAP4 command methods. 271 if attr in Commands: 272 return getattr(self, attr.lower()) 273 raise AttributeError("Unknown IMAP4 command: '%s'" % attr) 274 275 def __enter__(self): 276 return self 277 278 def __exit__(self, *args): 279 if self.state == "LOGOUT": 280 return 281 282 try: 283 self.logout() 284 except OSError: 285 pass 286 287 288 # Overridable methods 289 290 291 def _create_socket(self, timeout): 292 # Default value of IMAP4.host is '', but socket.getaddrinfo() 293 # (which is used by socket.create_connection()) expects None 294 # as a default value for host. 295 if timeout is not None and not timeout: 296 raise ValueError('Non-blocking socket (timeout=0) is not supported') 297 host = None if not self.host else self.host 298 sys.audit("imaplib.open", self, self.host, self.port) 299 address = (host, self.port) 300 if timeout is not None: 301 return socket.create_connection(address, timeout) 302 return socket.create_connection(address) 303 304 def open(self, host='', port=IMAP4_PORT, timeout=None): 305 """Setup connection to remote server on "host:port" 306 (default: localhost:standard IMAP4 port). 307 This connection will be used by the routines: 308 read, readline, send, shutdown. 309 """ 310 self.host = host 311 self.port = port 312 self.sock = self._create_socket(timeout) 313 self.file = self.sock.makefile('rb') 314 315 316 def read(self, size): 317 """Read 'size' bytes from remote.""" 318 return self.file.read(size) 319 320 321 def readline(self): 322 """Read line from remote.""" 323 line = self.file.readline(_MAXLINE + 1) 324 if len(line) > _MAXLINE: 325 raise self.error("got more than %d bytes" % _MAXLINE) 326 return line 327 328 329 def send(self, data): 330 """Send data to remote.""" 331 sys.audit("imaplib.send", self, data) 332 self.sock.sendall(data) 333 334 335 def shutdown(self): 336 """Close I/O established in "open".""" 337 self.file.close() 338 try: 339 self.sock.shutdown(socket.SHUT_RDWR) 340 except OSError as exc: 341 # The server might already have closed the connection. 342 # On Windows, this may result in WSAEINVAL (error 10022): 343 # An invalid operation was attempted. 344 if (exc.errno != errno.ENOTCONN 345 and getattr(exc, 'winerror', 0) != 10022): 346 raise 347 finally: 348 self.sock.close() 349 350 351 def socket(self): 352 """Return socket instance used to connect to IMAP4 server. 353 354 socket = <instance>.socket() 355 """ 356 return self.sock 357 358 359 360 # Utility methods 361 362 363 def recent(self): 364 """Return most recent 'RECENT' responses if any exist, 365 else prompt server for an update using the 'NOOP' command. 366 367 (typ, [data]) = <instance>.recent() 368 369 'data' is None if no new messages, 370 else list of RECENT responses, most recent last. 371 """ 372 name = 'RECENT' 373 typ, dat = self._untagged_response('OK', [None], name) 374 if dat[-1]: 375 return typ, dat 376 typ, dat = self.noop() # Prod server for response 377 return self._untagged_response(typ, dat, name) 378 379 380 def response(self, code): 381 """Return data for response 'code' if received, or None. 382 383 Old value for response 'code' is cleared. 384 385 (code, [data]) = <instance>.response(code) 386 """ 387 return self._untagged_response(code, [None], code.upper()) 388 389 390 391 # IMAP4 commands 392 393 394 def append(self, mailbox, flags, date_time, message): 395 """Append message to named mailbox. 396 397 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message) 398 399 All args except `message' can be None. 400 """ 401 name = 'APPEND' 402 if not mailbox: 403 mailbox = 'INBOX' 404 if flags: 405 if (flags[0],flags[-1]) != ('(',')'): 406 flags = '(%s)' % flags 407 else: 408 flags = None 409 if date_time: 410 date_time = Time2Internaldate(date_time) 411 else: 412 date_time = None 413 literal = MapCRLF.sub(CRLF, message) 414 if self.utf8_enabled: 415 literal = b'UTF8 (' + literal + b')' 416 self.literal = literal 417 return self._simple_command(name, mailbox, flags, date_time) 418 419 420 def authenticate(self, mechanism, authobject): 421 """Authenticate command - requires response processing. 422 423 'mechanism' specifies which authentication mechanism is to 424 be used - it must appear in <instance>.capabilities in the 425 form AUTH=<mechanism>. 426 427 'authobject' must be a callable object: 428 429 data = authobject(response) 430 431 It will be called to process server continuation responses; the 432 response argument it is passed will be a bytes. It should return bytes 433 data that will be base64 encoded and sent to the server. It should 434 return None if the client abort response '*' should be sent instead. 435 """ 436 mech = mechanism.upper() 437 # XXX: shouldn't this code be removed, not commented out? 438 #cap = 'AUTH=%s' % mech 439 #if not cap in self.capabilities: # Let the server decide! 440 # raise self.error("Server doesn't allow %s authentication." % mech) 441 self.literal = _Authenticator(authobject).process 442 typ, dat = self._simple_command('AUTHENTICATE', mech) 443 if typ != 'OK': 444 raise self.error(dat[-1].decode('utf-8', 'replace')) 445 self.state = 'AUTH' 446 return typ, dat 447 448 449 def capability(self): 450 """(typ, [data]) = <instance>.capability() 451 Fetch capabilities list from server.""" 452 453 name = 'CAPABILITY' 454 typ, dat = self._simple_command(name) 455 return self._untagged_response(typ, dat, name) 456 457 458 def check(self): 459 """Checkpoint mailbox on server. 460 461 (typ, [data]) = <instance>.check() 462 """ 463 return self._simple_command('CHECK') 464 465 466 def close(self): 467 """Close currently selected mailbox. 468 469 Deleted messages are removed from writable mailbox. 470 This is the recommended command before 'LOGOUT'. 471 472 (typ, [data]) = <instance>.close() 473 """ 474 try: 475 typ, dat = self._simple_command('CLOSE') 476 finally: 477 self.state = 'AUTH' 478 return typ, dat 479 480 481 def copy(self, message_set, new_mailbox): 482 """Copy 'message_set' messages onto end of 'new_mailbox'. 483 484 (typ, [data]) = <instance>.copy(message_set, new_mailbox) 485 """ 486 return self._simple_command('COPY', message_set, new_mailbox) 487 488 489 def create(self, mailbox): 490 """Create new mailbox. 491 492 (typ, [data]) = <instance>.create(mailbox) 493 """ 494 return self._simple_command('CREATE', mailbox) 495 496 497 def delete(self, mailbox): 498 """Delete old mailbox. 499 500 (typ, [data]) = <instance>.delete(mailbox) 501 """ 502 return self._simple_command('DELETE', mailbox) 503 504 def deleteacl(self, mailbox, who): 505 """Delete the ACLs (remove any rights) set for who on mailbox. 506 507 (typ, [data]) = <instance>.deleteacl(mailbox, who) 508 """ 509 return self._simple_command('DELETEACL', mailbox, who) 510 511 def enable(self, capability): 512 """Send an RFC5161 enable string to the server. 513 514 (typ, [data]) = <instance>.enable(capability) 515 """ 516 if 'ENABLE' not in self.capabilities: 517 raise IMAP4.error("Server does not support ENABLE") 518 typ, data = self._simple_command('ENABLE', capability) 519 if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper(): 520 self._mode_utf8() 521 return typ, data 522 523 def expunge(self): 524 """Permanently remove deleted items from selected mailbox. 525 526 Generates 'EXPUNGE' response for each deleted message. 527 528 (typ, [data]) = <instance>.expunge() 529 530 'data' is list of 'EXPUNGE'd message numbers in order received. 531 """ 532 name = 'EXPUNGE' 533 typ, dat = self._simple_command(name) 534 return self._untagged_response(typ, dat, name) 535 536 537 def fetch(self, message_set, message_parts): 538 """Fetch (parts of) messages. 539 540 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts) 541 542 'message_parts' should be a string of selected parts 543 enclosed in parentheses, eg: "(UID BODY[TEXT])". 544 545 'data' are tuples of message part envelope and data. 546 """ 547 name = 'FETCH' 548 typ, dat = self._simple_command(name, message_set, message_parts) 549 return self._untagged_response(typ, dat, name) 550 551 552 def getacl(self, mailbox): 553 """Get the ACLs for a mailbox. 554 555 (typ, [data]) = <instance>.getacl(mailbox) 556 """ 557 typ, dat = self._simple_command('GETACL', mailbox) 558 return self._untagged_response(typ, dat, 'ACL') 559 560 561 def getannotation(self, mailbox, entry, attribute): 562 """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute) 563 Retrieve ANNOTATIONs.""" 564 565 typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute) 566 return self._untagged_response(typ, dat, 'ANNOTATION') 567 568 569 def getquota(self, root): 570 """Get the quota root's resource usage and limits. 571 572 Part of the IMAP4 QUOTA extension defined in rfc2087. 573 574 (typ, [data]) = <instance>.getquota(root) 575 """ 576 typ, dat = self._simple_command('GETQUOTA', root) 577 return self._untagged_response(typ, dat, 'QUOTA') 578 579 580 def getquotaroot(self, mailbox): 581 """Get the list of quota roots for the named mailbox. 582 583 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox) 584 """ 585 typ, dat = self._simple_command('GETQUOTAROOT', mailbox) 586 typ, quota = self._untagged_response(typ, dat, 'QUOTA') 587 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT') 588 return typ, [quotaroot, quota] 589 590 591 def list(self, directory='""', pattern='*'): 592 """List mailbox names in directory matching pattern. 593 594 (typ, [data]) = <instance>.list(directory='""', pattern='*') 595 596 'data' is list of LIST responses. 597 """ 598 name = 'LIST' 599 typ, dat = self._simple_command(name, directory, pattern) 600 return self._untagged_response(typ, dat, name) 601 602 603 def login(self, user, password): 604 """Identify client using plaintext password. 605 606 (typ, [data]) = <instance>.login(user, password) 607 608 NB: 'password' will be quoted. 609 """ 610 typ, dat = self._simple_command('LOGIN', user, self._quote(password)) 611 if typ != 'OK': 612 raise self.error(dat[-1]) 613 self.state = 'AUTH' 614 return typ, dat 615 616 617 def login_cram_md5(self, user, password): 618 """ Force use of CRAM-MD5 authentication. 619 620 (typ, [data]) = <instance>.login_cram_md5(user, password) 621 """ 622 self.user, self.password = user, password 623 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH) 624 625 626 def _CRAM_MD5_AUTH(self, challenge): 627 """ Authobject to use with CRAM-MD5 authentication. """ 628 import hmac 629 pwd = (self.password.encode('utf-8') if isinstance(self.password, str) 630 else self.password) 631 return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest() 632 633 634 def logout(self): 635 """Shutdown connection to server. 636 637 (typ, [data]) = <instance>.logout() 638 639 Returns server 'BYE' response. 640 """ 641 self.state = 'LOGOUT' 642 typ, dat = self._simple_command('LOGOUT') 643 self.shutdown() 644 return typ, dat 645 646 647 def lsub(self, directory='""', pattern='*'): 648 """List 'subscribed' mailbox names in directory matching pattern. 649 650 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*') 651 652 'data' are tuples of message part envelope and data. 653 """ 654 name = 'LSUB' 655 typ, dat = self._simple_command(name, directory, pattern) 656 return self._untagged_response(typ, dat, name) 657 658 def myrights(self, mailbox): 659 """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox). 660 661 (typ, [data]) = <instance>.myrights(mailbox) 662 """ 663 typ,dat = self._simple_command('MYRIGHTS', mailbox) 664 return self._untagged_response(typ, dat, 'MYRIGHTS') 665 666 def namespace(self): 667 """ Returns IMAP namespaces ala rfc2342 668 669 (typ, [data, ...]) = <instance>.namespace() 670 """ 671 name = 'NAMESPACE' 672 typ, dat = self._simple_command(name) 673 return self._untagged_response(typ, dat, name) 674 675 676 def noop(self): 677 """Send NOOP command. 678 679 (typ, [data]) = <instance>.noop() 680 """ 681 if __debug__: 682 if self.debug >= 3: 683 self._dump_ur(self.untagged_responses) 684 return self._simple_command('NOOP') 685 686 687 def partial(self, message_num, message_part, start, length): 688 """Fetch truncated part of a message. 689 690 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length) 691 692 'data' is tuple of message part envelope and data. 693 """ 694 name = 'PARTIAL' 695 typ, dat = self._simple_command(name, message_num, message_part, start, length) 696 return self._untagged_response(typ, dat, 'FETCH') 697 698 699 def proxyauth(self, user): 700 """Assume authentication as "user". 701 702 Allows an authorised administrator to proxy into any user's 703 mailbox. 704 705 (typ, [data]) = <instance>.proxyauth(user) 706 """ 707 708 name = 'PROXYAUTH' 709 return self._simple_command('PROXYAUTH', user) 710 711 712 def rename(self, oldmailbox, newmailbox): 713 """Rename old mailbox name to new. 714 715 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox) 716 """ 717 return self._simple_command('RENAME', oldmailbox, newmailbox) 718 719 720 def search(self, charset, *criteria): 721 """Search mailbox for matching messages. 722 723 (typ, [data]) = <instance>.search(charset, criterion, ...) 724 725 'data' is space separated list of matching message numbers. 726 If UTF8 is enabled, charset MUST be None. 727 """ 728 name = 'SEARCH' 729 if charset: 730 if self.utf8_enabled: 731 raise IMAP4.error("Non-None charset not valid in UTF8 mode") 732 typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria) 733 else: 734 typ, dat = self._simple_command(name, *criteria) 735 return self._untagged_response(typ, dat, name) 736 737 738 def select(self, mailbox='INBOX', readonly=False): 739 """Select a mailbox. 740 741 Flush all untagged responses. 742 743 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False) 744 745 'data' is count of messages in mailbox ('EXISTS' response). 746 747 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so 748 other responses should be obtained via <instance>.response('FLAGS') etc. 749 """ 750 self.untagged_responses = {} # Flush old responses. 751 self.is_readonly = readonly 752 if readonly: 753 name = 'EXAMINE' 754 else: 755 name = 'SELECT' 756 typ, dat = self._simple_command(name, mailbox) 757 if typ != 'OK': 758 self.state = 'AUTH' # Might have been 'SELECTED' 759 return typ, dat 760 self.state = 'SELECTED' 761 if 'READ-ONLY' in self.untagged_responses \ 762 and not readonly: 763 if __debug__: 764 if self.debug >= 1: 765 self._dump_ur(self.untagged_responses) 766 raise self.readonly('%s is not writable' % mailbox) 767 return typ, self.untagged_responses.get('EXISTS', [None]) 768 769 770 def setacl(self, mailbox, who, what): 771 """Set a mailbox acl. 772 773 (typ, [data]) = <instance>.setacl(mailbox, who, what) 774 """ 775 return self._simple_command('SETACL', mailbox, who, what) 776 777 778 def setannotation(self, *args): 779 """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+) 780 Set ANNOTATIONs.""" 781 782 typ, dat = self._simple_command('SETANNOTATION', *args) 783 return self._untagged_response(typ, dat, 'ANNOTATION') 784 785 786 def setquota(self, root, limits): 787 """Set the quota root's resource limits. 788 789 (typ, [data]) = <instance>.setquota(root, limits) 790 """ 791 typ, dat = self._simple_command('SETQUOTA', root, limits) 792 return self._untagged_response(typ, dat, 'QUOTA') 793 794 795 def sort(self, sort_criteria, charset, *search_criteria): 796 """IMAP4rev1 extension SORT command. 797 798 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...) 799 """ 800 name = 'SORT' 801 #if not name in self.capabilities: # Let the server decide! 802 # raise self.error('unimplemented extension command: %s' % name) 803 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'): 804 sort_criteria = '(%s)' % sort_criteria 805 typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria) 806 return self._untagged_response(typ, dat, name) 807 808 809 def starttls(self, ssl_context=None): 810 name = 'STARTTLS' 811 if not HAVE_SSL: 812 raise self.error('SSL support missing') 813 if self._tls_established: 814 raise self.abort('TLS session already established') 815 if name not in self.capabilities: 816 raise self.abort('TLS not supported by server') 817 # Generate a default SSL context if none was passed. 818 if ssl_context is None: 819 ssl_context = ssl._create_stdlib_context() 820 typ, dat = self._simple_command(name) 821 if typ == 'OK': 822 self.sock = ssl_context.wrap_socket(self.sock, 823 server_hostname=self.host) 824 self.file = self.sock.makefile('rb') 825 self._tls_established = True 826 self._get_capabilities() 827 else: 828 raise self.error("Couldn't establish TLS session") 829 return self._untagged_response(typ, dat, name) 830 831 832 def status(self, mailbox, names): 833 """Request named status conditions for mailbox. 834 835 (typ, [data]) = <instance>.status(mailbox, names) 836 """ 837 name = 'STATUS' 838 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide! 839 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name) 840 typ, dat = self._simple_command(name, mailbox, names) 841 return self._untagged_response(typ, dat, name) 842 843 844 def store(self, message_set, command, flags): 845 """Alters flag dispositions for messages in mailbox. 846 847 (typ, [data]) = <instance>.store(message_set, command, flags) 848 """ 849 if (flags[0],flags[-1]) != ('(',')'): 850 flags = '(%s)' % flags # Avoid quoting the flags 851 typ, dat = self._simple_command('STORE', message_set, command, flags) 852 return self._untagged_response(typ, dat, 'FETCH') 853 854 855 def subscribe(self, mailbox): 856 """Subscribe to new mailbox. 857 858 (typ, [data]) = <instance>.subscribe(mailbox) 859 """ 860 return self._simple_command('SUBSCRIBE', mailbox) 861 862 863 def thread(self, threading_algorithm, charset, *search_criteria): 864 """IMAPrev1 extension THREAD command. 865 866 (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...) 867 """ 868 name = 'THREAD' 869 typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria) 870 return self._untagged_response(typ, dat, name) 871 872 873 def uid(self, command, *args): 874 """Execute "command arg ..." with messages identified by UID, 875 rather than message number. 876 877 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...) 878 879 Returns response appropriate to 'command'. 880 """ 881 command = command.upper() 882 if not command in Commands: 883 raise self.error("Unknown IMAP4 UID command: %s" % command) 884 if self.state not in Commands[command]: 885 raise self.error("command %s illegal in state %s, " 886 "only allowed in states %s" % 887 (command, self.state, 888 ', '.join(Commands[command]))) 889 name = 'UID' 890 typ, dat = self._simple_command(name, command, *args) 891 if command in ('SEARCH', 'SORT', 'THREAD'): 892 name = command 893 else: 894 name = 'FETCH' 895 return self._untagged_response(typ, dat, name) 896 897 898 def unsubscribe(self, mailbox): 899 """Unsubscribe from old mailbox. 900 901 (typ, [data]) = <instance>.unsubscribe(mailbox) 902 """ 903 return self._simple_command('UNSUBSCRIBE', mailbox) 904 905 906 def unselect(self): 907 """Free server's resources associated with the selected mailbox 908 and returns the server to the authenticated state. 909 This command performs the same actions as CLOSE, except 910 that no messages are permanently removed from the currently 911 selected mailbox. 912 913 (typ, [data]) = <instance>.unselect() 914 """ 915 try: 916 typ, data = self._simple_command('UNSELECT') 917 finally: 918 self.state = 'AUTH' 919 return typ, data 920 921 922 def xatom(self, name, *args): 923 """Allow simple extension commands 924 notified by server in CAPABILITY response. 925 926 Assumes command is legal in current state. 927 928 (typ, [data]) = <instance>.xatom(name, arg, ...) 929 930 Returns response appropriate to extension command `name'. 931 """ 932 name = name.upper() 933 #if not name in self.capabilities: # Let the server decide! 934 # raise self.error('unknown extension command: %s' % name) 935 if not name in Commands: 936 Commands[name] = (self.state,) 937 return self._simple_command(name, *args) 938 939 940 941 # Private methods 942 943 944 def _append_untagged(self, typ, dat): 945 if dat is None: 946 dat = b'' 947 ur = self.untagged_responses 948 if __debug__: 949 if self.debug >= 5: 950 self._mesg('untagged_responses[%s] %s += ["%r"]' % 951 (typ, len(ur.get(typ,'')), dat)) 952 if typ in ur: 953 ur[typ].append(dat) 954 else: 955 ur[typ] = [dat] 956 957 958 def _check_bye(self): 959 bye = self.untagged_responses.get('BYE') 960 if bye: 961 raise self.abort(bye[-1].decode(self._encoding, 'replace')) 962 963 964 def _command(self, name, *args): 965 966 if self.state not in Commands[name]: 967 self.literal = None 968 raise self.error("command %s illegal in state %s, " 969 "only allowed in states %s" % 970 (name, self.state, 971 ', '.join(Commands[name]))) 972 973 for typ in ('OK', 'NO', 'BAD'): 974 if typ in self.untagged_responses: 975 del self.untagged_responses[typ] 976 977 if 'READ-ONLY' in self.untagged_responses \ 978 and not self.is_readonly: 979 raise self.readonly('mailbox status changed to READ-ONLY') 980 981 tag = self._new_tag() 982 name = bytes(name, self._encoding) 983 data = tag + b' ' + name 984 for arg in args: 985 if arg is None: continue 986 if isinstance(arg, str): 987 arg = bytes(arg, self._encoding) 988 data = data + b' ' + arg 989 990 literal = self.literal 991 if literal is not None: 992 self.literal = None 993 if type(literal) is type(self._command): 994 literator = literal 995 else: 996 literator = None 997 data = data + bytes(' {%s}' % len(literal), self._encoding) 998 999 if __debug__: 1000 if self.debug >= 4: 1001 self._mesg('> %r' % data) 1002 else: 1003 self._log('> %r' % data) 1004 1005 try: 1006 self.send(data + CRLF) 1007 except OSError as val: 1008 raise self.abort('socket error: %s' % val) 1009 1010 if literal is None: 1011 return tag 1012 1013 while 1: 1014 # Wait for continuation response 1015 1016 while self._get_response(): 1017 if self.tagged_commands[tag]: # BAD/NO? 1018 return tag 1019 1020 # Send literal 1021 1022 if literator: 1023 literal = literator(self.continuation_response) 1024 1025 if __debug__: 1026 if self.debug >= 4: 1027 self._mesg('write literal size %s' % len(literal)) 1028 1029 try: 1030 self.send(literal) 1031 self.send(CRLF) 1032 except OSError as val: 1033 raise self.abort('socket error: %s' % val) 1034 1035 if not literator: 1036 break 1037 1038 return tag 1039 1040 1041 def _command_complete(self, name, tag): 1042 logout = (name == 'LOGOUT') 1043 # BYE is expected after LOGOUT 1044 if not logout: 1045 self._check_bye() 1046 try: 1047 typ, data = self._get_tagged_response(tag, expect_bye=logout) 1048 except self.abort as val: 1049 raise self.abort('command: %s => %s' % (name, val)) 1050 except self.error as val: 1051 raise self.error('command: %s => %s' % (name, val)) 1052 if not logout: 1053 self._check_bye() 1054 if typ == 'BAD': 1055 raise self.error('%s command error: %s %s' % (name, typ, data)) 1056 return typ, data 1057 1058 1059 def _get_capabilities(self): 1060 typ, dat = self.capability() 1061 if dat == [None]: 1062 raise self.error('no CAPABILITY response from server') 1063 dat = str(dat[-1], self._encoding) 1064 dat = dat.upper() 1065 self.capabilities = tuple(dat.split()) 1066 1067 1068 def _get_response(self): 1069 1070 # Read response and store. 1071 # 1072 # Returns None for continuation responses, 1073 # otherwise first response line received. 1074 1075 resp = self._get_line() 1076 1077 # Command completion response? 1078 1079 if self._match(self.tagre, resp): 1080 tag = self.mo.group('tag') 1081 if not tag in self.tagged_commands: 1082 raise self.abort('unexpected tagged response: %r' % resp) 1083 1084 typ = self.mo.group('type') 1085 typ = str(typ, self._encoding) 1086 dat = self.mo.group('data') 1087 self.tagged_commands[tag] = (typ, [dat]) 1088 else: 1089 dat2 = None 1090 1091 # '*' (untagged) responses? 1092 1093 if not self._match(Untagged_response, resp): 1094 if self._match(self.Untagged_status, resp): 1095 dat2 = self.mo.group('data2') 1096 1097 if self.mo is None: 1098 # Only other possibility is '+' (continuation) response... 1099 1100 if self._match(Continuation, resp): 1101 self.continuation_response = self.mo.group('data') 1102 return None # NB: indicates continuation 1103 1104 raise self.abort("unexpected response: %r" % resp) 1105 1106 typ = self.mo.group('type') 1107 typ = str(typ, self._encoding) 1108 dat = self.mo.group('data') 1109 if dat is None: dat = b'' # Null untagged response 1110 if dat2: dat = dat + b' ' + dat2 1111 1112 # Is there a literal to come? 1113 1114 while self._match(self.Literal, dat): 1115 1116 # Read literal direct from connection. 1117 1118 size = int(self.mo.group('size')) 1119 if __debug__: 1120 if self.debug >= 4: 1121 self._mesg('read literal size %s' % size) 1122 data = self.read(size) 1123 1124 # Store response with literal as tuple 1125 1126 self._append_untagged(typ, (dat, data)) 1127 1128 # Read trailer - possibly containing another literal 1129 1130 dat = self._get_line() 1131 1132 self._append_untagged(typ, dat) 1133 1134 # Bracketed response information? 1135 1136 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat): 1137 typ = self.mo.group('type') 1138 typ = str(typ, self._encoding) 1139 self._append_untagged(typ, self.mo.group('data')) 1140 1141 if __debug__: 1142 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'): 1143 self._mesg('%s response: %r' % (typ, dat)) 1144 1145 return resp 1146 1147 1148 def _get_tagged_response(self, tag, expect_bye=False): 1149 1150 while 1: 1151 result = self.tagged_commands[tag] 1152 if result is not None: 1153 del self.tagged_commands[tag] 1154 return result 1155 1156 if expect_bye: 1157 typ = 'BYE' 1158 bye = self.untagged_responses.pop(typ, None) 1159 if bye is not None: 1160 # Server replies to the "LOGOUT" command with "BYE" 1161 return (typ, bye) 1162 1163 # If we've seen a BYE at this point, the socket will be 1164 # closed, so report the BYE now. 1165 self._check_bye() 1166 1167 # Some have reported "unexpected response" exceptions. 1168 # Note that ignoring them here causes loops. 1169 # Instead, send me details of the unexpected response and 1170 # I'll update the code in `_get_response()'. 1171 1172 try: 1173 self._get_response() 1174 except self.abort as val: 1175 if __debug__: 1176 if self.debug >= 1: 1177 self.print_log() 1178 raise 1179 1180 1181 def _get_line(self): 1182 1183 line = self.readline() 1184 if not line: 1185 raise self.abort('socket error: EOF') 1186 1187 # Protocol mandates all lines terminated by CRLF 1188 if not line.endswith(b'\r\n'): 1189 raise self.abort('socket error: unterminated line: %r' % line) 1190 1191 line = line[:-2] 1192 if __debug__: 1193 if self.debug >= 4: 1194 self._mesg('< %r' % line) 1195 else: 1196 self._log('< %r' % line) 1197 return line 1198 1199 1200 def _match(self, cre, s): 1201 1202 # Run compiled regular expression match method on 's'. 1203 # Save result, return success. 1204 1205 self.mo = cre.match(s) 1206 if __debug__: 1207 if self.mo is not None and self.debug >= 5: 1208 self._mesg("\tmatched %r => %r" % (cre.pattern, self.mo.groups())) 1209 return self.mo is not None 1210 1211 1212 def _new_tag(self): 1213 1214 tag = self.tagpre + bytes(str(self.tagnum), self._encoding) 1215 self.tagnum = self.tagnum + 1 1216 self.tagged_commands[tag] = None 1217 return tag 1218 1219 1220 def _quote(self, arg): 1221 1222 arg = arg.replace('\\', '\\\\') 1223 arg = arg.replace('"', '\\"') 1224 1225 return '"' + arg + '"' 1226 1227 1228 def _simple_command(self, name, *args): 1229 1230 return self._command_complete(name, self._command(name, *args)) 1231 1232 1233 def _untagged_response(self, typ, dat, name): 1234 if typ == 'NO': 1235 return typ, dat 1236 if not name in self.untagged_responses: 1237 return typ, [None] 1238 data = self.untagged_responses.pop(name) 1239 if __debug__: 1240 if self.debug >= 5: 1241 self._mesg('untagged_responses[%s] => %s' % (name, data)) 1242 return typ, data 1243 1244 1245 if __debug__: 1246 1247 def _mesg(self, s, secs=None): 1248 if secs is None: 1249 secs = time.time() 1250 tm = time.strftime('%M:%S', time.localtime(secs)) 1251 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s)) 1252 sys.stderr.flush() 1253 1254 def _dump_ur(self, untagged_resp_dict): 1255 if not untagged_resp_dict: 1256 return 1257 items = (f'{key}: {value!r}' 1258 for key, value in untagged_resp_dict.items()) 1259 self._mesg('untagged responses dump:' + '\n\t\t'.join(items)) 1260 1261 def _log(self, line): 1262 # Keep log of last `_cmd_log_len' interactions for debugging. 1263 self._cmd_log[self._cmd_log_idx] = (line, time.time()) 1264 self._cmd_log_idx += 1 1265 if self._cmd_log_idx >= self._cmd_log_len: 1266 self._cmd_log_idx = 0 1267 1268 def print_log(self): 1269 self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log)) 1270 i, n = self._cmd_log_idx, self._cmd_log_len 1271 while n: 1272 try: 1273 self._mesg(*self._cmd_log[i]) 1274 except: 1275 pass 1276 i += 1 1277 if i >= self._cmd_log_len: 1278 i = 0 1279 n -= 1 1280 1281 1282if HAVE_SSL: 1283 1284 class IMAP4_SSL(IMAP4): 1285 1286 """IMAP4 client class over SSL connection 1287 1288 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile[, ssl_context[, timeout=None]]]]]]) 1289 1290 host - host's name (default: localhost); 1291 port - port number (default: standard IMAP4 SSL port); 1292 keyfile - PEM formatted file that contains your private key (default: None); 1293 certfile - PEM formatted certificate chain file (default: None); 1294 ssl_context - a SSLContext object that contains your certificate chain 1295 and private key (default: None) 1296 Note: if ssl_context is provided, then parameters keyfile or 1297 certfile should not be set otherwise ValueError is raised. 1298 timeout - socket timeout (default: None) If timeout is not given or is None, 1299 the global default socket timeout is used 1300 1301 for more documentation see the docstring of the parent class IMAP4. 1302 """ 1303 1304 1305 def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None, 1306 certfile=None, ssl_context=None, timeout=None): 1307 if ssl_context is not None and keyfile is not None: 1308 raise ValueError("ssl_context and keyfile arguments are mutually " 1309 "exclusive") 1310 if ssl_context is not None and certfile is not None: 1311 raise ValueError("ssl_context and certfile arguments are mutually " 1312 "exclusive") 1313 if keyfile is not None or certfile is not None: 1314 import warnings 1315 warnings.warn("keyfile and certfile are deprecated, use a " 1316 "custom ssl_context instead", DeprecationWarning, 2) 1317 self.keyfile = keyfile 1318 self.certfile = certfile 1319 if ssl_context is None: 1320 ssl_context = ssl._create_stdlib_context(certfile=certfile, 1321 keyfile=keyfile) 1322 self.ssl_context = ssl_context 1323 IMAP4.__init__(self, host, port, timeout) 1324 1325 def _create_socket(self, timeout): 1326 sock = IMAP4._create_socket(self, timeout) 1327 return self.ssl_context.wrap_socket(sock, 1328 server_hostname=self.host) 1329 1330 def open(self, host='', port=IMAP4_SSL_PORT, timeout=None): 1331 """Setup connection to remote server on "host:port". 1332 (default: localhost:standard IMAP4 SSL port). 1333 This connection will be used by the routines: 1334 read, readline, send, shutdown. 1335 """ 1336 IMAP4.open(self, host, port, timeout) 1337 1338 __all__.append("IMAP4_SSL") 1339 1340 1341class IMAP4_stream(IMAP4): 1342 1343 """IMAP4 client class over a stream 1344 1345 Instantiate with: IMAP4_stream(command) 1346 1347 "command" - a string that can be passed to subprocess.Popen() 1348 1349 for more documentation see the docstring of the parent class IMAP4. 1350 """ 1351 1352 1353 def __init__(self, command): 1354 self.command = command 1355 IMAP4.__init__(self) 1356 1357 1358 def open(self, host=None, port=None, timeout=None): 1359 """Setup a stream connection. 1360 This connection will be used by the routines: 1361 read, readline, send, shutdown. 1362 """ 1363 self.host = None # For compatibility with parent class 1364 self.port = None 1365 self.sock = None 1366 self.file = None 1367 self.process = subprocess.Popen(self.command, 1368 bufsize=DEFAULT_BUFFER_SIZE, 1369 stdin=subprocess.PIPE, stdout=subprocess.PIPE, 1370 shell=True, close_fds=True) 1371 self.writefile = self.process.stdin 1372 self.readfile = self.process.stdout 1373 1374 def read(self, size): 1375 """Read 'size' bytes from remote.""" 1376 return self.readfile.read(size) 1377 1378 1379 def readline(self): 1380 """Read line from remote.""" 1381 return self.readfile.readline() 1382 1383 1384 def send(self, data): 1385 """Send data to remote.""" 1386 self.writefile.write(data) 1387 self.writefile.flush() 1388 1389 1390 def shutdown(self): 1391 """Close I/O established in "open".""" 1392 self.readfile.close() 1393 self.writefile.close() 1394 self.process.wait() 1395 1396 1397 1398class _Authenticator: 1399 1400 """Private class to provide en/decoding 1401 for base64-based authentication conversation. 1402 """ 1403 1404 def __init__(self, mechinst): 1405 self.mech = mechinst # Callable object to provide/process data 1406 1407 def process(self, data): 1408 ret = self.mech(self.decode(data)) 1409 if ret is None: 1410 return b'*' # Abort conversation 1411 return self.encode(ret) 1412 1413 def encode(self, inp): 1414 # 1415 # Invoke binascii.b2a_base64 iteratively with 1416 # short even length buffers, strip the trailing 1417 # line feed from the result and append. "Even" 1418 # means a number that factors to both 6 and 8, 1419 # so when it gets to the end of the 8-bit input 1420 # there's no partial 6-bit output. 1421 # 1422 oup = b'' 1423 if isinstance(inp, str): 1424 inp = inp.encode('utf-8') 1425 while inp: 1426 if len(inp) > 48: 1427 t = inp[:48] 1428 inp = inp[48:] 1429 else: 1430 t = inp 1431 inp = b'' 1432 e = binascii.b2a_base64(t) 1433 if e: 1434 oup = oup + e[:-1] 1435 return oup 1436 1437 def decode(self, inp): 1438 if not inp: 1439 return b'' 1440 return binascii.a2b_base64(inp) 1441 1442Months = ' Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ') 1443Mon2num = {s.encode():n+1 for n, s in enumerate(Months[1:])} 1444 1445def Internaldate2tuple(resp): 1446 """Parse an IMAP4 INTERNALDATE string. 1447 1448 Return corresponding local time. The return value is a 1449 time.struct_time tuple or None if the string has wrong format. 1450 """ 1451 1452 mo = InternalDate.match(resp) 1453 if not mo: 1454 return None 1455 1456 mon = Mon2num[mo.group('mon')] 1457 zonen = mo.group('zonen') 1458 1459 day = int(mo.group('day')) 1460 year = int(mo.group('year')) 1461 hour = int(mo.group('hour')) 1462 min = int(mo.group('min')) 1463 sec = int(mo.group('sec')) 1464 zoneh = int(mo.group('zoneh')) 1465 zonem = int(mo.group('zonem')) 1466 1467 # INTERNALDATE timezone must be subtracted to get UT 1468 1469 zone = (zoneh*60 + zonem)*60 1470 if zonen == b'-': 1471 zone = -zone 1472 1473 tt = (year, mon, day, hour, min, sec, -1, -1, -1) 1474 utc = calendar.timegm(tt) - zone 1475 1476 return time.localtime(utc) 1477 1478 1479 1480def Int2AP(num): 1481 1482 """Convert integer to A-P string representation.""" 1483 1484 val = b''; AP = b'ABCDEFGHIJKLMNOP' 1485 num = int(abs(num)) 1486 while num: 1487 num, mod = divmod(num, 16) 1488 val = AP[mod:mod+1] + val 1489 return val 1490 1491 1492 1493def ParseFlags(resp): 1494 1495 """Convert IMAP4 flags response to python tuple.""" 1496 1497 mo = Flags.match(resp) 1498 if not mo: 1499 return () 1500 1501 return tuple(mo.group('flags').split()) 1502 1503 1504def Time2Internaldate(date_time): 1505 1506 """Convert date_time to IMAP4 INTERNALDATE representation. 1507 1508 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'. The 1509 date_time argument can be a number (int or float) representing 1510 seconds since epoch (as returned by time.time()), a 9-tuple 1511 representing local time, an instance of time.struct_time (as 1512 returned by time.localtime()), an aware datetime instance or a 1513 double-quoted string. In the last case, it is assumed to already 1514 be in the correct format. 1515 """ 1516 if isinstance(date_time, (int, float)): 1517 dt = datetime.fromtimestamp(date_time, 1518 timezone.utc).astimezone() 1519 elif isinstance(date_time, tuple): 1520 try: 1521 gmtoff = date_time.tm_gmtoff 1522 except AttributeError: 1523 if time.daylight: 1524 dst = date_time[8] 1525 if dst == -1: 1526 dst = time.localtime(time.mktime(date_time))[8] 1527 gmtoff = -(time.timezone, time.altzone)[dst] 1528 else: 1529 gmtoff = -time.timezone 1530 delta = timedelta(seconds=gmtoff) 1531 dt = datetime(*date_time[:6], tzinfo=timezone(delta)) 1532 elif isinstance(date_time, datetime): 1533 if date_time.tzinfo is None: 1534 raise ValueError("date_time must be aware") 1535 dt = date_time 1536 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'): 1537 return date_time # Assume in correct format 1538 else: 1539 raise ValueError("date_time not of a known type") 1540 fmt = '"%d-{}-%Y %H:%M:%S %z"'.format(Months[dt.month]) 1541 return dt.strftime(fmt) 1542 1543 1544 1545if __name__ == '__main__': 1546 1547 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]' 1548 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"' 1549 # to test the IMAP4_stream class 1550 1551 import getopt, getpass 1552 1553 try: 1554 optlist, args = getopt.getopt(sys.argv[1:], 'd:s:') 1555 except getopt.error as val: 1556 optlist, args = (), () 1557 1558 stream_command = None 1559 for opt,val in optlist: 1560 if opt == '-d': 1561 Debug = int(val) 1562 elif opt == '-s': 1563 stream_command = val 1564 if not args: args = (stream_command,) 1565 1566 if not args: args = ('',) 1567 1568 host = args[0] 1569 1570 USER = getpass.getuser() 1571 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost")) 1572 1573 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'} 1574 test_seq1 = ( 1575 ('login', (USER, PASSWD)), 1576 ('create', ('/tmp/xxx 1',)), 1577 ('rename', ('/tmp/xxx 1', '/tmp/yyy')), 1578 ('CREATE', ('/tmp/yyz 2',)), 1579 ('append', ('/tmp/yyz 2', None, None, test_mesg)), 1580 ('list', ('/tmp', 'yy*')), 1581 ('select', ('/tmp/yyz 2',)), 1582 ('search', (None, 'SUBJECT', 'test')), 1583 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')), 1584 ('store', ('1', 'FLAGS', r'(\Deleted)')), 1585 ('namespace', ()), 1586 ('expunge', ()), 1587 ('recent', ()), 1588 ('close', ()), 1589 ) 1590 1591 test_seq2 = ( 1592 ('select', ()), 1593 ('response',('UIDVALIDITY',)), 1594 ('uid', ('SEARCH', 'ALL')), 1595 ('response', ('EXISTS',)), 1596 ('append', (None, None, None, test_mesg)), 1597 ('recent', ()), 1598 ('logout', ()), 1599 ) 1600 1601 def run(cmd, args): 1602 M._mesg('%s %s' % (cmd, args)) 1603 typ, dat = getattr(M, cmd)(*args) 1604 M._mesg('%s => %s %s' % (cmd, typ, dat)) 1605 if typ == 'NO': raise dat[0] 1606 return dat 1607 1608 try: 1609 if stream_command: 1610 M = IMAP4_stream(stream_command) 1611 else: 1612 M = IMAP4(host) 1613 if M.state == 'AUTH': 1614 test_seq1 = test_seq1[1:] # Login not needed 1615 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION) 1616 M._mesg('CAPABILITIES = %r' % (M.capabilities,)) 1617 1618 for cmd,args in test_seq1: 1619 run(cmd, args) 1620 1621 for ml in run('list', ('/tmp/', 'yy%')): 1622 mo = re.match(r'.*"([^"]+)"$', ml) 1623 if mo: path = mo.group(1) 1624 else: path = ml.split()[-1] 1625 run('delete', (path,)) 1626 1627 for cmd,args in test_seq2: 1628 dat = run(cmd, args) 1629 1630 if (cmd,args) != ('uid', ('SEARCH', 'ALL')): 1631 continue 1632 1633 uid = dat[-1].split() 1634 if not uid: continue 1635 run('uid', ('FETCH', '%s' % uid[-1], 1636 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)')) 1637 1638 print('\nAll tests OK.') 1639 1640 except: 1641 print('\nTests failed.') 1642 1643 if not Debug: 1644 print(''' 1645If you would like to see debugging output, 1646try: %s -d5 1647''' % sys.argv[0]) 1648 1649 raise 1650