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