0001 """IMAP4 client. 0002 0003 Based on RFC 2060. 0004 0005 Public class: IMAP4 0006 Public variable: Debug 0007 Public functions: Internaldate2tuple 0008 Int2AP 0009 ParseFlags 0010 Time2Internaldate 0011 """ 0012 0013 # Author: Piers Lauder <piers@cs.su.oz.au> December 1997. 0014 # 0015 # Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998. 0016 # String method conversion by ESR, February 2001. 0017 # GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001. 0018 # IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002. 0019 # GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002. 0020 # PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002. 0021 0022 __version__ = "2.55" 0023 0024 import binascii, os, random, re, socket, sys, time 0025 0026 __all__ = ["IMAP4", "IMAP4_SSL", "IMAP4_stream", "Internaldate2tuple", 0027 "Int2AP", "ParseFlags", "Time2Internaldate"] 0028 0029 # Globals 0030 0031 CRLF = '\r\n' 0032 Debug = 0 0033 IMAP4_PORT = 143 0034 IMAP4_SSL_PORT = 993 0035 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first 0036 0037 # Commands 0038 0039 Commands = { 0040 # name valid states 0041 'APPEND': ('AUTH', 'SELECTED'), 0042 'AUTHENTICATE': ('NONAUTH',), 0043 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 0044 'CHECK': ('SELECTED',), 0045 'CLOSE': ('SELECTED',), 0046 'COPY': ('SELECTED',), 0047 'CREATE': ('AUTH', 'SELECTED'), 0048 'DELETE': ('AUTH', 'SELECTED'), 0049 'DELETEACL': ('AUTH', 'SELECTED'), 0050 'EXAMINE': ('AUTH', 'SELECTED'), 0051 'EXPUNGE': ('SELECTED',), 0052 'FETCH': ('SELECTED',), 0053 'GETACL': ('AUTH', 'SELECTED'), 0054 'GETQUOTA': ('AUTH', 'SELECTED'), 0055 'GETQUOTAROOT': ('AUTH', 'SELECTED'), 0056 'MYRIGHTS': ('AUTH', 'SELECTED'), 0057 'LIST': ('AUTH', 'SELECTED'), 0058 'LOGIN': ('NONAUTH',), 0059 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 0060 'LSUB': ('AUTH', 'SELECTED'), 0061 'NAMESPACE': ('AUTH', 'SELECTED'), 0062 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 0063 'PARTIAL': ('SELECTED',), # NB: obsolete 0064 'PROXYAUTH': ('AUTH',), 0065 'RENAME': ('AUTH', 'SELECTED'), 0066 'SEARCH': ('SELECTED',), 0067 'SELECT': ('AUTH', 'SELECTED'), 0068 'SETACL': ('AUTH', 'SELECTED'), 0069 'SETQUOTA': ('AUTH', 'SELECTED'), 0070 'SORT': ('SELECTED',), 0071 'STATUS': ('AUTH', 'SELECTED'), 0072 'STORE': ('SELECTED',), 0073 'SUBSCRIBE': ('AUTH', 'SELECTED'), 0074 'THREAD': ('SELECTED',), 0075 'UID': ('SELECTED',), 0076 'UNSUBSCRIBE': ('AUTH', 'SELECTED'), 0077 } 0078 0079 # Patterns to match server responses 0080 0081 Continuation = re.compile(r'\+( (?P<data>.*))?') 0082 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)') 0083 InternalDate = re.compile(r'.*INTERNALDATE "' 0084 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])' 0085 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])' 0086 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])' 0087 r'"') 0088 Literal = re.compile(r'.*{(?P<size>\d+)}$') 0089 MapCRLF = re.compile(r'\r\n|\r|\n') 0090 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]') 0091 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?') 0092 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?') 0093 0094 0095 0096 class IMAP4: 0097 0098 """IMAP4 client class. 0099 0100 Instantiate with: IMAP4([host[, port]]) 0101 0102 host - host's name (default: localhost); 0103 port - port number (default: standard IMAP4 port). 0104 0105 All IMAP4rev1 commands are supported by methods of the same 0106 name (in lower-case). 0107 0108 All arguments to commands are converted to strings, except for 0109 AUTHENTICATE, and the last argument to APPEND which is passed as 0110 an IMAP4 literal. If necessary (the string contains any 0111 non-printing characters or white-space and isn't enclosed with 0112 either parentheses or double quotes) each string is quoted. 0113 However, the 'password' argument to the LOGIN command is always 0114 quoted. If you want to avoid having an argument string quoted 0115 (eg: the 'flags' argument to STORE) then enclose the string in 0116 parentheses (eg: "(\Deleted)"). 0117 0118 Each command returns a tuple: (type, [data, ...]) where 'type' 0119 is usually 'OK' or 'NO', and 'data' is either the text from the 0120 tagged response, or untagged results from command. Each 'data' 0121 is either a string, or a tuple. If a tuple, then the first part 0122 is the header of the response, and the second part contains 0123 the data (ie: 'literal' value). 0124 0125 Errors raise the exception class <instance>.error("<reason>"). 0126 IMAP4 server errors raise <instance>.abort("<reason>"), 0127 which is a sub-class of 'error'. Mailbox status changes 0128 from READ-WRITE to READ-ONLY raise the exception class 0129 <instance>.readonly("<reason>"), which is a sub-class of 'abort'. 0130 0131 "error" exceptions imply a program error. 0132 "abort" exceptions imply the connection should be reset, and 0133 the command re-tried. 0134 "readonly" exceptions imply the command should be re-tried. 0135 0136 Note: to use this module, you must read the RFCs pertaining 0137 to the IMAP4 protocol, as the semantics of the arguments to 0138 each IMAP4 command are left to the invoker, not to mention 0139 the results. 0140 """ 0141 0142 class error(Exception): pass # Logical errors - debug required 0143 class abort(error): pass # Service errors - close and retry 0144 class readonly(abort): pass # Mailbox status changed to READ-ONLY 0145 0146 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]") 0147 0148 def __init__(self, host = '', port = IMAP4_PORT): 0149 self.debug = Debug 0150 self.state = 'LOGOUT' 0151 self.literal = None # A literal argument to a command 0152 self.tagged_commands = {} # Tagged commands awaiting response 0153 self.untagged_responses = {} # {typ: [data, ...], ...} 0154 self.continuation_response = '' # Last continuation response 0155 self.is_readonly = None # READ-ONLY desired state 0156 self.tagnum = 0 0157 0158 # Open socket to server. 0159 0160 self.open(host, port) 0161 0162 # Create unique tag for this session, 0163 # and compile tagged response matcher. 0164 0165 self.tagpre = Int2AP(random.randint(0, 31999)) 0166 self.tagre = re.compile(r'(?P<tag>' 0167 + self.tagpre 0168 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)') 0169 0170 # Get server welcome message, 0171 # request and store CAPABILITY response. 0172 0173 if __debug__: 0174 self._cmd_log_len = 10 0175 self._cmd_log_idx = 0 0176 self._cmd_log = {} # Last `_cmd_log_len' interactions 0177 if self.debug >= 1: 0178 self._mesg('imaplib version %s' % __version__) 0179 self._mesg('new IMAP4 connection, tag=%s' % self.tagpre) 0180 0181 self.welcome = self._get_response() 0182 if 'PREAUTH' in self.untagged_responses: 0183 self.state = 'AUTH' 0184 elif 'OK' in self.untagged_responses: 0185 self.state = 'NONAUTH' 0186 else: 0187 raise self.error(self.welcome) 0188 0189 cap = 'CAPABILITY' 0190 self._simple_command(cap) 0191 if not cap in self.untagged_responses: 0192 raise self.error('no CAPABILITY response from server') 0193 self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split()) 0194 0195 if __debug__: 0196 if self.debug >= 3: 0197 self._mesg('CAPABILITIES: %r' % (self.capabilities,)) 0198 0199 for version in AllowedVersions: 0200 if not version in self.capabilities: 0201 continue 0202 self.PROTOCOL_VERSION = version 0203 return 0204 0205 raise self.error('server not IMAP4 compliant') 0206 0207 0208 def __getattr__(self, attr): 0209 # Allow UPPERCASE variants of IMAP4 command methods. 0210 if attr in Commands: 0211 return getattr(self, attr.lower()) 0212 raise AttributeError("Unknown IMAP4 command: '%s'" % attr) 0213 0214 0215 0216 # Overridable methods 0217 0218 0219 def open(self, host = '', port = IMAP4_PORT): 0220 """Setup connection to remote server on "host:port" 0221 (default: localhost:standard IMAP4 port). 0222 This connection will be used by the routines: 0223 read, readline, send, shutdown. 0224 """ 0225 self.host = host 0226 self.port = port 0227 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 0228 self.sock.connect((host, port)) 0229 self.file = self.sock.makefile('rb') 0230 0231 0232 def read(self, size): 0233 """Read 'size' bytes from remote.""" 0234 return self.file.read(size) 0235 0236 0237 def readline(self): 0238 """Read line from remote.""" 0239 return self.file.readline() 0240 0241 0242 def send(self, data): 0243 """Send data to remote.""" 0244 self.sock.sendall(data) 0245 0246 0247 def shutdown(self): 0248 """Close I/O established in "open".""" 0249 self.file.close() 0250 self.sock.close() 0251 0252 0253 def socket(self): 0254 """Return socket instance used to connect to IMAP4 server. 0255 0256 socket = <instance>.socket() 0257 """ 0258 return self.sock 0259 0260 0261 0262 # Utility methods 0263 0264 0265 def recent(self): 0266 """Return most recent 'RECENT' responses if any exist, 0267 else prompt server for an update using the 'NOOP' command. 0268 0269 (typ, [data]) = <instance>.recent() 0270 0271 'data' is None if no new messages, 0272 else list of RECENT responses, most recent last. 0273 """ 0274 name = 'RECENT' 0275 typ, dat = self._untagged_response('OK', [None], name) 0276 if dat[-1]: 0277 return typ, dat 0278 typ, dat = self.noop() # Prod server for response 0279 return self._untagged_response(typ, dat, name) 0280 0281 0282 def response(self, code): 0283 """Return data for response 'code' if received, or None. 0284 0285 Old value for response 'code' is cleared. 0286 0287 (code, [data]) = <instance>.response(code) 0288 """ 0289 return self._untagged_response(code, [None], code.upper()) 0290 0291 0292 0293 # IMAP4 commands 0294 0295 0296 def append(self, mailbox, flags, date_time, message): 0297 """Append message to named mailbox. 0298 0299 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message) 0300 0301 All args except `message' can be None. 0302 """ 0303 name = 'APPEND' 0304 if not mailbox: 0305 mailbox = 'INBOX' 0306 if flags: 0307 if (flags[0],flags[-1]) != ('(',')'): 0308 flags = '(%s)' % flags 0309 else: 0310 flags = None 0311 if date_time: 0312 date_time = Time2Internaldate(date_time) 0313 else: 0314 date_time = None 0315 self.literal = MapCRLF.sub(CRLF, message) 0316 return self._simple_command(name, mailbox, flags, date_time) 0317 0318 0319 def authenticate(self, mechanism, authobject): 0320 """Authenticate command - requires response processing. 0321 0322 'mechanism' specifies which authentication mechanism is to 0323 be used - it must appear in <instance>.capabilities in the 0324 form AUTH=<mechanism>. 0325 0326 'authobject' must be a callable object: 0327 0328 data = authobject(response) 0329 0330 It will be called to process server continuation responses. 0331 It should return data that will be encoded and sent to server. 0332 It should return None if the client abort response '*' should 0333 be sent instead. 0334 """ 0335 mech = mechanism.upper() 0336 # XXX: shouldn't this code be removed, not commented out? 0337 #cap = 'AUTH=%s' % mech 0338 #if not cap in self.capabilities: # Let the server decide! 0339 # raise self.error("Server doesn't allow %s authentication." % mech) 0340 self.literal = _Authenticator(authobject).process 0341 typ, dat = self._simple_command('AUTHENTICATE', mech) 0342 if typ != 'OK': 0343 raise self.error(dat[-1]) 0344 self.state = 'AUTH' 0345 return typ, dat 0346 0347 0348 def check(self): 0349 """Checkpoint mailbox on server. 0350 0351 (typ, [data]) = <instance>.check() 0352 """ 0353 return self._simple_command('CHECK') 0354 0355 0356 def close(self): 0357 """Close currently selected mailbox. 0358 0359 Deleted messages are removed from writable mailbox. 0360 This is the recommended command before 'LOGOUT'. 0361 0362 (typ, [data]) = <instance>.close() 0363 """ 0364 try: 0365 typ, dat = self._simple_command('CLOSE') 0366 finally: 0367 self.state = 'AUTH' 0368 return typ, dat 0369 0370 0371 def copy(self, message_set, new_mailbox): 0372 """Copy 'message_set' messages onto end of 'new_mailbox'. 0373 0374 (typ, [data]) = <instance>.copy(message_set, new_mailbox) 0375 """ 0376 return self._simple_command('COPY', message_set, new_mailbox) 0377 0378 0379 def create(self, mailbox): 0380 """Create new mailbox. 0381 0382 (typ, [data]) = <instance>.create(mailbox) 0383 """ 0384 return self._simple_command('CREATE', mailbox) 0385 0386 0387 def delete(self, mailbox): 0388 """Delete old mailbox. 0389 0390 (typ, [data]) = <instance>.delete(mailbox) 0391 """ 0392 return self._simple_command('DELETE', mailbox) 0393 0394 def deleteacl(self, mailbox, who): 0395 """Delete the ACLs (remove any rights) set for who on mailbox. 0396 0397 (typ, [data]) = <instance>.deleteacl(mailbox, who) 0398 """ 0399 return self._simple_command('DELETEACL', mailbox, who) 0400 0401 def expunge(self): 0402 """Permanently remove deleted items from selected mailbox. 0403 0404 Generates 'EXPUNGE' response for each deleted message. 0405 0406 (typ, [data]) = <instance>.expunge() 0407 0408 'data' is list of 'EXPUNGE'd message numbers in order received. 0409 """ 0410 name = 'EXPUNGE' 0411 typ, dat = self._simple_command(name) 0412 return self._untagged_response(typ, dat, name) 0413 0414 0415 def fetch(self, message_set, message_parts): 0416 """Fetch (parts of) messages. 0417 0418 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts) 0419 0420 'message_parts' should be a string of selected parts 0421 enclosed in parentheses, eg: "(UID BODY[TEXT])". 0422 0423 'data' are tuples of message part envelope and data. 0424 """ 0425 name = 'FETCH' 0426 typ, dat = self._simple_command(name, message_set, message_parts) 0427 return self._untagged_response(typ, dat, name) 0428 0429 0430 def getacl(self, mailbox): 0431 """Get the ACLs for a mailbox. 0432 0433 (typ, [data]) = <instance>.getacl(mailbox) 0434 """ 0435 typ, dat = self._simple_command('GETACL', mailbox) 0436 return self._untagged_response(typ, dat, 'ACL') 0437 0438 0439 def getquota(self, root): 0440 """Get the quota root's resource usage and limits. 0441 0442 Part of the IMAP4 QUOTA extension defined in rfc2087. 0443 0444 (typ, [data]) = <instance>.getquota(root) 0445 """ 0446 typ, dat = self._simple_command('GETQUOTA', root) 0447 return self._untagged_response(typ, dat, 'QUOTA') 0448 0449 0450 def getquotaroot(self, mailbox): 0451 """Get the list of quota roots for the named mailbox. 0452 0453 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox) 0454 """ 0455 typ, dat = self._simple_command('GETQUOTAROOT', mailbox) 0456 typ, quota = self._untagged_response(typ, dat, 'QUOTA') 0457 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT') 0458 return typ, [quotaroot, quota] 0459 0460 0461 def list(self, directory='""', pattern='*'): 0462 """List mailbox names in directory matching pattern. 0463 0464 (typ, [data]) = <instance>.list(directory='""', pattern='*') 0465 0466 'data' is list of LIST responses. 0467 """ 0468 name = 'LIST' 0469 typ, dat = self._simple_command(name, directory, pattern) 0470 return self._untagged_response(typ, dat, name) 0471 0472 0473 def login(self, user, password): 0474 """Identify client using plaintext password. 0475 0476 (typ, [data]) = <instance>.login(user, password) 0477 0478 NB: 'password' will be quoted. 0479 """ 0480 typ, dat = self._simple_command('LOGIN', user, self._quote(password)) 0481 if typ != 'OK': 0482 raise self.error(dat[-1]) 0483 self.state = 'AUTH' 0484 return typ, dat 0485 0486 0487 def login_cram_md5(self, user, password): 0488 """ Force use of CRAM-MD5 authentication. 0489 0490 (typ, [data]) = <instance>.login_cram_md5(user, password) 0491 """ 0492 self.user, self.password = user, password 0493 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH) 0494 0495 0496 def _CRAM_MD5_AUTH(self, challenge): 0497 """ Authobject to use with CRAM-MD5 authentication. """ 0498 import hmac 0499 return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest() 0500 0501 0502 def logout(self): 0503 """Shutdown connection to server. 0504 0505 (typ, [data]) = <instance>.logout() 0506 0507 Returns server 'BYE' response. 0508 """ 0509 self.state = 'LOGOUT' 0510 try: typ, dat = self._simple_command('LOGOUT') 0511 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] 0512 self.shutdown() 0513 if 'BYE' in self.untagged_responses: 0514 return 'BYE', self.untagged_responses['BYE'] 0515 return typ, dat 0516 0517 0518 def lsub(self, directory='""', pattern='*'): 0519 """List 'subscribed' mailbox names in directory matching pattern. 0520 0521 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*') 0522 0523 'data' are tuples of message part envelope and data. 0524 """ 0525 name = 'LSUB' 0526 typ, dat = self._simple_command(name, directory, pattern) 0527 return self._untagged_response(typ, dat, name) 0528 0529 def myrights(self, mailbox): 0530 """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox). 0531 0532 (typ, [data]) = <instance>.myrights(mailbox) 0533 """ 0534 typ,dat = self._simple_command('MYRIGHTS', mailbox) 0535 return self._untagged_response(typ, dat, 'MYRIGHTS') 0536 0537 def namespace(self): 0538 """ Returns IMAP namespaces ala rfc2342 0539 0540 (typ, [data, ...]) = <instance>.namespace() 0541 """ 0542 name = 'NAMESPACE' 0543 typ, dat = self._simple_command(name) 0544 return self._untagged_response(typ, dat, name) 0545 0546 0547 def noop(self): 0548 """Send NOOP command. 0549 0550 (typ, [data]) = <instance>.noop() 0551 """ 0552 if __debug__: 0553 if self.debug >= 3: 0554 self._dump_ur(self.untagged_responses) 0555 return self._simple_command('NOOP') 0556 0557 0558 def partial(self, message_num, message_part, start, length): 0559 """Fetch truncated part of a message. 0560 0561 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length) 0562 0563 'data' is tuple of message part envelope and data. 0564 """ 0565 name = 'PARTIAL' 0566 typ, dat = self._simple_command(name, message_num, message_part, start, length) 0567 return self._untagged_response(typ, dat, 'FETCH') 0568 0569 0570 def proxyauth(self, user): 0571 """Assume authentication as "user". 0572 0573 Allows an authorised administrator to proxy into any user's 0574 mailbox. 0575 0576 (typ, [data]) = <instance>.proxyauth(user) 0577 """ 0578 0579 name = 'PROXYAUTH' 0580 return self._simple_command('PROXYAUTH', user) 0581 0582 0583 def rename(self, oldmailbox, newmailbox): 0584 """Rename old mailbox name to new. 0585 0586 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox) 0587 """ 0588 return self._simple_command('RENAME', oldmailbox, newmailbox) 0589 0590 0591 def search(self, charset, *criteria): 0592 """Search mailbox for matching messages. 0593 0594 (typ, [data]) = <instance>.search(charset, criterion, ...) 0595 0596 'data' is space separated list of matching message numbers. 0597 """ 0598 name = 'SEARCH' 0599 if charset: 0600 typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria) 0601 else: 0602 typ, dat = self._simple_command(name, *criteria) 0603 return self._untagged_response(typ, dat, name) 0604 0605 0606 def select(self, mailbox='INBOX', readonly=None): 0607 """Select a mailbox. 0608 0609 Flush all untagged responses. 0610 0611 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None) 0612 0613 'data' is count of messages in mailbox ('EXISTS' response). 0614 0615 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so 0616 other responses should be obtained via <instance>.response('FLAGS') etc. 0617 """ 0618 self.untagged_responses = {} # Flush old responses. 0619 self.is_readonly = readonly 0620 if readonly is not None: 0621 name = 'EXAMINE' 0622 else: 0623 name = 'SELECT' 0624 typ, dat = self._simple_command(name, mailbox) 0625 if typ != 'OK': 0626 self.state = 'AUTH' # Might have been 'SELECTED' 0627 return typ, dat 0628 self.state = 'SELECTED' 0629 if 'READ-ONLY' in self.untagged_responses \ 0630 and not readonly: 0631 if __debug__: 0632 if self.debug >= 1: 0633 self._dump_ur(self.untagged_responses) 0634 raise self.readonly('%s is not writable' % mailbox) 0635 return typ, self.untagged_responses.get('EXISTS', [None]) 0636 0637 0638 def setacl(self, mailbox, who, what): 0639 """Set a mailbox acl. 0640 0641 (typ, [data]) = <instance>.setacl(mailbox, who, what) 0642 """ 0643 return self._simple_command('SETACL', mailbox, who, what) 0644 0645 0646 def setquota(self, root, limits): 0647 """Set the quota root's resource limits. 0648 0649 (typ, [data]) = <instance>.setquota(root, limits) 0650 """ 0651 typ, dat = self._simple_command('SETQUOTA', root, limits) 0652 return self._untagged_response(typ, dat, 'QUOTA') 0653 0654 0655 def sort(self, sort_criteria, charset, *search_criteria): 0656 """IMAP4rev1 extension SORT command. 0657 0658 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...) 0659 """ 0660 name = 'SORT' 0661 #if not name in self.capabilities: # Let the server decide! 0662 # raise self.error('unimplemented extension command: %s' % name) 0663 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'): 0664 sort_criteria = '(%s)' % sort_criteria 0665 typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria) 0666 return self._untagged_response(typ, dat, name) 0667 0668 0669 def status(self, mailbox, names): 0670 """Request named status conditions for mailbox. 0671 0672 (typ, [data]) = <instance>.status(mailbox, names) 0673 """ 0674 name = 'STATUS' 0675 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide! 0676 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name) 0677 typ, dat = self._simple_command(name, mailbox, names) 0678 return self._untagged_response(typ, dat, name) 0679 0680 0681 def store(self, message_set, command, flags): 0682 """Alters flag dispositions for messages in mailbox. 0683 0684 (typ, [data]) = <instance>.store(message_set, command, flags) 0685 """ 0686 if (flags[0],flags[-1]) != ('(',')'): 0687 flags = '(%s)' % flags # Avoid quoting the flags 0688 typ, dat = self._simple_command('STORE', message_set, command, flags) 0689 return self._untagged_response(typ, dat, 'FETCH') 0690 0691 0692 def subscribe(self, mailbox): 0693 """Subscribe to new mailbox. 0694 0695 (typ, [data]) = <instance>.subscribe(mailbox) 0696 """ 0697 return self._simple_command('SUBSCRIBE', mailbox) 0698 0699 0700 def thread(self, threading_algorithm, charset, *search_criteria): 0701 """IMAPrev1 extension THREAD command. 0702 0703 (type, [data]) = <instance>.thread(threading_alogrithm, charset, search_criteria, ...) 0704 """ 0705 name = 'THREAD' 0706 typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria) 0707 return self._untagged_response(typ, dat, name) 0708 0709 0710 def uid(self, command, *args): 0711 """Execute "command arg ..." with messages identified by UID, 0712 rather than message number. 0713 0714 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...) 0715 0716 Returns response appropriate to 'command'. 0717 """ 0718 command = command.upper() 0719 if not command in Commands: 0720 raise self.error("Unknown IMAP4 UID command: %s" % command) 0721 if self.state not in Commands[command]: 0722 raise self.error('command %s illegal in state %s' 0723 % (command, self.state)) 0724 name = 'UID' 0725 typ, dat = self._simple_command(name, command, *args) 0726 if command in ('SEARCH', 'SORT'): 0727 name = command 0728 else: 0729 name = 'FETCH' 0730 return self._untagged_response(typ, dat, name) 0731 0732 0733 def unsubscribe(self, mailbox): 0734 """Unsubscribe from old mailbox. 0735 0736 (typ, [data]) = <instance>.unsubscribe(mailbox) 0737 """ 0738 return self._simple_command('UNSUBSCRIBE', mailbox) 0739 0740 0741 def xatom(self, name, *args): 0742 """Allow simple extension commands 0743 notified by server in CAPABILITY response. 0744 0745 Assumes command is legal in current state. 0746 0747 (typ, [data]) = <instance>.xatom(name, arg, ...) 0748 0749 Returns response appropriate to extension command `name'. 0750 """ 0751 name = name.upper() 0752 #if not name in self.capabilities: # Let the server decide! 0753 # raise self.error('unknown extension command: %s' % name) 0754 if not name in Commands: 0755 Commands[name] = (self.state,) 0756 return self._simple_command(name, *args) 0757 0758 0759 0760 # Private methods 0761 0762 0763 def _append_untagged(self, typ, dat): 0764 0765 if dat is None: dat = '' 0766 ur = self.untagged_responses 0767 if __debug__: 0768 if self.debug >= 5: 0769 self._mesg('untagged_responses[%s] %s += ["%s"]' % 0770 (typ, len(ur.get(typ,'')), dat)) 0771 if typ in ur: 0772 ur[typ].append(dat) 0773 else: 0774 ur[typ] = [dat] 0775 0776 0777 def _check_bye(self): 0778 bye = self.untagged_responses.get('BYE') 0779 if bye: 0780 raise self.abort(bye[-1]) 0781 0782 0783 def _command(self, name, *args): 0784 0785 if self.state not in Commands[name]: 0786 self.literal = None 0787 raise self.error( 0788 'command %s illegal in state %s' % (name, self.state)) 0789 0790 for typ in ('OK', 'NO', 'BAD'): 0791 if typ in self.untagged_responses: 0792 del self.untagged_responses[typ] 0793 0794 if 'READ-ONLY' in self.untagged_responses \ 0795 and not self.is_readonly: 0796 raise self.readonly('mailbox status changed to READ-ONLY') 0797 0798 tag = self._new_tag() 0799 data = '%s %s' % (tag, name) 0800 for arg in args: 0801 if arg is None: continue 0802 data = '%s %s' % (data, self._checkquote(arg)) 0803 0804 literal = self.literal 0805 if literal is not None: 0806 self.literal = None 0807 if type(literal) is type(self._command): 0808 literator = literal 0809 else: 0810 literator = None 0811 data = '%s {%s}' % (data, len(literal)) 0812 0813 if __debug__: 0814 if self.debug >= 4: 0815 self._mesg('> %s' % data) 0816 else: 0817 self._log('> %s' % data) 0818 0819 try: 0820 self.send('%s%s' % (data, CRLF)) 0821 except (socket.error, OSError), val: 0822 raise self.abort('socket error: %s' % val) 0823 0824 if literal is None: 0825 return tag 0826 0827 while 1: 0828 # Wait for continuation response 0829 0830 while self._get_response(): 0831 if self.tagged_commands[tag]: # BAD/NO? 0832 return tag 0833 0834 # Send literal 0835 0836 if literator: 0837 literal = literator(self.continuation_response) 0838 0839 if __debug__: 0840 if self.debug >= 4: 0841 self._mesg('write literal size %s' % len(literal)) 0842 0843 try: 0844 self.send(literal) 0845 self.send(CRLF) 0846 except (socket.error, OSError), val: 0847 raise self.abort('socket error: %s' % val) 0848 0849 if not literator: 0850 break 0851 0852 return tag 0853 0854 0855 def _command_complete(self, name, tag): 0856 self._check_bye() 0857 try: 0858 typ, data = self._get_tagged_response(tag) 0859 except self.abort, val: 0860 raise self.abort('command: %s => %s' % (name, val)) 0861 except self.error, val: 0862 raise self.error('command: %s => %s' % (name, val)) 0863 self._check_bye() 0864 if typ == 'BAD': 0865 raise self.error('%s command error: %s %s' % (name, typ, data)) 0866 return typ, data 0867 0868 0869 def _get_response(self): 0870 0871 # Read response and store. 0872 # 0873 # Returns None for continuation responses, 0874 # otherwise first response line received. 0875 0876 resp = self._get_line() 0877 0878 # Command completion response? 0879 0880 if self._match(self.tagre, resp): 0881 tag = self.mo.group('tag') 0882 if not tag in self.tagged_commands: 0883 raise self.abort('unexpected tagged response: %s' % resp) 0884 0885 typ = self.mo.group('type') 0886 dat = self.mo.group('data') 0887 self.tagged_commands[tag] = (typ, [dat]) 0888 else: 0889 dat2 = None 0890 0891 # '*' (untagged) responses? 0892 0893 if not self._match(Untagged_response, resp): 0894 if self._match(Untagged_status, resp): 0895 dat2 = self.mo.group('data2') 0896 0897 if self.mo is None: 0898 # Only other possibility is '+' (continuation) response... 0899 0900 if self._match(Continuation, resp): 0901 self.continuation_response = self.mo.group('data') 0902 return None # NB: indicates continuation 0903 0904 raise self.abort("unexpected response: '%s'" % resp) 0905 0906 typ = self.mo.group('type') 0907 dat = self.mo.group('data') 0908 if dat is None: dat = '' # Null untagged response 0909 if dat2: dat = dat + ' ' + dat2 0910 0911 # Is there a literal to come? 0912 0913 while self._match(Literal, dat): 0914 0915 # Read literal direct from connection. 0916 0917 size = int(self.mo.group('size')) 0918 if __debug__: 0919 if self.debug >= 4: 0920 self._mesg('read literal size %s' % size) 0921 data = self.read(size) 0922 0923 # Store response with literal as tuple 0924 0925 self._append_untagged(typ, (dat, data)) 0926 0927 # Read trailer - possibly containing another literal 0928 0929 dat = self._get_line() 0930 0931 self._append_untagged(typ, dat) 0932 0933 # Bracketed response information? 0934 0935 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat): 0936 self._append_untagged(self.mo.group('type'), self.mo.group('data')) 0937 0938 if __debug__: 0939 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'): 0940 self._mesg('%s response: %s' % (typ, dat)) 0941 0942 return resp 0943 0944 0945 def _get_tagged_response(self, tag): 0946 0947 while 1: 0948 result = self.tagged_commands[tag] 0949 if result is not None: 0950 del self.tagged_commands[tag] 0951 return result 0952 0953 # Some have reported "unexpected response" exceptions. 0954 # Note that ignoring them here causes loops. 0955 # Instead, send me details of the unexpected response and 0956 # I'll update the code in `_get_response()'. 0957 0958 try: 0959 self._get_response() 0960 except self.abort, val: 0961 if __debug__: 0962 if self.debug >= 1: 0963 self.print_log() 0964 raise 0965 0966 0967 def _get_line(self): 0968 0969 line = self.readline() 0970 if not line: 0971 raise self.abort('socket error: EOF') 0972 0973 # Protocol mandates all lines terminated by CRLF 0974 0975 line = line[:-2] 0976 if __debug__: 0977 if self.debug >= 4: 0978 self._mesg('< %s' % line) 0979 else: 0980 self._log('< %s' % line) 0981 return line 0982 0983 0984 def _match(self, cre, s): 0985 0986 # Run compiled regular expression match method on 's'. 0987 # Save result, return success. 0988 0989 self.mo = cre.match(s) 0990 if __debug__: 0991 if self.mo is not None and self.debug >= 5: 0992 self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups())) 0993 return self.mo is not None 0994 0995 0996 def _new_tag(self): 0997 0998 tag = '%s%s' % (self.tagpre, self.tagnum) 0999 self.tagnum = self.tagnum + 1 1000 self.tagged_commands[tag] = None 1001 return tag 1002 1003 1004 def _checkquote(self, arg): 1005 1006 # Must quote command args if non-alphanumeric chars present, 1007 # and not already quoted. 1008 1009 if type(arg) is not type(''): 1010 return arg 1011 if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')): 1012 return arg 1013 if arg and self.mustquote.search(arg) is None: 1014 return arg 1015 return self._quote(arg) 1016 1017 1018 def _quote(self, arg): 1019 1020 arg = arg.replace('\\', '\\\\') 1021 arg = arg.replace('"', '\\"') 1022 1023 return '"%s"' % arg 1024 1025 1026 def _simple_command(self, name, *args): 1027 1028 return self._command_complete(name, self._command(name, *args)) 1029 1030 1031 def _untagged_response(self, typ, dat, name): 1032 1033 if typ == 'NO': 1034 return typ, dat 1035 if not name in self.untagged_responses: 1036 return typ, [None] 1037 data = self.untagged_responses.pop(name) 1038 if __debug__: 1039 if self.debug >= 5: 1040 self._mesg('untagged_responses[%s] => %s' % (name, data)) 1041 return typ, data 1042 1043 1044 if __debug__: 1045 1046 def _mesg(self, s, secs=None): 1047 if secs is None: 1048 secs = time.time() 1049 tm = time.strftime('%M:%S', time.localtime(secs)) 1050 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s)) 1051 sys.stderr.flush() 1052 1053 def _dump_ur(self, dict): 1054 # Dump untagged responses (in `dict'). 1055 l = dict.items() 1056 if not l: return 1057 t = '\n\t\t' 1058 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l) 1059 self._mesg('untagged responses dump:%s%s' % (t, t.join(l))) 1060 1061 def _log(self, line): 1062 # Keep log of last `_cmd_log_len' interactions for debugging. 1063 self._cmd_log[self._cmd_log_idx] = (line, time.time()) 1064 self._cmd_log_idx += 1 1065 if self._cmd_log_idx >= self._cmd_log_len: 1066 self._cmd_log_idx = 0 1067 1068 def print_log(self): 1069 self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log)) 1070 i, n = self._cmd_log_idx, self._cmd_log_len 1071 while n: 1072 try: 1073 self._mesg(*self._cmd_log[i]) 1074 except: 1075 pass 1076 i += 1 1077 if i >= self._cmd_log_len: 1078 i = 0 1079 n -= 1 1080 1081 1082 1083 class IMAP4_SSL(IMAP4): 1084 1085 """IMAP4 client class over SSL connection 1086 1087 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]]) 1088 1089 host - host's name (default: localhost); 1090 port - port number (default: standard IMAP4 SSL port). 1091 keyfile - PEM formatted file that contains your private key (default: None); 1092 certfile - PEM formatted certificate chain file (default: None); 1093 1094 for more documentation see the docstring of the parent class IMAP4. 1095 """ 1096 1097 1098 def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None): 1099 self.keyfile = keyfile 1100 self.certfile = certfile 1101 IMAP4.__init__(self, host, port) 1102 1103 1104 def open(self, host = '', port = IMAP4_SSL_PORT): 1105 """Setup connection to remote server on "host:port". 1106 (default: localhost:standard IMAP4 SSL port). 1107 This connection will be used by the routines: 1108 read, readline, send, shutdown. 1109 """ 1110 self.host = host 1111 self.port = port 1112 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1113 self.sock.connect((host, port)) 1114 self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile) 1115 1116 1117 def read(self, size): 1118 """Read 'size' bytes from remote.""" 1119 # sslobj.read() sometimes returns < size bytes 1120 chunks = [] 1121 read = 0 1122 while read < size: 1123 data = self.sslobj.read(size-read) 1124 read += len(data) 1125 chunks.append(data) 1126 1127 return ''.join(chunks) 1128 1129 1130 def readline(self): 1131 """Read line from remote.""" 1132 # NB: socket.ssl needs a "readline" method, or perhaps a "makefile" method. 1133 line = [] 1134 while 1: 1135 char = self.sslobj.read(1) 1136 line.append(char) 1137 if char == "\n": return ''.join(line) 1138 1139 1140 def send(self, data): 1141 """Send data to remote.""" 1142 # NB: socket.ssl needs a "sendall" method to match socket objects. 1143 bytes = len(data) 1144 while bytes > 0: 1145 sent = self.sslobj.write(data) 1146 if sent == bytes: 1147 break # avoid copy 1148 data = data[sent:] 1149 bytes = bytes - sent 1150 1151 1152 def shutdown(self): 1153 """Close I/O established in "open".""" 1154 self.sock.close() 1155 1156 1157 def socket(self): 1158 """Return socket instance used to connect to IMAP4 server. 1159 1160 socket = <instance>.socket() 1161 """ 1162 return self.sock 1163 1164 1165 def ssl(self): 1166 """Return SSLObject instance used to communicate with the IMAP4 server. 1167 1168 ssl = <instance>.socket.ssl() 1169 """ 1170 return self.sslobj 1171 1172 1173 1174 class IMAP4_stream(IMAP4): 1175 1176 """IMAP4 client class over a stream 1177 1178 Instantiate with: IMAP4_stream(command) 1179 1180 where "command" is a string that can be passed to os.popen2() 1181 1182 for more documentation see the docstring of the parent class IMAP4. 1183 """ 1184 1185 1186 def __init__(self, command): 1187 self.command = command 1188 IMAP4.__init__(self) 1189 1190 1191 def open(self, host = None, port = None): 1192 """Setup a stream connection. 1193 This connection will be used by the routines: 1194 read, readline, send, shutdown. 1195 """ 1196 self.host = None # For compatibility with parent class 1197 self.port = None 1198 self.sock = None 1199 self.file = None 1200 self.writefile, self.readfile = os.popen2(self.command) 1201 1202 1203 def read(self, size): 1204 """Read 'size' bytes from remote.""" 1205 return self.readfile.read(size) 1206 1207 1208 def readline(self): 1209 """Read line from remote.""" 1210 return self.readfile.readline() 1211 1212 1213 def send(self, data): 1214 """Send data to remote.""" 1215 self.writefile.write(data) 1216 self.writefile.flush() 1217 1218 1219 def shutdown(self): 1220 """Close I/O established in "open".""" 1221 self.readfile.close() 1222 self.writefile.close() 1223 1224 1225 1226 class _Authenticator: 1227 1228 """Private class to provide en/decoding 1229 for base64-based authentication conversation. 1230 """ 1231 1232 def __init__(self, mechinst): 1233 self.mech = mechinst # Callable object to provide/process data 1234 1235 def process(self, data): 1236 ret = self.mech(self.decode(data)) 1237 if ret is None: 1238 return '*' # Abort conversation 1239 return self.encode(ret) 1240 1241 def encode(self, inp): 1242 # 1243 # Invoke binascii.b2a_base64 iteratively with 1244 # short even length buffers, strip the trailing 1245 # line feed from the result and append. "Even" 1246 # means a number that factors to both 6 and 8, 1247 # so when it gets to the end of the 8-bit input 1248 # there's no partial 6-bit output. 1249 # 1250 oup = '' 1251 while inp: 1252 if len(inp) > 48: 1253 t = inp[:48] 1254 inp = inp[48:] 1255 else: 1256 t = inp 1257 inp = '' 1258 e = binascii.b2a_base64(t) 1259 if e: 1260 oup = oup + e[:-1] 1261 return oup 1262 1263 def decode(self, inp): 1264 if not inp: 1265 return '' 1266 return binascii.a2b_base64(inp) 1267 1268 1269 1270 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 1271 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} 1272 1273 def Internaldate2tuple(resp): 1274 """Convert IMAP4 INTERNALDATE to UT. 1275 1276 Returns Python time module tuple. 1277 """ 1278 1279 mo = InternalDate.match(resp) 1280 if not mo: 1281 return None 1282 1283 mon = Mon2num[mo.group('mon')] 1284 zonen = mo.group('zonen') 1285 1286 day = int(mo.group('day')) 1287 year = int(mo.group('year')) 1288 hour = int(mo.group('hour')) 1289 min = int(mo.group('min')) 1290 sec = int(mo.group('sec')) 1291 zoneh = int(mo.group('zoneh')) 1292 zonem = int(mo.group('zonem')) 1293 1294 # INTERNALDATE timezone must be subtracted to get UT 1295 1296 zone = (zoneh*60 + zonem)*60 1297 if zonen == '-': 1298 zone = -zone 1299 1300 tt = (year, mon, day, hour, min, sec, -1, -1, -1) 1301 1302 utc = time.mktime(tt) 1303 1304 # Following is necessary because the time module has no 'mkgmtime'. 1305 # 'mktime' assumes arg in local timezone, so adds timezone/altzone. 1306 1307 lt = time.localtime(utc) 1308 if time.daylight and lt[-1]: 1309 zone = zone + time.altzone 1310 else: 1311 zone = zone + time.timezone 1312 1313 return time.localtime(utc - zone) 1314 1315 1316 1317 def Int2AP(num): 1318 1319 """Convert integer to A-P string representation.""" 1320 1321 val = ''; AP = 'ABCDEFGHIJKLMNOP' 1322 num = int(abs(num)) 1323 while num: 1324 num, mod = divmod(num, 16) 1325 val = AP[mod] + val 1326 return val 1327 1328 1329 1330 def ParseFlags(resp): 1331 1332 """Convert IMAP4 flags response to python tuple.""" 1333 1334 mo = Flags.match(resp) 1335 if not mo: 1336 return () 1337 1338 return tuple(mo.group('flags').split()) 1339 1340 1341 def Time2Internaldate(date_time): 1342 1343 """Convert 'date_time' to IMAP4 INTERNALDATE representation. 1344 1345 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"' 1346 """ 1347 1348 if isinstance(date_time, (int, float)): 1349 tt = time.localtime(date_time) 1350 elif isinstance(date_time, (tuple, time.struct_time)): 1351 tt = date_time 1352 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'): 1353 return date_time # Assume in correct format 1354 else: 1355 raise ValueError("date_time not of a known type") 1356 1357 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt) 1358 if dt[0] == '0': 1359 dt = ' ' + dt[1:] 1360 if time.daylight and tt[-1]: 1361 zone = -time.altzone 1362 else: 1363 zone = -time.timezone 1364 return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"' 1365 1366 1367 1368 if __name__ == '__main__': 1369 1370 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]' 1371 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"' 1372 # to test the IMAP4_stream class 1373 1374 import getopt, getpass 1375 1376 try: 1377 optlist, args = getopt.getopt(sys.argv[1:], 'd:s:') 1378 except getopt.error, val: 1379 optlist, args = (), () 1380 1381 stream_command = None 1382 for opt,val in optlist: 1383 if opt == '-d': 1384 Debug = int(val) 1385 elif opt == '-s': 1386 stream_command = val 1387 if not args: args = (stream_command,) 1388 1389 if not args: args = ('',) 1390 1391 host = args[0] 1392 1393 USER = getpass.getuser() 1394 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost")) 1395 1396 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'} 1397 test_seq1 = ( 1398 ('login', (USER, PASSWD)), 1399 ('create', ('/tmp/xxx 1',)), 1400 ('rename', ('/tmp/xxx 1', '/tmp/yyy')), 1401 ('CREATE', ('/tmp/yyz 2',)), 1402 ('append', ('/tmp/yyz 2', None, None, test_mesg)), 1403 ('list', ('/tmp', 'yy*')), 1404 ('select', ('/tmp/yyz 2',)), 1405 ('search', (None, 'SUBJECT', 'test')), 1406 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')), 1407 ('store', ('1', 'FLAGS', '(\Deleted)')), 1408 ('namespace', ()), 1409 ('expunge', ()), 1410 ('recent', ()), 1411 ('close', ()), 1412 ) 1413 1414 test_seq2 = ( 1415 ('select', ()), 1416 ('response',('UIDVALIDITY',)), 1417 ('uid', ('SEARCH', 'ALL')), 1418 ('response', ('EXISTS',)), 1419 ('append', (None, None, None, test_mesg)), 1420 ('recent', ()), 1421 ('logout', ()), 1422 ) 1423 1424 def run(cmd, args): 1425 M._mesg('%s %s' % (cmd, args)) 1426 typ, dat = getattr(M, cmd)(*args) 1427 M._mesg('%s => %s %s' % (cmd, typ, dat)) 1428 if typ == 'NO': raise dat[0] 1429 return dat 1430 1431 try: 1432 if stream_command: 1433 M = IMAP4_stream(stream_command) 1434 else: 1435 M = IMAP4(host) 1436 if M.state == 'AUTH': 1437 test_seq1 = test_seq1[1:] # Login not needed 1438 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION) 1439 M._mesg('CAPABILITIES = %r' % (M.capabilities,)) 1440 1441 for cmd,args in test_seq1: 1442 run(cmd, args) 1443 1444 for ml in run('list', ('/tmp/', 'yy%')): 1445 mo = re.match(r'.*"([^"]+)"$', ml) 1446 if mo: path = mo.group(1) 1447 else: path = ml.split()[-1] 1448 run('delete', (path,)) 1449 1450 for cmd,args in test_seq2: 1451 dat = run(cmd, args) 1452 1453 if (cmd,args) != ('uid', ('SEARCH', 'ALL')): 1454 continue 1455 1456 uid = dat[-1].split() 1457 if not uid: continue 1458 run('uid', ('FETCH', '%s' % uid[-1], 1459 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)')) 1460 1461 print '\nAll tests OK.' 1462 1463 except: 1464 print '\nTests failed.' 1465 1466 if not Debug: 1467 print ''' 1468 If you would like to see debugging output, 1469 try: %s -d5 1470 ''' % sys.argv[0] 1471 1472 raise 1473
Generated by PyXR 0.9.4