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