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, os, random, re, socket, 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(r'\+( (?P<data>.*))?')
85
Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
86
InternalDate = re.compile(r'.*INTERNALDATE "'
87
r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
88
r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
89
r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
91
Literal = re.compile(r'.*{(?P<size>\d+)}$')
92
MapCRLF = re.compile(r'\r\n|\r|\n')
93
Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
94
Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
95
Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
101
"""IMAP4 client class.
103
Instantiate with: IMAP4([host[, port]])
105
host - host's name (default: localhost);
106
port - port number (default: standard IMAP4 port).
108
All IMAP4rev1 commands are supported by methods of the same
109
name (in lower-case).
111
All arguments to commands are converted to strings, except for
112
AUTHENTICATE, and the last argument to APPEND which is passed as
113
an IMAP4 literal. If necessary (the string contains any
114
non-printing characters or white-space and isn't enclosed with
115
either parentheses or double quotes) each string is quoted.
116
However, the 'password' argument to the LOGIN command is always
117
quoted. If you want to avoid having an argument string quoted
118
(eg: the 'flags' argument to STORE) then enclose the string in
119
parentheses (eg: "(\Deleted)").
121
Each command returns a tuple: (type, [data, ...]) where 'type'
122
is usually 'OK' or 'NO', and 'data' is either the text from the
123
tagged response, or untagged results from command. Each 'data'
124
is either a string, or a tuple. If a tuple, then the first part
125
is the header of the response, and the second part contains
126
the data (ie: 'literal' value).
128
Errors raise the exception class <instance>.error("<reason>").
129
IMAP4 server errors raise <instance>.abort("<reason>"),
130
which is a sub-class of 'error'. Mailbox status changes
131
from READ-WRITE to READ-ONLY raise the exception class
132
<instance>.readonly("<reason>"), which is a sub-class of 'abort'.
134
"error" exceptions imply a program error.
135
"abort" exceptions imply the connection should be reset, and
136
the command re-tried.
137
"readonly" exceptions imply the command should be re-tried.
139
Note: to use this module, you must read the RFCs pertaining to the
140
IMAP4 protocol, as the semantics of the arguments to each IMAP4
141
command are left to the invoker, not to mention the results. Also,
142
most IMAP servers implement a sub-set of the commands available here.
145
class error(Exception): pass # Logical errors - debug required
146
class abort(error): pass # Service errors - close and retry
147
class readonly(abort): pass # Mailbox status changed to READ-ONLY
149
mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
151
def __init__(self, host = '', port = IMAP4_PORT):
153
self.state = 'LOGOUT'
154
self.literal = None # A literal argument to a command
155
self.tagged_commands = {} # Tagged commands awaiting response
156
self.untagged_responses = {} # {typ: [data, ...], ...}
157
self.continuation_response = '' # Last continuation response
158
self.is_readonly = False # READ-ONLY desired state
161
# Open socket to server.
163
self.open(host, port)
165
# Create unique tag for this session,
166
# and compile tagged response matcher.
168
self.tagpre = Int2AP(random.randint(4096, 65535))
169
self.tagre = re.compile(r'(?P<tag>'
171
+ r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
173
# Get server welcome message,
174
# request and store CAPABILITY response.
177
self._cmd_log_len = 10
178
self._cmd_log_idx = 0
179
self._cmd_log = {} # Last `_cmd_log_len' interactions
181
self._mesg('imaplib version %s' % __version__)
182
self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
184
self.welcome = self._get_response()
185
if 'PREAUTH' in self.untagged_responses:
187
elif 'OK' in self.untagged_responses:
188
self.state = 'NONAUTH'
190
raise self.error(self.welcome)
192
typ, dat = self.capability()
194
raise self.error('no CAPABILITY response from server')
195
self.capabilities = tuple(dat[-1].upper().split())
199
self._mesg('CAPABILITIES: %r' % (self.capabilities,))
201
for version in AllowedVersions:
202
if not version in self.capabilities:
204
self.PROTOCOL_VERSION = version
207
raise self.error('server not IMAP4 compliant')
210
def __getattr__(self, attr):
211
# Allow UPPERCASE variants of IMAP4 command methods.
213
return getattr(self, attr.lower())
214
raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
218
# Overridable methods
221
def open(self, host = '', port = IMAP4_PORT):
222
"""Setup connection to remote server on "host:port"
223
(default: localhost:standard IMAP4 port).
224
This connection will be used by the routines:
225
read, readline, send, shutdown.
229
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
230
self.sock.connect((host, port))
231
self.file = self.sock.makefile('rb')
234
def read(self, size):
235
"""Read 'size' bytes from remote."""
236
return self.file.read(size)
240
"""Read line from remote."""
241
return self.file.readline()
244
def send(self, data):
245
"""Send data to remote."""
246
self.sock.sendall(data)
250
"""Close I/O established in "open"."""
256
"""Return socket instance used to connect to IMAP4 server.
258
socket = <instance>.socket()
268
"""Return most recent 'RECENT' responses if any exist,
269
else prompt server for an update using the 'NOOP' command.
271
(typ, [data]) = <instance>.recent()
273
'data' is None if no new messages,
274
else list of RECENT responses, most recent last.
277
typ, dat = self._untagged_response('OK', [None], name)
280
typ, dat = self.noop() # Prod server for response
281
return self._untagged_response(typ, dat, name)
284
def response(self, code):
285
"""Return data for response 'code' if received, or None.
287
Old value for response 'code' is cleared.
289
(code, [data]) = <instance>.response(code)
291
return self._untagged_response(code, [None], code.upper())
298
def append(self, mailbox, flags, date_time, message):
299
"""Append message to named mailbox.
301
(typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
303
All args except `message' can be None.
309
if (flags[0],flags[-1]) != ('(',')'):
310
flags = '(%s)' % flags
314
date_time = Time2Internaldate(date_time)
317
self.literal = MapCRLF.sub(CRLF, message)
318
return self._simple_command(name, mailbox, flags, date_time)
321
def authenticate(self, mechanism, authobject):
322
"""Authenticate command - requires response processing.
324
'mechanism' specifies which authentication mechanism is to
325
be used - it must appear in <instance>.capabilities in the
326
form AUTH=<mechanism>.
328
'authobject' must be a callable object:
330
data = authobject(response)
332
It will be called to process server continuation responses.
333
It should return data that will be encoded and sent to server.
334
It should return None if the client abort response '*' should
337
mech = mechanism.upper()
338
# XXX: shouldn't this code be removed, not commented out?
339
#cap = 'AUTH=%s' % mech
340
#if not cap in self.capabilities: # Let the server decide!
341
# raise self.error("Server doesn't allow %s authentication." % mech)
342
self.literal = _Authenticator(authobject).process
343
typ, dat = self._simple_command('AUTHENTICATE', mech)
345
raise self.error(dat[-1])
350
def capability(self):
351
"""(typ, [data]) = <instance>.capability()
352
Fetch capabilities list from server."""
355
typ, dat = self._simple_command(name)
356
return self._untagged_response(typ, dat, name)
360
"""Checkpoint mailbox on server.
362
(typ, [data]) = <instance>.check()
364
return self._simple_command('CHECK')
368
"""Close currently selected mailbox.
370
Deleted messages are removed from writable mailbox.
371
This is the recommended command before 'LOGOUT'.
373
(typ, [data]) = <instance>.close()
376
typ, dat = self._simple_command('CLOSE')
382
def copy(self, message_set, new_mailbox):
383
"""Copy 'message_set' messages onto end of 'new_mailbox'.
385
(typ, [data]) = <instance>.copy(message_set, new_mailbox)
387
return self._simple_command('COPY', message_set, new_mailbox)
390
def create(self, mailbox):
391
"""Create new mailbox.
393
(typ, [data]) = <instance>.create(mailbox)
395
return self._simple_command('CREATE', mailbox)
398
def delete(self, mailbox):
399
"""Delete old mailbox.
401
(typ, [data]) = <instance>.delete(mailbox)
403
return self._simple_command('DELETE', mailbox)
405
def deleteacl(self, mailbox, who):
406
"""Delete the ACLs (remove any rights) set for who on mailbox.
408
(typ, [data]) = <instance>.deleteacl(mailbox, who)
410
return self._simple_command('DELETEACL', mailbox, who)
413
"""Permanently remove deleted items from selected mailbox.
415
Generates 'EXPUNGE' response for each deleted message.
417
(typ, [data]) = <instance>.expunge()
419
'data' is list of 'EXPUNGE'd message numbers in order received.
422
typ, dat = self._simple_command(name)
423
return self._untagged_response(typ, dat, name)
426
def fetch(self, message_set, message_parts):
427
"""Fetch (parts of) messages.
429
(typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
431
'message_parts' should be a string of selected parts
432
enclosed in parentheses, eg: "(UID BODY[TEXT])".
434
'data' are tuples of message part envelope and data.
437
typ, dat = self._simple_command(name, message_set, message_parts)
438
return self._untagged_response(typ, dat, name)
441
def getacl(self, mailbox):
442
"""Get the ACLs for a mailbox.
444
(typ, [data]) = <instance>.getacl(mailbox)
446
typ, dat = self._simple_command('GETACL', mailbox)
447
return self._untagged_response(typ, dat, 'ACL')
450
def getannotation(self, mailbox, entry, attribute):
451
"""(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
452
Retrieve ANNOTATIONs."""
454
typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
455
return self._untagged_response(typ, dat, 'ANNOTATION')
458
def getquota(self, root):
459
"""Get the quota root's resource usage and limits.
461
Part of the IMAP4 QUOTA extension defined in rfc2087.
463
(typ, [data]) = <instance>.getquota(root)
465
typ, dat = self._simple_command('GETQUOTA', root)
466
return self._untagged_response(typ, dat, 'QUOTA')
469
def getquotaroot(self, mailbox):
470
"""Get the list of quota roots for the named mailbox.
472
(typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
474
typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
475
typ, quota = self._untagged_response(typ, dat, 'QUOTA')
476
typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
477
return typ, [quotaroot, quota]
480
def list(self, directory='""', pattern='*'):
481
"""List mailbox names in directory matching pattern.
483
(typ, [data]) = <instance>.list(directory='""', pattern='*')
485
'data' is list of LIST responses.
488
typ, dat = self._simple_command(name, directory, pattern)
489
return self._untagged_response(typ, dat, name)
492
def login(self, user, password):
493
"""Identify client using plaintext password.
495
(typ, [data]) = <instance>.login(user, password)
497
NB: 'password' will be quoted.
499
typ, dat = self._simple_command('LOGIN', user, self._quote(password))
501
raise self.error(dat[-1])
506
def login_cram_md5(self, user, password):
507
""" Force use of CRAM-MD5 authentication.
509
(typ, [data]) = <instance>.login_cram_md5(user, password)
511
self.user, self.password = user, password
512
return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
515
def _CRAM_MD5_AUTH(self, challenge):
516
""" Authobject to use with CRAM-MD5 authentication. """
518
return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
522
"""Shutdown connection to server.
524
(typ, [data]) = <instance>.logout()
526
Returns server 'BYE' response.
528
self.state = 'LOGOUT'
529
try: typ, dat = self._simple_command('LOGOUT')
530
except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
532
if 'BYE' in self.untagged_responses:
533
return 'BYE', self.untagged_responses['BYE']
537
def lsub(self, directory='""', pattern='*'):
538
"""List 'subscribed' mailbox names in directory matching pattern.
540
(typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
542
'data' are tuples of message part envelope and data.
545
typ, dat = self._simple_command(name, directory, pattern)
546
return self._untagged_response(typ, dat, name)
548
def myrights(self, mailbox):
549
"""Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
551
(typ, [data]) = <instance>.myrights(mailbox)
553
typ,dat = self._simple_command('MYRIGHTS', mailbox)
554
return self._untagged_response(typ, dat, 'MYRIGHTS')
557
""" Returns IMAP namespaces ala rfc2342
559
(typ, [data, ...]) = <instance>.namespace()
562
typ, dat = self._simple_command(name)
563
return self._untagged_response(typ, dat, name)
567
"""Send NOOP command.
569
(typ, [data]) = <instance>.noop()
573
self._dump_ur(self.untagged_responses)
574
return self._simple_command('NOOP')
577
def partial(self, message_num, message_part, start, length):
578
"""Fetch truncated part of a message.
580
(typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
582
'data' is tuple of message part envelope and data.
585
typ, dat = self._simple_command(name, message_num, message_part, start, length)
586
return self._untagged_response(typ, dat, 'FETCH')
589
def proxyauth(self, user):
590
"""Assume authentication as "user".
592
Allows an authorised administrator to proxy into any user's
595
(typ, [data]) = <instance>.proxyauth(user)
599
return self._simple_command('PROXYAUTH', user)
602
def rename(self, oldmailbox, newmailbox):
603
"""Rename old mailbox name to new.
605
(typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
607
return self._simple_command('RENAME', oldmailbox, newmailbox)
610
def search(self, charset, *criteria):
611
"""Search mailbox for matching messages.
613
(typ, [data]) = <instance>.search(charset, criterion, ...)
615
'data' is space separated list of matching message numbers.
619
typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
621
typ, dat = self._simple_command(name, *criteria)
622
return self._untagged_response(typ, dat, name)
625
def select(self, mailbox='INBOX', readonly=False):
628
Flush all untagged responses.
630
(typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
632
'data' is count of messages in mailbox ('EXISTS' response).
634
Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
635
other responses should be obtained via <instance>.response('FLAGS') etc.
637
self.untagged_responses = {} # Flush old responses.
638
self.is_readonly = readonly
643
typ, dat = self._simple_command(name, mailbox)
645
self.state = 'AUTH' # Might have been 'SELECTED'
647
self.state = 'SELECTED'
648
if 'READ-ONLY' in self.untagged_responses \
652
self._dump_ur(self.untagged_responses)
653
raise self.readonly('%s is not writable' % mailbox)
654
return typ, self.untagged_responses.get('EXISTS', [None])
657
def setacl(self, mailbox, who, what):
658
"""Set a mailbox acl.
660
(typ, [data]) = <instance>.setacl(mailbox, who, what)
662
return self._simple_command('SETACL', mailbox, who, what)
665
def setannotation(self, *args):
666
"""(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
669
typ, dat = self._simple_command('SETANNOTATION', *args)
670
return self._untagged_response(typ, dat, 'ANNOTATION')
673
def setquota(self, root, limits):
674
"""Set the quota root's resource limits.
676
(typ, [data]) = <instance>.setquota(root, limits)
678
typ, dat = self._simple_command('SETQUOTA', root, limits)
679
return self._untagged_response(typ, dat, 'QUOTA')
682
def sort(self, sort_criteria, charset, *search_criteria):
683
"""IMAP4rev1 extension SORT command.
685
(typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
688
#if not name in self.capabilities: # Let the server decide!
689
# raise self.error('unimplemented extension command: %s' % name)
690
if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
691
sort_criteria = '(%s)' % sort_criteria
692
typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
693
return self._untagged_response(typ, dat, name)
696
def status(self, mailbox, names):
697
"""Request named status conditions for mailbox.
699
(typ, [data]) = <instance>.status(mailbox, names)
702
#if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
703
# raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
704
typ, dat = self._simple_command(name, mailbox, names)
705
return self._untagged_response(typ, dat, name)
708
def store(self, message_set, command, flags):
709
"""Alters flag dispositions for messages in mailbox.
711
(typ, [data]) = <instance>.store(message_set, command, flags)
713
if (flags[0],flags[-1]) != ('(',')'):
714
flags = '(%s)' % flags # Avoid quoting the flags
715
typ, dat = self._simple_command('STORE', message_set, command, flags)
716
return self._untagged_response(typ, dat, 'FETCH')
719
def subscribe(self, mailbox):
720
"""Subscribe to new mailbox.
722
(typ, [data]) = <instance>.subscribe(mailbox)
724
return self._simple_command('SUBSCRIBE', mailbox)
727
def thread(self, threading_algorithm, charset, *search_criteria):
728
"""IMAPrev1 extension THREAD command.
730
(type, [data]) = <instance>.thread(threading_alogrithm, charset, search_criteria, ...)
733
typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
734
return self._untagged_response(typ, dat, name)
737
def uid(self, command, *args):
738
"""Execute "command arg ..." with messages identified by UID,
739
rather than message number.
741
(typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
743
Returns response appropriate to 'command'.
745
command = command.upper()
746
if not command in Commands:
747
raise self.error("Unknown IMAP4 UID command: %s" % command)
748
if self.state not in Commands[command]:
749
raise self.error("command %s illegal in state %s, "
750
"only allowed in states %s" %
751
(command, self.state,
752
', '.join(Commands[command])))
754
typ, dat = self._simple_command(name, command, *args)
755
if command in ('SEARCH', 'SORT'):
759
return self._untagged_response(typ, dat, name)
762
def unsubscribe(self, mailbox):
763
"""Unsubscribe from old mailbox.
765
(typ, [data]) = <instance>.unsubscribe(mailbox)
767
return self._simple_command('UNSUBSCRIBE', mailbox)
770
def xatom(self, name, *args):
771
"""Allow simple extension commands
772
notified by server in CAPABILITY response.
774
Assumes command is legal in current state.
776
(typ, [data]) = <instance>.xatom(name, arg, ...)
778
Returns response appropriate to extension command `name'.
781
#if not name in self.capabilities: # Let the server decide!
782
# raise self.error('unknown extension command: %s' % name)
783
if not name in Commands:
784
Commands[name] = (self.state,)
785
return self._simple_command(name, *args)
792
def _append_untagged(self, typ, dat):
794
if dat is None: dat = ''
795
ur = self.untagged_responses
798
self._mesg('untagged_responses[%s] %s += ["%s"]' %
799
(typ, len(ur.get(typ,'')), dat))
806
def _check_bye(self):
807
bye = self.untagged_responses.get('BYE')
809
raise self.abort(bye[-1])
812
def _command(self, name, *args):
814
if self.state not in Commands[name]:
816
raise self.error("command %s illegal in state %s, "
817
"only allowed in states %s" %
819
', '.join(Commands[name])))
821
for typ in ('OK', 'NO', 'BAD'):
822
if typ in self.untagged_responses:
823
del self.untagged_responses[typ]
825
if 'READ-ONLY' in self.untagged_responses \
826
and not self.is_readonly:
827
raise self.readonly('mailbox status changed to READ-ONLY')
829
tag = self._new_tag()
830
data = '%s %s' % (tag, name)
832
if arg is None: continue
833
data = '%s %s' % (data, self._checkquote(arg))
835
literal = self.literal
836
if literal is not None:
838
if type(literal) is type(self._command):
842
data = '%s {%s}' % (data, len(literal))
846
self._mesg('> %s' % data)
848
self._log('> %s' % data)
851
self.send('%s%s' % (data, CRLF))
852
except (socket.error, OSError), val:
853
raise self.abort('socket error: %s' % val)
859
# Wait for continuation response
861
while self._get_response():
862
if self.tagged_commands[tag]: # BAD/NO?
868
literal = literator(self.continuation_response)
872
self._mesg('write literal size %s' % len(literal))
877
except (socket.error, OSError), val:
878
raise self.abort('socket error: %s' % val)
886
def _command_complete(self, name, tag):
889
typ, data = self._get_tagged_response(tag)
890
except self.abort, val:
891
raise self.abort('command: %s => %s' % (name, val))
892
except self.error, val:
893
raise self.error('command: %s => %s' % (name, val))
896
raise self.error('%s command error: %s %s' % (name, typ, data))
900
def _get_response(self):
902
# Read response and store.
904
# Returns None for continuation responses,
905
# otherwise first response line received.
907
resp = self._get_line()
909
# Command completion response?
911
if self._match(self.tagre, resp):
912
tag = self.mo.group('tag')
913
if not tag in self.tagged_commands:
914
raise self.abort('unexpected tagged response: %s' % resp)
916
typ = self.mo.group('type')
917
dat = self.mo.group('data')
918
self.tagged_commands[tag] = (typ, [dat])
922
# '*' (untagged) responses?
924
if not self._match(Untagged_response, resp):
925
if self._match(Untagged_status, resp):
926
dat2 = self.mo.group('data2')
929
# Only other possibility is '+' (continuation) response...
931
if self._match(Continuation, resp):
932
self.continuation_response = self.mo.group('data')
933
return None # NB: indicates continuation
935
raise self.abort("unexpected response: '%s'" % resp)
937
typ = self.mo.group('type')
938
dat = self.mo.group('data')
939
if dat is None: dat = '' # Null untagged response
940
if dat2: dat = dat + ' ' + dat2
942
# Is there a literal to come?
944
while self._match(Literal, dat):
946
# Read literal direct from connection.
948
size = int(self.mo.group('size'))
951
self._mesg('read literal size %s' % size)
952
data = self.read(size)
954
# Store response with literal as tuple
956
self._append_untagged(typ, (dat, data))
958
# Read trailer - possibly containing another literal
960
dat = self._get_line()
962
self._append_untagged(typ, dat)
964
# Bracketed response information?
966
if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
967
self._append_untagged(self.mo.group('type'), self.mo.group('data'))
970
if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
971
self._mesg('%s response: %s' % (typ, dat))
976
def _get_tagged_response(self, tag):
979
result = self.tagged_commands[tag]
980
if result is not None:
981
del self.tagged_commands[tag]
984
# Some have reported "unexpected response" exceptions.
985
# Note that ignoring them here causes loops.
986
# Instead, send me details of the unexpected response and
987
# I'll update the code in `_get_response()'.
991
except self.abort, val:
1000
line = self.readline()
1002
raise self.abort('socket error: EOF')
1004
# Protocol mandates all lines terminated by CRLF
1009
self._mesg('< %s' % line)
1011
self._log('< %s' % line)
1015
def _match(self, cre, s):
1017
# Run compiled regular expression match method on 's'.
1018
# Save result, return success.
1020
self.mo = cre.match(s)
1022
if self.mo is not None and self.debug >= 5:
1023
self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
1024
return self.mo is not None
1029
tag = '%s%s' % (self.tagpre, self.tagnum)
1030
self.tagnum = self.tagnum + 1
1031
self.tagged_commands[tag] = None
1035
def _checkquote(self, arg):
1037
# Must quote command args if non-alphanumeric chars present,
1038
# and not already quoted.
1040
if type(arg) is not type(''):
1042
if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
1044
if arg and self.mustquote.search(arg) is None:
1046
return self._quote(arg)
1049
def _quote(self, arg):
1051
arg = arg.replace('\\', '\\\\')
1052
arg = arg.replace('"', '\\"')
1057
def _simple_command(self, name, *args):
1059
return self._command_complete(name, self._command(name, *args))
1062
def _untagged_response(self, typ, dat, name):
1066
if not name in self.untagged_responses:
1068
data = self.untagged_responses.pop(name)
1071
self._mesg('untagged_responses[%s] => %s' % (name, data))
1077
def _mesg(self, s, secs=None):
1080
tm = time.strftime('%M:%S', time.localtime(secs))
1081
sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1084
def _dump_ur(self, dict):
1085
# Dump untagged responses (in `dict').
1089
l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1090
self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1092
def _log(self, line):
1093
# Keep log of last `_cmd_log_len' interactions for debugging.
1094
self._cmd_log[self._cmd_log_idx] = (line, time.time())
1095
self._cmd_log_idx += 1
1096
if self._cmd_log_idx >= self._cmd_log_len:
1097
self._cmd_log_idx = 0
1099
def print_log(self):
1100
self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
1101
i, n = self._cmd_log_idx, self._cmd_log_len
1104
self._mesg(*self._cmd_log[i])
1108
if i >= self._cmd_log_len:
1119
class IMAP4_SSL(IMAP4):
1121
"""IMAP4 client class over SSL connection
1123
Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1125
host - host's name (default: localhost);
1126
port - port number (default: standard IMAP4 SSL port).
1127
keyfile - PEM formatted file that contains your private key (default: None);
1128
certfile - PEM formatted certificate chain file (default: None);
1130
for more documentation see the docstring of the parent class IMAP4.
1134
def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
1135
self.keyfile = keyfile
1136
self.certfile = certfile
1137
IMAP4.__init__(self, host, port)
1140
def open(self, host = '', port = IMAP4_SSL_PORT):
1141
"""Setup connection to remote server on "host:port".
1142
(default: localhost:standard IMAP4 SSL port).
1143
This connection will be used by the routines:
1144
read, readline, send, shutdown.
1148
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1149
self.sock.connect((host, port))
1150
self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
1153
def read(self, size):
1154
"""Read 'size' bytes from remote."""
1155
# sslobj.read() sometimes returns < size bytes
1159
data = self.sslobj.read(min(size-read, 16384))
1163
return ''.join(chunks)
1167
"""Read line from remote."""
1170
char = self.sslobj.read(1)
1172
if char == "\n": return ''.join(line)
1175
def send(self, data):
1176
"""Send data to remote."""
1179
sent = self.sslobj.write(data)
1183
bytes = bytes - sent
1187
"""Close I/O established in "open"."""
1192
"""Return socket instance used to connect to IMAP4 server.
1194
socket = <instance>.socket()
1200
"""Return SSLObject instance used to communicate with the IMAP4 server.
1202
ssl = ssl.wrap_socket(<instance>.socket)
1206
__all__.append("IMAP4_SSL")
1209
class IMAP4_stream(IMAP4):
1211
"""IMAP4 client class over a stream
1213
Instantiate with: IMAP4_stream(command)
1215
where "command" is a string that can be passed to os.popen2()
1217
for more documentation see the docstring of the parent class IMAP4.
1221
def __init__(self, command):
1222
self.command = command
1223
IMAP4.__init__(self)
1226
def open(self, host = None, port = None):
1227
"""Setup a stream connection.
1228
This connection will be used by the routines:
1229
read, readline, send, shutdown.
1231
self.host = None # For compatibility with parent class
1235
self.writefile, self.readfile = os.popen2(self.command)
1238
def read(self, size):
1239
"""Read 'size' bytes from remote."""
1240
return self.readfile.read(size)
1244
"""Read line from remote."""
1245
return self.readfile.readline()
1248
def send(self, data):
1249
"""Send data to remote."""
1250
self.writefile.write(data)
1251
self.writefile.flush()
1255
"""Close I/O established in "open"."""
1256
self.readfile.close()
1257
self.writefile.close()
1261
class _Authenticator:
1263
"""Private class to provide en/decoding
1264
for base64-based authentication conversation.
1267
def __init__(self, mechinst):
1268
self.mech = mechinst # Callable object to provide/process data
1270
def process(self, data):
1271
ret = self.mech(self.decode(data))
1273
return '*' # Abort conversation
1274
return self.encode(ret)
1276
def encode(self, inp):
1278
# Invoke binascii.b2a_base64 iteratively with
1279
# short even length buffers, strip the trailing
1280
# line feed from the result and append. "Even"
1281
# means a number that factors to both 6 and 8,
1282
# so when it gets to the end of the 8-bit input
1283
# there's no partial 6-bit output.
1293
e = binascii.b2a_base64(t)
1298
def decode(self, inp):
1301
return binascii.a2b_base64(inp)
1305
Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1306
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1308
def Internaldate2tuple(resp):
1309
"""Convert IMAP4 INTERNALDATE to UT.
1311
Returns Python time module tuple.
1314
mo = InternalDate.match(resp)
1318
mon = Mon2num[mo.group('mon')]
1319
zonen = mo.group('zonen')
1321
day = int(mo.group('day'))
1322
year = int(mo.group('year'))
1323
hour = int(mo.group('hour'))
1324
min = int(mo.group('min'))
1325
sec = int(mo.group('sec'))
1326
zoneh = int(mo.group('zoneh'))
1327
zonem = int(mo.group('zonem'))
1329
# INTERNALDATE timezone must be subtracted to get UT
1331
zone = (zoneh*60 + zonem)*60
1335
tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1337
utc = time.mktime(tt)
1339
# Following is necessary because the time module has no 'mkgmtime'.
1340
# 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1342
lt = time.localtime(utc)
1343
if time.daylight and lt[-1]:
1344
zone = zone + time.altzone
1346
zone = zone + time.timezone
1348
return time.localtime(utc - zone)
1354
"""Convert integer to A-P string representation."""
1356
val = ''; AP = 'ABCDEFGHIJKLMNOP'
1359
num, mod = divmod(num, 16)
1365
def ParseFlags(resp):
1367
"""Convert IMAP4 flags response to python tuple."""
1369
mo = Flags.match(resp)
1373
return tuple(mo.group('flags').split())
1376
def Time2Internaldate(date_time):
1378
"""Convert 'date_time' to IMAP4 INTERNALDATE representation.
1380
Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1383
if isinstance(date_time, (int, float)):
1384
tt = time.localtime(date_time)
1385
elif isinstance(date_time, (tuple, time.struct_time)):
1387
elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1388
return date_time # Assume in correct format
1390
raise ValueError("date_time not of a known type")
1392
dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1395
if time.daylight and tt[-1]:
1396
zone = -time.altzone
1398
zone = -time.timezone
1399
return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
1403
if __name__ == '__main__':
1405
# To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1406
# or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1407
# to test the IMAP4_stream class
1409
import getopt, getpass
1412
optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
1413
except getopt.error, val:
1414
optlist, args = (), ()
1416
stream_command = None
1417
for opt,val in optlist:
1421
stream_command = val
1422
if not args: args = (stream_command,)
1424
if not args: args = ('',)
1428
USER = getpass.getuser()
1429
PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1431
test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
1433
('login', (USER, PASSWD)),
1434
('create', ('/tmp/xxx 1',)),
1435
('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1436
('CREATE', ('/tmp/yyz 2',)),
1437
('append', ('/tmp/yyz 2', None, None, test_mesg)),
1438
('list', ('/tmp', 'yy*')),
1439
('select', ('/tmp/yyz 2',)),
1440
('search', (None, 'SUBJECT', 'test')),
1441
('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1442
('store', ('1', 'FLAGS', '(\Deleted)')),
1451
('response',('UIDVALIDITY',)),
1452
('uid', ('SEARCH', 'ALL')),
1453
('response', ('EXISTS',)),
1454
('append', (None, None, None, test_mesg)),
1460
M._mesg('%s %s' % (cmd, args))
1461
typ, dat = getattr(M, cmd)(*args)
1462
M._mesg('%s => %s %s' % (cmd, typ, dat))
1463
if typ == 'NO': raise dat[0]
1468
M = IMAP4_stream(stream_command)
1471
if M.state == 'AUTH':
1472
test_seq1 = test_seq1[1:] # Login not needed
1473
M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1474
M._mesg('CAPABILITIES = %r' % (M.capabilities,))
1476
for cmd,args in test_seq1:
1479
for ml in run('list', ('/tmp/', 'yy%')):
1480
mo = re.match(r'.*"([^"]+)"$', ml)
1481
if mo: path = mo.group(1)
1482
else: path = ml.split()[-1]
1483
run('delete', (path,))
1485
for cmd,args in test_seq2:
1486
dat = run(cmd, args)
1488
if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1491
uid = dat[-1].split()
1492
if not uid: continue
1493
run('uid', ('FETCH', '%s' % uid[-1],
1494
'(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1496
print '\nAll tests OK.'
1499
print '\nTests failed.'
1503
If you would like to see debugging output,