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