source: titan/mediathek/localhoster/lib/python2.7/imaplib.py @ 41267

Last change on this file since 41267 was 40094, checked in by obi, 7 years ago

tithek add yoztube-dl support

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