7
Public functions: Internaldate2tuple
13
# Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
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.
25
import binascii, random, re, socket, subprocess, sys, time
27
__all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple",
28
"Int2AP", "ParseFlags", "Time2Internaldate"]
36
AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
42
'APPEND': ('AUTH', 'SELECTED'),
43
'AUTHENTICATE': ('NONAUTH',),
44
'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
45
'CHECK': ('SELECTED',),
46
'CLOSE': ('SELECTED',),
47
'COPY': ('SELECTED',),
48
'CREATE': ('AUTH', 'SELECTED'),
49
'DELETE': ('AUTH', 'SELECTED'),
50
'DELETEACL': ('AUTH', 'SELECTED'),
51
'EXAMINE': ('AUTH', 'SELECTED'),
52
'EXPUNGE': ('SELECTED',),
53
'FETCH': ('SELECTED',),
54
'GETACL': ('AUTH', 'SELECTED'),
55
'GETANNOTATION':('AUTH', 'SELECTED'),
56
'GETQUOTA': ('AUTH', 'SELECTED'),
57
'GETQUOTAROOT': ('AUTH', 'SELECTED'),
58
'MYRIGHTS': ('AUTH', 'SELECTED'),
59
'LIST': ('AUTH', 'SELECTED'),
60
'LOGIN': ('NONAUTH',),
61
'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
62
'LSUB': ('AUTH', 'SELECTED'),
63
'NAMESPACE': ('AUTH', 'SELECTED'),
64
'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
65
'PARTIAL': ('SELECTED',), # NB: obsolete
66
'PROXYAUTH': ('AUTH',),
67
'RENAME': ('AUTH', 'SELECTED'),
68
'SEARCH': ('SELECTED',),
69
'SELECT': ('AUTH', 'SELECTED'),
70
'SETACL': ('AUTH', 'SELECTED'),
71
'SETANNOTATION':('AUTH', 'SELECTED'),
72
'SETQUOTA': ('AUTH', 'SELECTED'),
73
'SORT': ('SELECTED',),
74
'STATUS': ('AUTH', 'SELECTED'),
75
'STORE': ('SELECTED',),
76
'SUBSCRIBE': ('AUTH', 'SELECTED'),
77
'THREAD': ('SELECTED',),
79
'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
82
# Patterns to match server responses
84
Continuation = re.compile(br'\+( (?P<data>.*))?')
85
Flags = re.compile(br'.*FLAGS \((?P<flags>[^\)]*)\)')
86
InternalDate = re.compile(br'.*INTERNALDATE "'
87
br'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
88
br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
89
br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
91
Literal = re.compile(br'.*{(?P<size>\d+)}$', re.ASCII)
92
MapCRLF = re.compile(br'\r\n|\r|\n')
93
Response_code = re.compile(br'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
94
Untagged_response = re.compile(br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
95
Untagged_status = re.compile(
96
br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?', re.ASCII)
102
"""IMAP4 client class.
104
Instantiate with: IMAP4([host[, port]])
106
host - host's name (default: localhost);
107
port - port number (default: standard IMAP4 port).
109
All IMAP4rev1 commands are supported by methods of the same
110
name (in lower-case).
112
All arguments to commands are converted to strings, except for
113
AUTHENTICATE, and the last argument to APPEND which is passed as
114
an IMAP4 literal. If necessary (the string contains any
115
non-printing characters or white-space and isn't enclosed with
116
either parentheses or double quotes) each string is quoted.
117
However, the 'password' argument to the LOGIN command is always
118
quoted. If you want to avoid having an argument string quoted
119
(eg: the 'flags' argument to STORE) then enclose the string in
120
parentheses (eg: "(\Deleted)").
122
Each command returns a tuple: (type, [data, ...]) where 'type'
123
is usually 'OK' or 'NO', and 'data' is either the text from the
124
tagged response, or untagged results from command. Each 'data'
125
is either a string, or a tuple. If a tuple, then the first part
126
is the header of the response, and the second part contains
127
the data (ie: 'literal' value).
129
Errors raise the exception class <instance>.error("<reason>").
130
IMAP4 server errors raise <instance>.abort("<reason>"),
131
which is a sub-class of 'error'. Mailbox status changes
132
from READ-WRITE to READ-ONLY raise the exception class
133
<instance>.readonly("<reason>"), which is a sub-class of 'abort'.
135
"error" exceptions imply a program error.
136
"abort" exceptions imply the connection should be reset, and
137
the command re-tried.
138
"readonly" exceptions imply the command should be re-tried.
140
Note: to use this module, you must read the RFCs pertaining to the
141
IMAP4 protocol, as the semantics of the arguments to each IMAP4
142
command are left to the invoker, not to mention the results. Also,
143
most IMAP servers implement a sub-set of the commands available here.
146
class error(Exception): pass # Logical errors - debug required
147
class abort(error): pass # Service errors - close and retry
148
class readonly(abort): pass # Mailbox status changed to READ-ONLY
150
mustquote = re.compile(br"[^\w!#$%&'*+,.:;<=>?^`|~-]", re.ASCII)
152
def __init__(self, host = '', port = IMAP4_PORT):
154
self.state = 'LOGOUT'
155
self.literal = None # A literal argument to a command
156
self.tagged_commands = {} # Tagged commands awaiting response
157
self.untagged_responses = {} # {typ: [data, ...], ...}
158
self.continuation_response = '' # Last continuation response
159
self.is_readonly = False # READ-ONLY desired state
162
# Open socket to server.
164
self.open(host, port)
166
# Create unique tag for this session,
167
# and compile tagged response matcher.
169
self.tagpre = Int2AP(random.randint(4096, 65535))
170
self.tagre = re.compile(br'(?P<tag>'
172
+ br'\d+) (?P<type>[A-Z]+) (?P<data>.*)', re.ASCII)
174
# Get server welcome message,
175
# request and store CAPABILITY response.
178
self._cmd_log_len = 10
179
self._cmd_log_idx = 0
180
self._cmd_log = {} # Last `_cmd_log_len' interactions
182
self._mesg('imaplib version %s' % __version__)
183
self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
185
self.welcome = self._get_response()
186
if 'PREAUTH' in self.untagged_responses:
188
elif 'OK' in self.untagged_responses:
189
self.state = 'NONAUTH'
191
raise self.error(self.welcome)
193
typ, dat = self.capability()
195
raise self.error('no CAPABILITY response from server')
196
dat = str(dat[-1], "ASCII")
198
self.capabilities = tuple(dat.split())
202
self._mesg('CAPABILITIES: %r' % (self.capabilities,))
204
for version in AllowedVersions:
205
if not version in self.capabilities:
207
self.PROTOCOL_VERSION = version
210
raise self.error('server not IMAP4 compliant')
213
def __getattr__(self, attr):
214
# Allow UPPERCASE variants of IMAP4 command methods.
216
return getattr(self, attr.lower())
217
raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
221
# Overridable methods
224
def _create_socket(self):
225
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
226
sock.connect((self.host, self.port))
229
def open(self, host = '', port = IMAP4_PORT):
230
"""Setup connection to remote server on "host:port"
231
(default: localhost:standard IMAP4 port).
232
This connection will be used by the routines:
233
read, readline, send, shutdown.
237
self.sock = self._create_socket()
238
self.file = self.sock.makefile('rb')
241
def read(self, size):
242
"""Read 'size' bytes from remote."""
246
data = self.file.read(min(size-read, 4096))
251
return b''.join(chunks)
255
"""Read line from remote."""
256
return self.file.readline()
259
def send(self, data):
260
"""Send data to remote."""
261
self.sock.sendall(data)
265
"""Close I/O established in "open"."""
271
"""Return socket instance used to connect to IMAP4 server.
273
socket = <instance>.socket()
283
"""Return most recent 'RECENT' responses if any exist,
284
else prompt server for an update using the 'NOOP' command.
286
(typ, [data]) = <instance>.recent()
288
'data' is None if no new messages,
289
else list of RECENT responses, most recent last.
292
typ, dat = self._untagged_response('OK', [None], name)
295
typ, dat = self.noop() # Prod server for response
296
return self._untagged_response(typ, dat, name)
299
def response(self, code):
300
"""Return data for response 'code' if received, or None.
302
Old value for response 'code' is cleared.
304
(code, [data]) = <instance>.response(code)
306
return self._untagged_response(code, [None], code.upper())
313
def append(self, mailbox, flags, date_time, message):
314
"""Append message to named mailbox.
316
(typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
318
All args except `message' can be None.
324
if (flags[0],flags[-1]) != ('(',')'):
325
flags = '(%s)' % flags
329
date_time = Time2Internaldate(date_time)
332
self.literal = MapCRLF.sub(CRLF, message)
333
return self._simple_command(name, mailbox, flags, date_time)
336
def authenticate(self, mechanism, authobject):
337
"""Authenticate command - requires response processing.
339
'mechanism' specifies which authentication mechanism is to
340
be used - it must appear in <instance>.capabilities in the
341
form AUTH=<mechanism>.
343
'authobject' must be a callable object:
345
data = authobject(response)
347
It will be called to process server continuation responses.
348
It should return data that will be encoded and sent to server.
349
It should return None if the client abort response '*' should
352
mech = mechanism.upper()
353
# XXX: shouldn't this code be removed, not commented out?
354
#cap = 'AUTH=%s' % mech
355
#if not cap in self.capabilities: # Let the server decide!
356
# raise self.error("Server doesn't allow %s authentication." % mech)
357
self.literal = _Authenticator(authobject).process
358
typ, dat = self._simple_command('AUTHENTICATE', mech)
360
raise self.error(dat[-1])
365
def capability(self):
366
"""(typ, [data]) = <instance>.capability()
367
Fetch capabilities list from server."""
370
typ, dat = self._simple_command(name)
371
return self._untagged_response(typ, dat, name)
375
"""Checkpoint mailbox on server.
377
(typ, [data]) = <instance>.check()
379
return self._simple_command('CHECK')
383
"""Close currently selected mailbox.
385
Deleted messages are removed from writable mailbox.
386
This is the recommended command before 'LOGOUT'.
388
(typ, [data]) = <instance>.close()
391
typ, dat = self._simple_command('CLOSE')
397
def copy(self, message_set, new_mailbox):
398
"""Copy 'message_set' messages onto end of 'new_mailbox'.
400
(typ, [data]) = <instance>.copy(message_set, new_mailbox)
402
return self._simple_command('COPY', message_set, new_mailbox)
405
def create(self, mailbox):
406
"""Create new mailbox.
408
(typ, [data]) = <instance>.create(mailbox)
410
return self._simple_command('CREATE', mailbox)
413
def delete(self, mailbox):
414
"""Delete old mailbox.
416
(typ, [data]) = <instance>.delete(mailbox)
418
return self._simple_command('DELETE', mailbox)
420
def deleteacl(self, mailbox, who):
421
"""Delete the ACLs (remove any rights) set for who on mailbox.
423
(typ, [data]) = <instance>.deleteacl(mailbox, who)
425
return self._simple_command('DELETEACL', mailbox, who)
428
"""Permanently remove deleted items from selected mailbox.
430
Generates 'EXPUNGE' response for each deleted message.
432
(typ, [data]) = <instance>.expunge()
434
'data' is list of 'EXPUNGE'd message numbers in order received.
437
typ, dat = self._simple_command(name)
438
return self._untagged_response(typ, dat, name)
441
def fetch(self, message_set, message_parts):
442
"""Fetch (parts of) messages.
444
(typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
446
'message_parts' should be a string of selected parts
447
enclosed in parentheses, eg: "(UID BODY[TEXT])".
449
'data' are tuples of message part envelope and data.
452
typ, dat = self._simple_command(name, message_set, message_parts)
453
return self._untagged_response(typ, dat, name)
456
def getacl(self, mailbox):
457
"""Get the ACLs for a mailbox.
459
(typ, [data]) = <instance>.getacl(mailbox)
461
typ, dat = self._simple_command('GETACL', mailbox)
462
return self._untagged_response(typ, dat, 'ACL')
465
def getannotation(self, mailbox, entry, attribute):
466
"""(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
467
Retrieve ANNOTATIONs."""
469
typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
470
return self._untagged_response(typ, dat, 'ANNOTATION')
473
def getquota(self, root):
474
"""Get the quota root's resource usage and limits.
476
Part of the IMAP4 QUOTA extension defined in rfc2087.
478
(typ, [data]) = <instance>.getquota(root)
480
typ, dat = self._simple_command('GETQUOTA', root)
481
return self._untagged_response(typ, dat, 'QUOTA')
484
def getquotaroot(self, mailbox):
485
"""Get the list of quota roots for the named mailbox.
487
(typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
489
typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
490
typ, quota = self._untagged_response(typ, dat, 'QUOTA')
491
typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
492
return typ, [quotaroot, quota]
495
def list(self, directory='""', pattern='*'):
496
"""List mailbox names in directory matching pattern.
498
(typ, [data]) = <instance>.list(directory='""', pattern='*')
500
'data' is list of LIST responses.
503
typ, dat = self._simple_command(name, directory, pattern)
504
return self._untagged_response(typ, dat, name)
507
def login(self, user, password):
508
"""Identify client using plaintext password.
510
(typ, [data]) = <instance>.login(user, password)
512
NB: 'password' will be quoted.
514
typ, dat = self._simple_command('LOGIN', user, self._quote(password))
516
raise self.error(dat[-1])
521
def login_cram_md5(self, user, password):
522
""" Force use of CRAM-MD5 authentication.
524
(typ, [data]) = <instance>.login_cram_md5(user, password)
526
self.user, self.password = user, password
527
return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
530
def _CRAM_MD5_AUTH(self, challenge):
531
""" Authobject to use with CRAM-MD5 authentication. """
533
return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
537
"""Shutdown connection to server.
539
(typ, [data]) = <instance>.logout()
541
Returns server 'BYE' response.
543
self.state = 'LOGOUT'
544
try: typ, dat = self._simple_command('LOGOUT')
545
except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
547
if 'BYE' in self.untagged_responses:
548
return 'BYE', self.untagged_responses['BYE']
552
def lsub(self, directory='""', pattern='*'):
553
"""List 'subscribed' mailbox names in directory matching pattern.
555
(typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
557
'data' are tuples of message part envelope and data.
560
typ, dat = self._simple_command(name, directory, pattern)
561
return self._untagged_response(typ, dat, name)
563
def myrights(self, mailbox):
564
"""Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
566
(typ, [data]) = <instance>.myrights(mailbox)
568
typ,dat = self._simple_command('MYRIGHTS', mailbox)
569
return self._untagged_response(typ, dat, 'MYRIGHTS')
572
""" Returns IMAP namespaces ala rfc2342
574
(typ, [data, ...]) = <instance>.namespace()
577
typ, dat = self._simple_command(name)
578
return self._untagged_response(typ, dat, name)
582
"""Send NOOP command.
584
(typ, [data]) = <instance>.noop()
588
self._dump_ur(self.untagged_responses)
589
return self._simple_command('NOOP')
592
def partial(self, message_num, message_part, start, length):
593
"""Fetch truncated part of a message.
595
(typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
597
'data' is tuple of message part envelope and data.
600
typ, dat = self._simple_command(name, message_num, message_part, start, length)
601
return self._untagged_response(typ, dat, 'FETCH')
604
def proxyauth(self, user):
605
"""Assume authentication as "user".
607
Allows an authorised administrator to proxy into any user's
610
(typ, [data]) = <instance>.proxyauth(user)
614
return self._simple_command('PROXYAUTH', user)
617
def rename(self, oldmailbox, newmailbox):
618
"""Rename old mailbox name to new.
620
(typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
622
return self._simple_command('RENAME', oldmailbox, newmailbox)
625
def search(self, charset, *criteria):
626
"""Search mailbox for matching messages.
628
(typ, [data]) = <instance>.search(charset, criterion, ...)
630
'data' is space separated list of matching message numbers.
634
typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
636
typ, dat = self._simple_command(name, *criteria)
637
return self._untagged_response(typ, dat, name)
640
def select(self, mailbox='INBOX', readonly=False):
643
Flush all untagged responses.
645
(typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
647
'data' is count of messages in mailbox ('EXISTS' response).
649
Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
650
other responses should be obtained via <instance>.response('FLAGS') etc.
652
self.untagged_responses = {} # Flush old responses.
653
self.is_readonly = readonly
658
typ, dat = self._simple_command(name, mailbox)
660
self.state = 'AUTH' # Might have been 'SELECTED'
662
self.state = 'SELECTED'
663
if 'READ-ONLY' in self.untagged_responses \
667
self._dump_ur(self.untagged_responses)
668
raise self.readonly('%s is not writable' % mailbox)
669
return typ, self.untagged_responses.get('EXISTS', [None])
672
def setacl(self, mailbox, who, what):
673
"""Set a mailbox acl.
675
(typ, [data]) = <instance>.setacl(mailbox, who, what)
677
return self._simple_command('SETACL', mailbox, who, what)
680
def setannotation(self, *args):
681
"""(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
684
typ, dat = self._simple_command('SETANNOTATION', *args)
685
return self._untagged_response(typ, dat, 'ANNOTATION')
688
def setquota(self, root, limits):
689
"""Set the quota root's resource limits.
691
(typ, [data]) = <instance>.setquota(root, limits)
693
typ, dat = self._simple_command('SETQUOTA', root, limits)
694
return self._untagged_response(typ, dat, 'QUOTA')
697
def sort(self, sort_criteria, charset, *search_criteria):
698
"""IMAP4rev1 extension SORT command.
700
(typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
703
#if not name in self.capabilities: # Let the server decide!
704
# raise self.error('unimplemented extension command: %s' % name)
705
if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
706
sort_criteria = '(%s)' % sort_criteria
707
typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
708
return self._untagged_response(typ, dat, name)
711
def status(self, mailbox, names):
712
"""Request named status conditions for mailbox.
714
(typ, [data]) = <instance>.status(mailbox, names)
717
#if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
718
# raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
719
typ, dat = self._simple_command(name, mailbox, names)
720
return self._untagged_response(typ, dat, name)
723
def store(self, message_set, command, flags):
724
"""Alters flag dispositions for messages in mailbox.
726
(typ, [data]) = <instance>.store(message_set, command, flags)
728
if (flags[0],flags[-1]) != ('(',')'):
729
flags = '(%s)' % flags # Avoid quoting the flags
730
typ, dat = self._simple_command('STORE', message_set, command, flags)
731
return self._untagged_response(typ, dat, 'FETCH')
734
def subscribe(self, mailbox):
735
"""Subscribe to new mailbox.
737
(typ, [data]) = <instance>.subscribe(mailbox)
739
return self._simple_command('SUBSCRIBE', mailbox)
742
def thread(self, threading_algorithm, charset, *search_criteria):
743
"""IMAPrev1 extension THREAD command.
745
(type, [data]) = <instance>.thread(threading_alogrithm, charset, search_criteria, ...)
748
typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
749
return self._untagged_response(typ, dat, name)
752
def uid(self, command, *args):
753
"""Execute "command arg ..." with messages identified by UID,
754
rather than message number.
756
(typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
758
Returns response appropriate to 'command'.
760
command = command.upper()
761
if not command in Commands:
762
raise self.error("Unknown IMAP4 UID command: %s" % command)
763
if self.state not in Commands[command]:
764
raise self.error("command %s illegal in state %s, "
765
"only allowed in states %s" %
766
(command, self.state,
767
', '.join(Commands[command])))
769
typ, dat = self._simple_command(name, command, *args)
770
if command in ('SEARCH', 'SORT'):
774
return self._untagged_response(typ, dat, name)
777
def unsubscribe(self, mailbox):
778
"""Unsubscribe from old mailbox.
780
(typ, [data]) = <instance>.unsubscribe(mailbox)
782
return self._simple_command('UNSUBSCRIBE', mailbox)
785
def xatom(self, name, *args):
786
"""Allow simple extension commands
787
notified by server in CAPABILITY response.
789
Assumes command is legal in current state.
791
(typ, [data]) = <instance>.xatom(name, arg, ...)
793
Returns response appropriate to extension command `name'.
796
#if not name in self.capabilities: # Let the server decide!
797
# raise self.error('unknown extension command: %s' % name)
798
if not name in Commands:
799
Commands[name] = (self.state,)
800
return self._simple_command(name, *args)
807
def _append_untagged(self, typ, dat):
810
ur = self.untagged_responses
813
self._mesg('untagged_responses[%s] %s += ["%r"]' %
814
(typ, len(ur.get(typ,'')), dat))
821
def _check_bye(self):
822
bye = self.untagged_responses.get('BYE')
824
raise self.abort(bye[-1])
827
def _command(self, name, *args):
829
if self.state not in Commands[name]:
831
raise self.error("command %s illegal in state %s, "
832
"only allowed in states %s" %
834
', '.join(Commands[name])))
836
for typ in ('OK', 'NO', 'BAD'):
837
if typ in self.untagged_responses:
838
del self.untagged_responses[typ]
840
if 'READ-ONLY' in self.untagged_responses \
841
and not self.is_readonly:
842
raise self.readonly('mailbox status changed to READ-ONLY')
844
tag = self._new_tag()
845
name = bytes(name, 'ASCII')
846
data = tag + b' ' + name
848
if arg is None: continue
849
if isinstance(arg, str):
850
arg = bytes(arg, "ASCII")
851
#data = data + b' ' + self._checkquote(arg)
852
data = data + b' ' + arg
854
literal = self.literal
855
if literal is not None:
857
if type(literal) is type(self._command):
861
data = data + bytes(' {%s}' % len(literal), 'ASCII')
865
self._mesg('> %r' % data)
867
self._log('> %r' % data)
870
self.send(data + CRLF)
871
except (socket.error, OSError) as val:
872
raise self.abort('socket error: %s' % val)
878
# Wait for continuation response
880
while self._get_response():
881
if self.tagged_commands[tag]: # BAD/NO?
887
literal = literator(self.continuation_response)
891
self._mesg('write literal size %s' % len(literal))
896
except (socket.error, OSError) as val:
897
raise self.abort('socket error: %s' % val)
905
def _command_complete(self, name, tag):
908
typ, data = self._get_tagged_response(tag)
909
except self.abort as val:
910
raise self.abort('command: %s => %s' % (name, val))
911
except self.error as val:
912
raise self.error('command: %s => %s' % (name, val))
915
raise self.error('%s command error: %s %s' % (name, typ, data))
919
def _get_response(self):
921
# Read response and store.
923
# Returns None for continuation responses,
924
# otherwise first response line received.
926
resp = self._get_line()
928
# Command completion response?
930
if self._match(self.tagre, resp):
931
tag = self.mo.group('tag')
932
if not tag in self.tagged_commands:
933
raise self.abort('unexpected tagged response: %s' % resp)
935
typ = self.mo.group('type')
936
typ = str(typ, 'ASCII')
937
dat = self.mo.group('data')
938
self.tagged_commands[tag] = (typ, [dat])
942
# '*' (untagged) responses?
944
if not self._match(Untagged_response, resp):
945
if self._match(Untagged_status, resp):
946
dat2 = self.mo.group('data2')
949
# Only other possibility is '+' (continuation) response...
951
if self._match(Continuation, resp):
952
self.continuation_response = self.mo.group('data')
953
return None # NB: indicates continuation
955
raise self.abort("unexpected response: '%s'" % resp)
957
typ = self.mo.group('type')
958
typ = str(typ, 'ascii')
959
dat = self.mo.group('data')
960
if dat is None: dat = b'' # Null untagged response
961
if dat2: dat = dat + b' ' + dat2
963
# Is there a literal to come?
965
while self._match(Literal, dat):
967
# Read literal direct from connection.
969
size = int(self.mo.group('size'))
972
self._mesg('read literal size %s' % size)
973
data = self.read(size)
975
# Store response with literal as tuple
977
self._append_untagged(typ, (dat, data))
979
# Read trailer - possibly containing another literal
981
dat = self._get_line()
983
self._append_untagged(typ, dat)
985
# Bracketed response information?
987
if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
988
typ = self.mo.group('type')
989
typ = str(typ, "ASCII")
990
self._append_untagged(typ, self.mo.group('data'))
993
if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
994
self._mesg('%s response: %r' % (typ, dat))
999
def _get_tagged_response(self, tag):
1002
result = self.tagged_commands[tag]
1003
if result is not None:
1004
del self.tagged_commands[tag]
1007
# Some have reported "unexpected response" exceptions.
1008
# Note that ignoring them here causes loops.
1009
# Instead, send me details of the unexpected response and
1010
# I'll update the code in `_get_response()'.
1013
self._get_response()
1014
except self.abort as val:
1021
def _get_line(self):
1023
line = self.readline()
1025
raise self.abort('socket error: EOF')
1027
# Protocol mandates all lines terminated by CRLF
1032
self._mesg('< %r' % line)
1034
self._log('< %r' % line)
1038
def _match(self, cre, s):
1040
# Run compiled regular expression match method on 's'.
1041
# Save result, return success.
1043
self.mo = cre.match(s)
1045
if self.mo is not None and self.debug >= 5:
1046
self._mesg("\tmatched r'%r' => %r" % (cre.pattern, self.mo.groups()))
1047
return self.mo is not None
1052
tag = self.tagpre + bytes(str(self.tagnum), 'ASCII')
1053
self.tagnum = self.tagnum + 1
1054
self.tagged_commands[tag] = None
1058
def _checkquote(self, arg):
1060
# Must quote command args if non-alphanumeric chars present,
1061
# and not already quoted.
1063
if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
1065
if arg and self.mustquote.search(arg) is None:
1067
return self._quote(arg)
1070
def _quote(self, arg):
1072
arg = arg.replace(b'\\', b'\\\\')
1073
arg = arg.replace(b'"', b'\\"')
1075
return b'"' + arg + b'"'
1078
def _simple_command(self, name, *args):
1080
return self._command_complete(name, self._command(name, *args))
1083
def _untagged_response(self, typ, dat, name):
1086
if not name in self.untagged_responses:
1088
data = self.untagged_responses.pop(name)
1091
self._mesg('untagged_responses[%s] => %s' % (name, data))
1097
def _mesg(self, s, secs=None):
1100
tm = time.strftime('%M:%S', time.localtime(secs))
1101
sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1104
def _dump_ur(self, dict):
1105
# Dump untagged responses (in `dict').
1109
l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1110
self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1112
def _log(self, line):
1113
# Keep log of last `_cmd_log_len' interactions for debugging.
1114
self._cmd_log[self._cmd_log_idx] = (line, time.time())
1115
self._cmd_log_idx += 1
1116
if self._cmd_log_idx >= self._cmd_log_len:
1117
self._cmd_log_idx = 0
1119
def print_log(self):
1120
self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
1121
i, n = self._cmd_log_idx, self._cmd_log_len
1124
self._mesg(*self._cmd_log[i])
1128
if i >= self._cmd_log_len:
1139
class IMAP4_SSL(IMAP4):
1141
"""IMAP4 client class over SSL connection
1143
Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1145
host - host's name (default: localhost);
1146
port - port number (default: standard IMAP4 SSL port).
1147
keyfile - PEM formatted file that contains your private key (default: None);
1148
certfile - PEM formatted certificate chain file (default: None);
1150
for more documentation see the docstring of the parent class IMAP4.
1154
def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
1155
self.keyfile = keyfile
1156
self.certfile = certfile
1157
IMAP4.__init__(self, host, port)
1159
def _create_socket(self):
1160
sock = IMAP4._create_socket(self)
1161
return ssl.wrap_socket(sock, self.keyfile, self.certfile)
1163
def open(self, host='', port=IMAP4_SSL_PORT):
1164
"""Setup connection to remote server on "host:port".
1165
(default: localhost:standard IMAP4 SSL port).
1166
This connection will be used by the routines:
1167
read, readline, send, shutdown.
1169
IMAP4.open(self, host, port)
1171
__all__.append("IMAP4_SSL")
1174
class IMAP4_stream(IMAP4):
1176
"""IMAP4 client class over a stream
1178
Instantiate with: IMAP4_stream(command)
1180
where "command" is a string that can be passed to subprocess.Popen()
1182
for more documentation see the docstring of the parent class IMAP4.
1186
def __init__(self, command):
1187
self.command = command
1188
IMAP4.__init__(self)
1191
def open(self, host = None, port = None):
1192
"""Setup a stream connection.
1193
This connection will be used by the routines:
1194
read, readline, send, shutdown.
1196
self.host = None # For compatibility with parent class
1200
self.process = subprocess.Popen(self.command,
1201
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1202
shell=True, close_fds=True)
1203
self.writefile = self.process.stdin
1204
self.readfile = self.process.stdout
1206
def read(self, size):
1207
"""Read 'size' bytes from remote."""
1208
return self.readfile.read(size)
1212
"""Read line from remote."""
1213
return self.readfile.readline()
1216
def send(self, data):
1217
"""Send data to remote."""
1218
self.writefile.write(data)
1219
self.writefile.flush()
1223
"""Close I/O established in "open"."""
1224
self.readfile.close()
1225
self.writefile.close()
1230
class _Authenticator:
1232
"""Private class to provide en/decoding
1233
for base64-based authentication conversation.
1236
def __init__(self, mechinst):
1237
self.mech = mechinst # Callable object to provide/process data
1239
def process(self, data):
1240
ret = self.mech(self.decode(data))
1242
return '*' # Abort conversation
1243
return self.encode(ret)
1245
def encode(self, inp):
1247
# Invoke binascii.b2a_base64 iteratively with
1248
# short even length buffers, strip the trailing
1249
# line feed from the result and append. "Even"
1250
# means a number that factors to both 6 and 8,
1251
# so when it gets to the end of the 8-bit input
1252
# there's no partial 6-bit output.
1262
e = binascii.b2a_base64(t)
1267
def decode(self, inp):
1270
return binascii.a2b_base64(inp)
1274
Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1275
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1277
def Internaldate2tuple(resp):
1278
"""Convert IMAP4 INTERNALDATE to UT.
1280
Returns Python time module tuple.
1283
mo = InternalDate.match(resp)
1287
mon = Mon2num[mo.group('mon')]
1288
zonen = mo.group('zonen')
1290
day = int(mo.group('day'))
1291
year = int(mo.group('year'))
1292
hour = int(mo.group('hour'))
1293
min = int(mo.group('min'))
1294
sec = int(mo.group('sec'))
1295
zoneh = int(mo.group('zoneh'))
1296
zonem = int(mo.group('zonem'))
1298
# INTERNALDATE timezone must be subtracted to get UT
1300
zone = (zoneh*60 + zonem)*60
1304
tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1306
utc = time.mktime(tt)
1308
# Following is necessary because the time module has no 'mkgmtime'.
1309
# 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1311
lt = time.localtime(utc)
1312
if time.daylight and lt[-1]:
1313
zone = zone + time.altzone
1315
zone = zone + time.timezone
1317
return time.localtime(utc - zone)
1323
"""Convert integer to A-P string representation."""
1325
val = b''; AP = b'ABCDEFGHIJKLMNOP'
1328
num, mod = divmod(num, 16)
1329
val = AP[mod:mod+1] + val
1334
def ParseFlags(resp):
1336
"""Convert IMAP4 flags response to python tuple."""
1338
mo = Flags.match(resp)
1342
return tuple(mo.group('flags').split())
1345
def Time2Internaldate(date_time):
1347
"""Convert 'date_time' to IMAP4 INTERNALDATE representation.
1349
Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1352
if isinstance(date_time, (int, float)):
1353
tt = time.localtime(date_time)
1354
elif isinstance(date_time, (tuple, time.struct_time)):
1356
elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1357
return date_time # Assume in correct format
1359
raise ValueError("date_time not of a known type")
1361
dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1364
if time.daylight and tt[-1]:
1365
zone = -time.altzone
1367
zone = -time.timezone
1368
return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
1372
if __name__ == '__main__':
1374
# To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1375
# or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1376
# to test the IMAP4_stream class
1378
import getopt, getpass
1381
optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
1382
except getopt.error as val:
1383
optlist, args = (), ()
1385
stream_command = None
1386
for opt,val in optlist:
1390
stream_command = val
1391
if not args: args = (stream_command,)
1393
if not args: args = ('',)
1397
USER = getpass.getuser()
1398
PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1400
test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
1402
('login', (USER, PASSWD)),
1403
('create', ('/tmp/xxx 1',)),
1404
('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1405
('CREATE', ('/tmp/yyz 2',)),
1406
('append', ('/tmp/yyz 2', None, None, test_mesg)),
1407
('list', ('/tmp', 'yy*')),
1408
('select', ('/tmp/yyz 2',)),
1409
('search', (None, 'SUBJECT', 'test')),
1410
('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1411
('store', ('1', 'FLAGS', '(\Deleted)')),
1420
('response',('UIDVALIDITY',)),
1421
('uid', ('SEARCH', 'ALL')),
1422
('response', ('EXISTS',)),
1423
('append', (None, None, None, test_mesg)),
1429
M._mesg('%s %s' % (cmd, args))
1430
typ, dat = getattr(M, cmd)(*args)
1431
M._mesg('%s => %s %s' % (cmd, typ, dat))
1432
if typ == 'NO': raise dat[0]
1437
M = IMAP4_stream(stream_command)
1440
if M.state == 'AUTH':
1441
test_seq1 = test_seq1[1:] # Login not needed
1442
M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1443
M._mesg('CAPABILITIES = %r' % (M.capabilities,))
1445
for cmd,args in test_seq1:
1448
for ml in run('list', ('/tmp/', 'yy%')):
1449
mo = re.match(r'.*"([^"]+)"$', ml)
1450
if mo: path = mo.group(1)
1451
else: path = ml.split()[-1]
1452
run('delete', (path,))
1454
for cmd,args in test_seq2:
1455
dat = run(cmd, args)
1457
if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1460
uid = dat[-1].split()
1461
if not uid: continue
1462
run('uid', ('FETCH', '%s' % uid[-1],
1463
'(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1465
print('\nAll tests OK.')
1468
print('\nTests failed.')
1472
If you would like to see debugging output,