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