]> code.delx.au - offlineimap/blob - offlineimap/imaplib2.py
Merge branch 'master' of ssh://jpgarch@complete.org/~jpgarch/git/offlineimap
[offlineimap] / offlineimap / imaplib2.py
1 #!/usr/bin/env python2.5
2
3 """Threaded IMAP4 client.
4
5 Based on RFC 2060 and original imaplib module.
6
7 Public classes: IMAP4
8 IMAP4_SSL
9 IMAP4_stream
10
11 Public functions: Internaldate2Time
12 ParseFlags
13 Time2Internaldate
14 """
15
16
17 __all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream",
18 "Internaldate2Time", "ParseFlags", "Time2Internaldate")
19
20 __version__ = "2.6"
21 __release__ = "2"
22 __revision__ = "6"
23 __credits__ = """
24 Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
25 String method conversion by ESR, February 2001.
26 GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
27 IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
28 GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
29 PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002.
30 IDLE via threads suggested by Philippe Normand <phil@respyre.org> January 2005.
31 GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
32 New socket open code from http://www.python.org/doc/lib/socket-example.html."""
33 __author__ = "Piers Lauder <piers@janeelix.com>"
34 # Source URL: http://www.cs.usyd.edu.au/~piers/python/imaplib2
35
36 import binascii, os, Queue, random, re, select, socket, sys, time, threading
37
38 select_module = select
39
40 # Globals
41
42 CRLF = '\r\n'
43 Debug = None # Backward compatibility
44 IMAP4_PORT = 143
45 IMAP4_SSL_PORT = 993
46
47 IDLE_TIMEOUT_RESPONSE = '* IDLE TIMEOUT'
48 IDLE_TIMEOUT = 60*29 # Don't stay in IDLE state longer
49
50 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
51
52 # Commands
53
54 CMD_VAL_STATES = 0
55 CMD_VAL_ASYNC = 1
56 NONAUTH, AUTH, SELECTED, LOGOUT = 'NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'
57
58 Commands = {
59 # name valid states asynchronous
60 'APPEND': ((AUTH, SELECTED), False),
61 'AUTHENTICATE': ((NONAUTH,), False),
62 'CAPABILITY': ((NONAUTH, AUTH, SELECTED), True),
63 'CHECK': ((SELECTED,), True),
64 'CLOSE': ((SELECTED,), False),
65 'COPY': ((SELECTED,), True),
66 'CREATE': ((AUTH, SELECTED), True),
67 'DELETE': ((AUTH, SELECTED), True),
68 'DELETEACL': ((AUTH, SELECTED), True),
69 'EXAMINE': ((AUTH, SELECTED), False),
70 'EXPUNGE': ((SELECTED,), True),
71 'FETCH': ((SELECTED,), True),
72 'GETACL': ((AUTH, SELECTED), True),
73 'GETANNOTATION':((AUTH, SELECTED), True),
74 'GETQUOTA': ((AUTH, SELECTED), True),
75 'GETQUOTAROOT': ((AUTH, SELECTED), True),
76 'IDLE': ((SELECTED,), False),
77 'LIST': ((AUTH, SELECTED), True),
78 'LOGIN': ((NONAUTH,), False),
79 'LOGOUT': ((NONAUTH, AUTH, LOGOUT, SELECTED), False),
80 'LSUB': ((AUTH, SELECTED), True),
81 'MYRIGHTS': ((AUTH, SELECTED), True),
82 'NAMESPACE': ((AUTH, SELECTED), True),
83 'NOOP': ((NONAUTH, AUTH, SELECTED), True),
84 'PARTIAL': ((SELECTED,), True),
85 'PROXYAUTH': ((AUTH,), False),
86 'RENAME': ((AUTH, SELECTED), True),
87 'SEARCH': ((SELECTED,), True),
88 'SELECT': ((AUTH, SELECTED), False),
89 'SETACL': ((AUTH, SELECTED), False),
90 'SETANNOTATION':((AUTH, SELECTED), True),
91 'SETQUOTA': ((AUTH, SELECTED), False),
92 'SORT': ((SELECTED,), True),
93 'STATUS': ((AUTH, SELECTED), True),
94 'STORE': ((SELECTED,), True),
95 'SUBSCRIBE': ((AUTH, SELECTED), False),
96 'THREAD': ((SELECTED,), True),
97 'UID': ((SELECTED,), True),
98 'UNSUBSCRIBE': ((AUTH, SELECTED), False),
99 }
100
101 UID_direct = ('SEARCH', 'SORT', 'THREAD')
102
103
104 def Int2AP(num):
105
106 """string = Int2AP(num)
107 Return 'num' converted to a string using characters from the set 'A'..'P'
108 """
109
110 val, a2p = [], 'ABCDEFGHIJKLMNOP'
111 num = int(abs(num))
112 while num:
113 num, mod = divmod(num, 16)
114 val.insert(0, a2p[mod])
115 return ''.join(val)
116
117
118
119 class Request(object):
120
121 """Private class to represent a request awaiting response."""
122
123 def __init__(self, parent, name=None, callback=None, cb_arg=None):
124 self.name = name
125 self.callback = callback # Function called to process result
126 self.callback_arg = cb_arg # Optional arg passed to "callback"
127
128 self.tag = '%s%s' % (parent.tagpre, parent.tagnum)
129 parent.tagnum += 1
130
131 self.ready = threading.Event()
132 self.response = None
133 self.aborted = None
134 self.data = None
135
136
137 def abort(self, typ, val):
138 self.aborted = (typ, val)
139 self.deliver(None)
140
141
142 def get_response(self, exc_fmt=None):
143 self.callback = None
144 self.ready.wait()
145
146 if self.aborted is not None:
147 typ, val = self.aborted
148 if exc_fmt is None:
149 exc_fmt = '%s - %%s' % typ
150 raise typ(exc_fmt % str(val))
151
152 return self.response
153
154
155 def deliver(self, response):
156 if self.callback is not None:
157 self.callback((response, self.callback_arg, self.aborted))
158 return
159
160 self.response = response
161 self.ready.set()
162
163
164
165
166 class IMAP4(object):
167
168 """Threaded IMAP4 client class.
169
170 Instantiate with:
171 IMAP4(host=None, port=None, debug=None, debug_file=None)
172
173 host - host's name (default: localhost);
174 port - port number (default: standard IMAP4 port);
175 debug - debug level (default: 0 - no debug);
176 debug_file - debug stream (default: sys.stderr).
177
178 All IMAP4rev1 commands are supported by methods of the same name.
179
180 Each command returns a tuple: (type, [data, ...]) where 'type'
181 is usually 'OK' or 'NO', and 'data' is either the text from the
182 tagged response, or untagged results from command. Each 'data' is
183 either a string, or a tuple. If a tuple, then the first part is the
184 header of the response, and the second part contains the data (ie:
185 'literal' value).
186
187 Errors raise the exception class <instance>.error("<reason>").
188 IMAP4 server errors raise <instance>.abort("<reason>"), which is
189 a sub-class of 'error'. Mailbox status changes from READ-WRITE to
190 READ-ONLY raise the exception class <instance>.readonly("<reason>"),
191 which is a sub-class of 'abort'.
192
193 "error" exceptions imply a program error.
194 "abort" exceptions imply the connection should be reset, and
195 the command re-tried.
196 "readonly" exceptions imply the command should be re-tried.
197
198 All commands take two optional named arguments:
199 'callback' and 'cb_arg'
200 If 'callback' is provided then the command is asynchronous, so after
201 the command is queued for transmission, the call returns immediately
202 with the tuple (None, None).
203 The result will be posted by invoking "callback" with one arg, a tuple:
204 callback((result, cb_arg, None))
205 or, if there was a problem:
206 callback((None, cb_arg, (exception class, reason)))
207
208 Otherwise the command is synchronous (waits for result). But note
209 that state-changing commands will both block until previous commands
210 have completed, and block subsequent commands until they have finished.
211
212 All (non-callback) arguments to commands are converted to strings,
213 except for AUTHENTICATE, and the last argument to APPEND which is
214 passed as an IMAP4 literal. If necessary (the string contains any
215 non-printing characters or white-space and isn't enclosed with either
216 parentheses or double quotes) each string is quoted. However, the
217 'password' argument to the LOGIN command is always quoted. If you
218 want to avoid having an argument string quoted (eg: the 'flags'
219 argument to STORE) then enclose the string in parentheses (eg:
220 "(\Deleted)").
221
222 There is one instance variable, 'state', that is useful for tracking
223 whether the client needs to login to the server. If it has the
224 value "AUTH" after instantiating the class, then the connection
225 is pre-authenticated (otherwise it will be "NONAUTH"). Selecting a
226 mailbox changes the state to be "SELECTED", closing a mailbox changes
227 back to "AUTH", and once the client has logged out, the state changes
228 to "LOGOUT" and no further commands may be issued.
229
230 Note: to use this module, you must read the RFCs pertaining to the
231 IMAP4 protocol, as the semantics of the arguments to each IMAP4
232 command are left to the invoker, not to mention the results. Also,
233 most IMAP servers implement a sub-set of the commands available here.
234
235 Note also that you must call logout() to shut down threads before
236 discarding an instance.
237 """
238
239 class error(Exception): pass # Logical errors - debug required
240 class abort(error): pass # Service errors - close and retry
241 class readonly(abort): pass # Mailbox status changed to READ-ONLY
242
243
244 continuation_cre = re.compile(r'\+( (?P<data>.*))?')
245 literal_cre = re.compile(r'.*{(?P<size>\d+)}$')
246 mapCRLF_cre = re.compile(r'\r\n|\r|\n')
247 mustquote_cre = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
248 response_code_cre = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
249 untagged_response_cre = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
250 untagged_status_cre = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
251
252
253 def __init__(self, host=None, port=None, debug=None, debug_file=None):
254
255 self.state = NONAUTH # IMAP4 protocol state
256 self.literal = None # A literal argument to a command
257 self.tagged_commands = {} # Tagged commands awaiting response
258 self.untagged_responses = {} # {typ: [data, ...], ...}
259 self.is_readonly = False # READ-ONLY desired state
260 self.idle_rqb = None # Server IDLE Request - see _IdleCont
261 self.idle_timeout = None # Must prod server occasionally
262
263 self._expecting_data = 0 # Expecting message data
264 self._accumulated_data = [] # Message data accumulated so far
265 self._literal_expected = None # Message data descriptor
266
267 # Create unique tag for this session,
268 # and compile tagged response matcher.
269
270 self.tagnum = 0
271 self.tagpre = Int2AP(random.randint(4096, 65535))
272 self.tagre = re.compile(r'(?P<tag>'
273 + self.tagpre
274 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
275
276 if __debug__: self._init_debug(debug, debug_file)
277
278 # Open socket to server.
279
280 self.open(host, port)
281
282 if __debug__:
283 if debug:
284 self._mesg('connected to %s on port %s' % (self.host, self.port))
285
286 # Threading
287
288 self.Terminate = False
289
290 self.state_change_free = threading.Event()
291 self.state_change_pending = threading.Lock()
292 self.commands_lock = threading.Lock()
293
294 self.ouq = Queue.Queue(10)
295 self.inq = Queue.Queue()
296
297 self.wrth = threading.Thread(target=self._writer)
298 self.wrth.start()
299 self.rdth = threading.Thread(target=self._reader)
300 self.rdth.start()
301 self.inth = threading.Thread(target=self._handler)
302 self.inth.start()
303
304 # Get server welcome message,
305 # request and store CAPABILITY response.
306
307 try:
308 self.welcome = self._request_push(tag='continuation').get_response('IMAP4 protocol error: %s')[1]
309
310 if 'PREAUTH' in self.untagged_responses:
311 self.state = AUTH
312 if __debug__: self._log(1, 'state => AUTH')
313 elif 'OK' in self.untagged_responses:
314 if __debug__: self._log(1, 'state => NONAUTH')
315 else:
316 raise self.error(self.welcome)
317
318 typ, dat = self.capability()
319 if dat == [None]:
320 raise self.error('no CAPABILITY response from server')
321 self.capabilities = tuple(dat[-1].upper().split())
322 if __debug__: self._log(3, 'CAPABILITY: %r' % (self.capabilities,))
323
324 for version in AllowedVersions:
325 if not version in self.capabilities:
326 continue
327 self.PROTOCOL_VERSION = version
328 break
329 else:
330 raise self.error('server not IMAP4 compliant')
331 except:
332 self._close_threads()
333 raise
334
335
336 def __getattr__(self, attr):
337 # Allow UPPERCASE variants of IMAP4 command methods.
338 if attr in Commands:
339 return getattr(self, attr.lower())
340 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
341
342
343
344 # Overridable methods
345
346
347 def open(self, host=None, port=None):
348 """open(host=None, port=None)
349 Setup connection to remote server on "host:port"
350 (default: localhost:standard IMAP4 port).
351 This connection will be used by the routines:
352 read, send, shutdown, socket."""
353
354 self.host = host is not None and host or ''
355 self.port = port is not None and port or IMAP4_PORT
356 self.sock = self.open_socket()
357 self.read_fd = self.sock.fileno()
358
359
360 def open_socket(self):
361 """Open socket choosing first address family available."""
362
363 msg = (-1, 'could not open socket')
364 for res in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM):
365 af, socktype, proto, canonname, sa = res
366 try:
367 s = socket.socket(af, socktype, proto)
368 except socket.error, msg:
369 continue
370 try:
371 s.connect(sa)
372 except socket.error, msg:
373 s.close()
374 continue
375 break
376 else:
377 raise socket.error(msg)
378
379 return s
380
381
382 def read(self, size):
383 """data = read(size)
384 Read at most 'size' bytes from remote."""
385
386 return self.sock.recv(size)
387
388
389 def send(self, data):
390 """send(data)
391 Send 'data' to remote."""
392
393 self.sock.sendall(data)
394
395
396 def shutdown(self):
397 """shutdown()
398 Close I/O established in "open"."""
399
400 self.sock.close()
401
402
403 def socket(self):
404 """socket = socket()
405 Return socket instance used to connect to IMAP4 server."""
406
407 return self.sock
408
409
410
411 # Utility methods
412
413
414 def recent(self, **kw):
415 """(typ, [data]) = recent()
416 Return most recent 'RECENT' responses if any exist,
417 else prompt server for an update using the 'NOOP' command.
418 'data' is None if no new messages,
419 else list of RECENT responses, most recent last."""
420
421 name = 'RECENT'
422 typ, dat = self._untagged_response('OK', [None], name)
423 if dat[-1]:
424 return self._deliver_dat(typ, dat, kw)
425 kw['untagged_response'] = name
426 return self.noop(**kw) # Prod server for response
427
428
429 def response(self, code, **kw):
430 """(code, [data]) = response(code)
431 Return data for response 'code' if received, or None.
432 Old value for response 'code' is cleared."""
433
434 typ, dat = self._untagged_response(code, [None], code.upper())
435 return self._deliver_dat(typ, dat, kw)
436
437
438
439
440 # IMAP4 commands
441
442
443 def append(self, mailbox, flags, date_time, message, **kw):
444 """(typ, [data]) = append(mailbox, flags, date_time, message)
445 Append message to named mailbox.
446 All args except `message' can be None."""
447
448 name = 'APPEND'
449 if not mailbox:
450 mailbox = 'INBOX'
451 if flags:
452 if (flags[0],flags[-1]) != ('(',')'):
453 flags = '(%s)' % flags
454 else:
455 flags = None
456 if date_time:
457 date_time = Time2Internaldate(date_time)
458 else:
459 date_time = None
460 self.literal = self.mapCRLF_cre.sub(CRLF, message)
461 try:
462 return self._simple_command(name, mailbox, flags, date_time, **kw)
463 finally:
464 self.state_change_pending.release()
465
466
467 def authenticate(self, mechanism, authobject, **kw):
468 """(typ, [data]) = authenticate(mechanism, authobject)
469 Authenticate command - requires response processing.
470
471 'mechanism' specifies which authentication mechanism is to
472 be used - it must appear in <instance>.capabilities in the
473 form AUTH=<mechanism>.
474
475 'authobject' must be a callable object:
476
477 data = authobject(response)
478
479 It will be called to process server continuation responses.
480 It should return data that will be encoded and sent to server.
481 It should return None if the client abort response '*' should
482 be sent instead."""
483
484 self.literal = _Authenticator(authobject).process
485 try:
486 typ, dat = self._simple_command('AUTHENTICATE', mechanism.upper())
487 if typ != 'OK':
488 self._deliver_exc(self.error, dat[-1])
489 self.state = AUTH
490 if __debug__: self._log(1, 'state => AUTH')
491 finally:
492 self.state_change_pending.release()
493 return self._deliver_dat(typ, dat, kw)
494
495
496 def capability(self, **kw):
497 """(typ, [data]) = capability()
498 Fetch capabilities list from server."""
499
500 name = 'CAPABILITY'
501 kw['untagged_response'] = name
502 return self._simple_command(name, **kw)
503
504
505 def check(self, **kw):
506 """(typ, [data]) = check()
507 Checkpoint mailbox on server."""
508
509 return self._simple_command('CHECK', **kw)
510
511
512 def close(self, **kw):
513 """(typ, [data]) = close()
514 Close currently selected mailbox.
515
516 Deleted messages are removed from writable mailbox.
517 This is the recommended command before 'LOGOUT'."""
518
519 if self.state != 'SELECTED':
520 raise self.error('No mailbox selected.')
521 try:
522 typ, dat = self._simple_command('CLOSE')
523 finally:
524 self.state = AUTH
525 if __debug__: self._log(1, 'state => AUTH')
526 self.state_change_pending.release()
527 return self._deliver_dat(typ, dat, kw)
528
529
530 def copy(self, message_set, new_mailbox, **kw):
531 """(typ, [data]) = copy(message_set, new_mailbox)
532 Copy 'message_set' messages onto end of 'new_mailbox'."""
533
534 return self._simple_command('COPY', message_set, new_mailbox, **kw)
535
536
537 def create(self, mailbox, **kw):
538 """(typ, [data]) = create(mailbox)
539 Create new mailbox."""
540
541 return self._simple_command('CREATE', mailbox, **kw)
542
543
544 def delete(self, mailbox, **kw):
545 """(typ, [data]) = delete(mailbox)
546 Delete old mailbox."""
547
548 return self._simple_command('DELETE', mailbox, **kw)
549
550
551 def deleteacl(self, mailbox, who, **kw):
552 """(typ, [data]) = deleteacl(mailbox, who)
553 Delete the ACLs (remove any rights) set for who on mailbox."""
554
555 return self._simple_command('DELETEACL', mailbox, who, **kw)
556
557
558 def examine(self, mailbox='INBOX', **kw):
559 """(typ, [data]) = examine(mailbox='INBOX', readonly=False)
560 Select a mailbox for READ-ONLY access. (Flushes all untagged responses.)
561 'data' is count of messages in mailbox ('EXISTS' response).
562 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
563 other responses should be obtained via "response('FLAGS')" etc."""
564
565 return self.select(mailbox=mailbox, readonly=True, **kw)
566
567
568 def expunge(self, **kw):
569 """(typ, [data]) = expunge()
570 Permanently remove deleted items from selected mailbox.
571 Generates 'EXPUNGE' response for each deleted message.
572 'data' is list of 'EXPUNGE'd message numbers in order received."""
573
574 name = 'EXPUNGE'
575 kw['untagged_response'] = name
576 return self._simple_command(name, **kw)
577
578
579 def fetch(self, message_set, message_parts, **kw):
580 """(typ, [data, ...]) = fetch(message_set, message_parts)
581 Fetch (parts of) messages.
582 'message_parts' should be a string of selected parts
583 enclosed in parentheses, eg: "(UID BODY[TEXT])".
584 'data' are tuples of message part envelope and data,
585 followed by a string containing the trailer."""
586
587 name = 'FETCH'
588 kw['untagged_response'] = name
589 return self._simple_command(name, message_set, message_parts, **kw)
590
591
592 def getacl(self, mailbox, **kw):
593 """(typ, [data]) = getacl(mailbox)
594 Get the ACLs for a mailbox."""
595
596 kw['untagged_response'] = 'ACL'
597 return self._simple_command('GETACL', mailbox, **kw)
598
599
600 def getannotation(self, mailbox, entry, attribute, **kw):
601 """(typ, [data]) = getannotation(mailbox, entry, attribute)
602 Retrieve ANNOTATIONs."""
603
604 kw['untagged_response'] = 'ANNOTATION'
605 return self._simple_command('GETANNOTATION', mailbox, entry, attribute, **kw)
606
607
608 def getquota(self, root, **kw):
609 """(typ, [data]) = getquota(root)
610 Get the quota root's resource usage and limits.
611 (Part of the IMAP4 QUOTA extension defined in rfc2087.)"""
612
613 kw['untagged_response'] = 'QUOTA'
614 return self._simple_command('GETQUOTA', root, **kw)
615
616
617 def getquotaroot(self, mailbox, **kw):
618 # Hmmm, this is non-std! Left for backwards-compatibility, sigh.
619 # NB: usage should have been defined as:
620 # (typ, [QUOTAROOT responses...]) = getquotaroot(mailbox)
621 # (typ, [QUOTA responses...]) = response('QUOTA')
622 """(typ, [[QUOTAROOT responses...], [QUOTA responses...]]) = getquotaroot(mailbox)
623 Get the list of quota roots for the named mailbox."""
624
625 typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
626 typ, quota = self._untagged_response(typ, dat, 'QUOTA')
627 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
628 return self._deliver_dat(typ, [quotaroot, quota], kw)
629
630
631 def idle(self, timeout=None, **kw):
632 """"(typ, [data]) = idle(timeout=None)
633 Put server into IDLE mode until server notifies some change,
634 or 'timeout' (secs) occurs (default: 29 minutes),
635 or another IMAP4 command is scheduled."""
636
637 name = 'IDLE'
638 self.literal = _IdleCont(self, timeout).process
639 try:
640 return self._simple_command(name, **kw)
641 finally:
642 self.state_change_pending.release()
643
644
645 def list(self, directory='""', pattern='*', **kw):
646 """(typ, [data]) = list(directory='""', pattern='*')
647 List mailbox names in directory matching pattern.
648 'data' is list of LIST responses.
649
650 NB: for 'pattern':
651 % matches all except separator ( so LIST "" "%" returns names at root)
652 * matches all (so LIST "" "*" returns whole directory tree from root)"""
653
654 name = 'LIST'
655 kw['untagged_response'] = name
656 return self._simple_command(name, directory, pattern, **kw)
657
658
659 def login(self, user, password, **kw):
660 """(typ, [data]) = login(user, password)
661 Identify client using plaintext password.
662 NB: 'password' will be quoted."""
663
664 try:
665 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
666 if typ != 'OK':
667 self._deliver_exc(self.error, dat[-1], kw)
668 self.state = AUTH
669 if __debug__: self._log(1, 'state => AUTH')
670 finally:
671 self.state_change_pending.release()
672 return self._deliver_dat(typ, dat, kw)
673
674
675 def login_cram_md5(self, user, password, **kw):
676 """(typ, [data]) = login_cram_md5(user, password)
677 Force use of CRAM-MD5 authentication."""
678
679 self.user, self.password = user, password
680 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH, **kw)
681
682
683 def _CRAM_MD5_AUTH(self, challenge):
684 """Authobject to use with CRAM-MD5 authentication."""
685 import hmac
686 return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
687
688
689 def logout(self, **kw):
690 """(typ, [data]) = logout()
691 Shutdown connection to server.
692 Returns server 'BYE' response."""
693
694 self.state = LOGOUT
695 if __debug__: self._log(1, 'state => LOGOUT')
696
697 try:
698 typ, dat = self._simple_command('LOGOUT')
699 except:
700 typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
701 if __debug__: self._log(1, dat)
702
703 self._close_threads()
704
705 self.state_change_pending.release()
706
707 if __debug__: self._log(1, 'connection closed')
708
709 bye = self.untagged_responses.get('BYE')
710 if bye:
711 typ, dat = 'BYE', bye
712 return self._deliver_dat(typ, dat, kw)
713
714
715 def lsub(self, directory='""', pattern='*', **kw):
716 """(typ, [data, ...]) = lsub(directory='""', pattern='*')
717 List 'subscribed' mailbox names in directory matching pattern.
718 'data' are tuples of message part envelope and data."""
719
720 name = 'LSUB'
721 kw['untagged_response'] = name
722 return self._simple_command(name, directory, pattern, **kw)
723
724
725 def myrights(self, mailbox):
726 """(typ, [data]) = myrights(mailbox)
727 Show my ACLs for a mailbox (i.e. the rights that I have on mailbox)."""
728
729 name = 'MYRIGHTS'
730 kw['untagged_response'] = name
731 return self._simple_command(name, mailbox, **kw)
732
733
734 def namespace(self, **kw):
735 """(typ, [data, ...]) = namespace()
736 Returns IMAP namespaces ala rfc2342."""
737
738 name = 'NAMESPACE'
739 kw['untagged_response'] = name
740 return self._simple_command(name, **kw)
741
742
743 def noop(self, **kw):
744 """(typ, [data]) = noop()
745 Send NOOP command."""
746
747 if __debug__: self._dump_ur(3)
748 return self._simple_command('NOOP', **kw)
749
750
751 def partial(self, message_num, message_part, start, length, **kw):
752 """(typ, [data, ...]) = partial(message_num, message_part, start, length)
753 Fetch truncated part of a message.
754 'data' is tuple of message part envelope and data.
755 NB: obsolete."""
756
757 name = 'PARTIAL'
758 kw['untagged_response'] = 'FETCH'
759 return self._simple_command(name, message_num, message_part, start, length, **kw)
760
761
762 def proxyauth(self, user, **kw):
763 """(typ, [data]) = proxyauth(user)
764 Assume authentication as 'user'.
765 (Allows an authorised administrator to proxy into any user's mailbox.)"""
766
767 try:
768 return self._simple_command('PROXYAUTH', user, **kw)
769 finally:
770 self.state_change_pending.release()
771
772
773 def rename(self, oldmailbox, newmailbox, **kw):
774 """(typ, [data]) = rename(oldmailbox, newmailbox)
775 Rename old mailbox name to new."""
776
777 return self._simple_command('RENAME', oldmailbox, newmailbox, **kw)
778
779
780 def search(self, charset, *criteria, **kw):
781 """(typ, [data]) = search(charset, criterion, ...)
782 Search mailbox for matching messages.
783 'data' is space separated list of matching message numbers."""
784
785 name = 'SEARCH'
786 kw['untagged_response'] = name
787 if charset:
788 return self._simple_command(name, 'CHARSET', charset, *criteria, **kw)
789 return self._simple_command(name, *criteria, **kw)
790
791
792 def select(self, mailbox='INBOX', readonly=False, **kw):
793 """(typ, [data]) = select(mailbox='INBOX', readonly=False)
794 Select a mailbox. (Flushes all untagged responses.)
795 'data' is count of messages in mailbox ('EXISTS' response).
796 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
797 other responses should be obtained via "response('FLAGS')" etc."""
798
799 self.commands_lock.acquire()
800 self.untagged_responses = {} # Flush old responses.
801 self.commands_lock.release()
802
803 self.is_readonly = readonly and True or False
804 if readonly:
805 name = 'EXAMINE'
806 else:
807 name = 'SELECT'
808 try:
809 rqb = self._command(name, mailbox)
810 typ, dat = rqb.get_response('command: %s => %%s' % rqb.name)
811 if typ != 'OK':
812 if self.state == SELECTED:
813 self.state = AUTH
814 if __debug__: self._log(1, 'state => AUTH')
815 if typ == 'BAD':
816 self._deliver_exc(self.error, '%s command error: %s %s' % (name, typ, dat), kw)
817 return self._deliver_dat(typ, dat, kw)
818 self.state = SELECTED
819 if __debug__: self._log(1, 'state => SELECTED')
820 finally:
821 self.state_change_pending.release()
822 if 'READ-ONLY' in self.untagged_responses and not readonly:
823 if __debug__: self._dump_ur(1)
824 self._deliver_exc(self.readonly, '%s is not writable' % mailbox, kw)
825 return self._deliver_dat(typ, self.untagged_responses.get('EXISTS', [None]), kw)
826
827
828 def setacl(self, mailbox, who, what, **kw):
829 """(typ, [data]) = setacl(mailbox, who, what)
830 Set a mailbox acl."""
831
832 try:
833 return self._simple_command('SETACL', mailbox, who, what, **kw)
834 finally:
835 self.state_change_pending.release()
836
837
838 def setannotation(self, *args, **kw):
839 """(typ, [data]) = setannotation(mailbox[, entry, attribute]+)
840 Set ANNOTATIONs."""
841
842 kw['untagged_response'] = 'ANNOTATION'
843 return self._simple_command('SETANNOTATION', *args, **kw)
844
845
846 def setquota(self, root, limits, **kw):
847 """(typ, [data]) = setquota(root, limits)
848 Set the quota root's resource limits."""
849
850 kw['untagged_response'] = 'QUOTA'
851 try:
852 return self._simple_command('SETQUOTA', root, limits, **kw)
853 finally:
854 self.state_change_pending.release()
855
856
857 def sort(self, sort_criteria, charset, *search_criteria, **kw):
858 """(typ, [data]) = sort(sort_criteria, charset, search_criteria, ...)
859 IMAP4rev1 extension SORT command."""
860
861 name = 'SORT'
862 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
863 sort_criteria = '(%s)' % sort_criteria
864 kw['untagged_response'] = name
865 return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw)
866
867
868 def status(self, mailbox, names, **kw):
869 """(typ, [data]) = status(mailbox, names)
870 Request named status conditions for mailbox."""
871
872 name = 'STATUS'
873 kw['untagged_response'] = name
874 return self._simple_command(name, mailbox, names, **kw)
875
876
877 def store(self, message_set, command, flags, **kw):
878 """(typ, [data]) = store(message_set, command, flags)
879 Alters flag dispositions for messages in mailbox."""
880
881 if (flags[0],flags[-1]) != ('(',')'):
882 flags = '(%s)' % flags # Avoid quoting the flags
883 kw['untagged_response'] = 'FETCH'
884 return self._simple_command('STORE', message_set, command, flags, **kw)
885
886
887 def subscribe(self, mailbox, **kw):
888 """(typ, [data]) = subscribe(mailbox)
889 Subscribe to new mailbox."""
890
891 try:
892 return self._simple_command('SUBSCRIBE', mailbox, **kw)
893 finally:
894 self.state_change_pending.release()
895
896
897 def thread(self, threading_algorithm, charset, *search_criteria, **kw):
898 """(type, [data]) = thread(threading_alogrithm, charset, search_criteria, ...)
899 IMAPrev1 extension THREAD command."""
900
901 name = 'THREAD'
902 kw['untagged_response'] = name
903 return self._simple_command(name, threading_algorithm, charset, *search_criteria, **kw)
904
905
906 def uid(self, command, *args, **kw):
907 """(typ, [data]) = uid(command, arg, ...)
908 Execute "command arg ..." with messages identified by UID,
909 rather than message number.
910 Assumes 'command' is legal in current state.
911 Returns response appropriate to 'command'."""
912
913 command = command.upper()
914 if command in UID_direct:
915 resp = command
916 else:
917 resp = 'FETCH'
918 kw['untagged_response'] = resp
919 return self._simple_command('UID', command, *args, **kw)
920
921
922 def unsubscribe(self, mailbox, **kw):
923 """(typ, [data]) = unsubscribe(mailbox)
924 Unsubscribe from old mailbox."""
925
926 try:
927 return self._simple_command('UNSUBSCRIBE', mailbox, **kw)
928 finally:
929 self.state_change_pending.release()
930
931
932 def xatom(self, name, *args, **kw):
933 """(typ, [data]) = xatom(name, arg, ...)
934 Allow simple extension commands notified by server in CAPABILITY response.
935 Assumes extension command 'name' is legal in current state.
936 Returns response appropriate to extension command 'name'."""
937
938 name = name.upper()
939 if not name in Commands:
940 Commands[name] = ((self.state,), False)
941 try:
942 return self._simple_command(name, *args, **kw)
943 finally:
944 if self.state_change_pending.locked():
945 self.state_change_pending.release()
946
947
948
949 # Internal methods
950
951
952 def _append_untagged(self, typ, dat):
953
954 if dat is None: dat = ''
955
956 self.commands_lock.acquire()
957 ur = self.untagged_responses.setdefault(typ, [])
958 ur.append(dat)
959 self.commands_lock.release()
960
961 if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%s"]' % (typ, len(ur)-1, dat))
962
963
964 def _check_bye(self):
965
966 bye = self.untagged_responses.get('BYE')
967 if bye:
968 raise self.abort(bye[-1])
969
970
971 def _checkquote(self, arg):
972
973 # Must quote command args if non-alphanumeric chars present,
974 # and not already quoted.
975
976 if not isinstance(arg, basestring):
977 return arg
978 if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
979 return arg
980 if arg and self.mustquote_cre.search(arg) is None:
981 return arg
982 return self._quote(arg)
983
984
985 def _command(self, name, *args, **kw):
986
987 if Commands[name][CMD_VAL_ASYNC]:
988 cmdtyp = 'async'
989 else:
990 cmdtyp = 'sync'
991
992 if __debug__: self._log(1, '[%s] %s %s' % (cmdtyp, name, args))
993
994 self.state_change_pending.acquire()
995
996 self._end_idle()
997
998 if cmdtyp == 'async':
999 self.state_change_pending.release()
1000 else:
1001 # Need to wait for all async commands to complete
1002 self._check_bye()
1003 self.commands_lock.acquire()
1004 if self.tagged_commands:
1005 self.state_change_free.clear()
1006 need_event = True
1007 else:
1008 need_event = False
1009 self.commands_lock.release()
1010 if need_event:
1011 if __debug__: self._log(4, 'sync command %s waiting for empty commands Q' % name)
1012 self.state_change_free.wait()
1013 if __debug__: self._log(4, 'sync command %s proceeding' % name)
1014
1015 if self.state not in Commands[name][CMD_VAL_STATES]:
1016 self.literal = None
1017 raise self.error('command %s illegal in state %s'
1018 % (name, self.state))
1019
1020 self._check_bye()
1021
1022 self.commands_lock.acquire()
1023 for typ in ('OK', 'NO', 'BAD'):
1024 if typ in self.untagged_responses:
1025 del self.untagged_responses[typ]
1026 self.commands_lock.release()
1027
1028 if 'READ-ONLY' in self.untagged_responses \
1029 and not self.is_readonly:
1030 self.literal = None
1031 raise self.readonly('mailbox status changed to READ-ONLY')
1032
1033 if self.Terminate:
1034 raise self.abort('connection closed')
1035
1036 rqb = self._request_push(name=name, **kw)
1037
1038 data = '%s %s' % (rqb.tag, name)
1039 for arg in args:
1040 if arg is None: continue
1041 data = '%s %s' % (data, self._checkquote(arg))
1042
1043 literal = self.literal
1044 if literal is not None:
1045 self.literal = None
1046 if isinstance(literal, str):
1047 literator = None
1048 data = '%s {%s}' % (data, len(literal))
1049 else:
1050 literator = literal
1051
1052 rqb.data = '%s%s' % (data, CRLF)
1053 self.ouq.put(rqb)
1054
1055 if literal is None:
1056 return rqb
1057
1058 crqb = self._request_push(tag='continuation')
1059
1060 while True:
1061 # Wait for continuation response
1062
1063 ok, data = crqb.get_response('command: %s => %%s' % name)
1064 if __debug__: self._log(3, 'continuation => %s, %s' % (ok, data))
1065
1066 # NO/BAD response?
1067
1068 if not ok:
1069 break
1070
1071 # Send literal
1072
1073 if literator is not None:
1074 literal = literator(data, rqb)
1075
1076 if literal is None:
1077 break
1078
1079 if __debug__: self._log(4, 'write literal size %s' % len(literal))
1080 crqb.data = '%s%s' % (literal, CRLF)
1081 self.ouq.put(crqb)
1082
1083 if literator is None:
1084 break
1085
1086 self.commands_lock.acquire()
1087 self.tagged_commands['continuation'] = crqb
1088 self.commands_lock.release()
1089
1090 return rqb
1091
1092
1093 def _command_complete(self, rqb, kw):
1094
1095 # Called for non-callback commands
1096
1097 typ, dat = rqb.get_response('command: %s => %%s' % rqb.name)
1098 self._check_bye()
1099 if typ == 'BAD':
1100 if __debug__: self._print_log()
1101 raise self.error('%s command error: %s %s' % (rqb.name, typ, dat))
1102 if 'untagged_response' in kw:
1103 return self._untagged_response(typ, dat, kw['untagged_response'])
1104 return typ, dat
1105
1106
1107 def _command_completer(self, (response, cb_arg, error)):
1108
1109 # Called for callback commands
1110 rqb, kw = cb_arg
1111 rqb.callback = kw['callback']
1112 rqb.callback_arg = kw.get('cb_arg')
1113 if error is not None:
1114 if __debug__: self._print_log()
1115 typ, val = error
1116 rqb.abort(typ, val)
1117 return
1118 bye = self.untagged_responses.get('BYE')
1119 if bye:
1120 rqb.abort(self.abort, bye[-1])
1121 return
1122 typ, dat = response
1123 if typ == 'BAD':
1124 if __debug__: self._print_log()
1125 rqb.abort(self.error, '%s command error: %s %s' % (rqb.name, typ, dat))
1126 return
1127 if 'untagged_response' in kw:
1128 rqb.deliver(self._untagged_response(typ, dat, kw['untagged_response']))
1129 else:
1130 rqb.deliver(response)
1131
1132
1133 def _deliver_dat(self, typ, dat, kw):
1134
1135 if 'callback' in kw:
1136 kw['callback'](((typ, dat), kw.get('cb_arg'), None))
1137 return typ, dat
1138
1139
1140 def _deliver_exc(self, exc, dat, kw):
1141
1142 if 'callback' in kw:
1143 kw['callback']((None, kw.get('cb_arg'), (exc, dat)))
1144 raise exc(dat)
1145
1146
1147 def _end_idle(self):
1148
1149 irqb = self.idle_rqb
1150 if irqb is not None:
1151 self.idle_rqb = None
1152 self.idle_timeout = None
1153 irqb.data = 'DONE%s' % CRLF
1154 self.ouq.put(irqb)
1155 if __debug__: self._log(2, 'server IDLE finished')
1156
1157
1158 def _match(self, cre, s):
1159
1160 # Run compiled regular expression 'cre' match method on 's'.
1161 # Save result, return success.
1162
1163 self.mo = cre.match(s)
1164 return self.mo is not None
1165
1166
1167 def _put_response(self, resp):
1168
1169 if self._expecting_data > 0:
1170 rlen = len(resp)
1171 dlen = min(self._expecting_data, rlen)
1172 self._expecting_data -= dlen
1173 if rlen <= dlen:
1174 self._accumulated_data.append(resp)
1175 return
1176 self._accumulated_data.append(resp[:dlen])
1177 resp = resp[dlen:]
1178
1179 if self._accumulated_data:
1180 typ, dat = self._literal_expected
1181 self._append_untagged(typ, (dat, ''.join(self._accumulated_data)))
1182 self._accumulated_data = []
1183
1184 # Protocol mandates all lines terminated by CRLF
1185 resp = resp[:-2]
1186
1187 if 'continuation' in self.tagged_commands:
1188 continuation_expected = True
1189 else:
1190 continuation_expected = False
1191
1192 if self._literal_expected is not None:
1193 dat = resp
1194 if self._match(self.literal_cre, dat):
1195 self._literal_expected[1] = dat
1196 self._expecting_data = int(self.mo.group('size'))
1197 if __debug__: self._log(4, 'expecting literal size %s' % self._expecting_data)
1198 return
1199 typ = self._literal_expected[0]
1200 self._literal_expected = None
1201 self._append_untagged(typ, dat) # Tail
1202 if __debug__: self._log(4, 'literal completed')
1203 else:
1204 # Command completion response?
1205 if self._match(self.tagre, resp):
1206 tag = self.mo.group('tag')
1207 typ = self.mo.group('type')
1208 dat = self.mo.group('data')
1209 if not tag in self.tagged_commands:
1210 if __debug__: self._log(1, 'unexpected tagged response: %s' % resp)
1211 else:
1212 self._request_pop(tag, (typ, [dat]))
1213 else:
1214 dat2 = None
1215
1216 # '*' (untagged) responses?
1217
1218 if not self._match(self.untagged_response_cre, resp):
1219 if self._match(self.untagged_status_cre, resp):
1220 dat2 = self.mo.group('data2')
1221
1222 if self.mo is None:
1223 # Only other possibility is '+' (continuation) response...
1224
1225 if self._match(self.continuation_cre, resp):
1226 if not continuation_expected:
1227 if __debug__: self._log(1, "unexpected continuation response: '%s'" % resp)
1228 return
1229 self._request_pop('continuation', (True, self.mo.group('data')))
1230 return
1231
1232 if __debug__: self._log(1, "unexpected response: '%s'" % resp)
1233 return
1234
1235 typ = self.mo.group('type')
1236 dat = self.mo.group('data')
1237 if dat is None: dat = '' # Null untagged response
1238 if dat2: dat = dat + ' ' + dat2
1239
1240 # Is there a literal to come?
1241
1242 if self._match(self.literal_cre, dat):
1243 self._expecting_data = int(self.mo.group('size'))
1244 if __debug__: self._log(4, 'read literal size %s' % self._expecting_data)
1245 self._literal_expected = [typ, dat]
1246 return
1247
1248 self._append_untagged(typ, dat)
1249
1250 if typ != 'OK':
1251 self._end_idle()
1252
1253 # Bracketed response information?
1254
1255 if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat):
1256 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
1257
1258 # Command waiting for aborted continuation response?
1259
1260 if continuation_expected:
1261 self._request_pop('continuation', (False, resp))
1262
1263 # Bad news?
1264
1265 if typ in ('NO', 'BAD', 'BYE'):
1266 if typ == 'BYE':
1267 self.Terminate = True
1268 if __debug__: self._log(1, '%s response: %s' % (typ, dat))
1269
1270
1271 def _quote(self, arg):
1272
1273 return '"%s"' % arg.replace('\\', '\\\\').replace('"', '\\"')
1274
1275
1276 def _request_pop(self, name, data):
1277
1278 if __debug__: self._log(4, '_request_pop(%s, %s)' % (name, data))
1279 self.commands_lock.acquire()
1280 rqb = self.tagged_commands.pop(name)
1281 if not self.tagged_commands:
1282 self.state_change_free.set()
1283 self.commands_lock.release()
1284 rqb.deliver(data)
1285
1286
1287 def _request_push(self, tag=None, name=None, **kw):
1288
1289 self.commands_lock.acquire()
1290 rqb = Request(self, name=name, **kw)
1291 if tag is None:
1292 tag = rqb.tag
1293 self.tagged_commands[tag] = rqb
1294 self.commands_lock.release()
1295 if __debug__: self._log(4, '_request_push(%s, %s, %s)' % (tag, name, `kw`))
1296 return rqb
1297
1298
1299 def _simple_command(self, name, *args, **kw):
1300
1301 if 'callback' in kw:
1302 rqb = self._command(name, callback=self._command_completer, *args)
1303 rqb.callback_arg = (rqb, kw)
1304 return (None, None)
1305 return self._command_complete(self._command(name, *args), kw)
1306
1307
1308 def _untagged_response(self, typ, dat, name):
1309
1310 if typ == 'NO':
1311 return typ, dat
1312 if not name in self.untagged_responses:
1313 return typ, [None]
1314 self.commands_lock.acquire()
1315 data = self.untagged_responses.pop(name)
1316 self.commands_lock.release()
1317 if __debug__: self._log(5, 'pop untagged_responses[%s] => %s' % (name, (typ, data)))
1318 return typ, data
1319
1320
1321
1322 # Threads
1323
1324
1325 def _close_threads(self):
1326
1327 self.ouq.put(None)
1328 self.wrth.join()
1329
1330 self.shutdown()
1331
1332 self.rdth.join()
1333 self.inth.join()
1334
1335
1336 def _handler(self):
1337
1338 threading.currentThread().setName('hdlr')
1339
1340 time.sleep(0.1) # Don't start handling before main thread ready
1341
1342 if __debug__: self._log(1, 'starting')
1343
1344 typ, val = self.abort, 'connection terminated'
1345
1346 while not self.Terminate:
1347 try:
1348 if self.idle_timeout is not None:
1349 timeout = self.idle_timeout - time.time()
1350 if timeout <= 0:
1351 timeout = 1
1352 if __debug__:
1353 if self.idle_rqb is not None:
1354 self._log(5, 'server IDLING, timeout=%.2f' % timeout)
1355 else:
1356 timeout = None
1357 line = self.inq.get(True, timeout)
1358 except Queue.Empty:
1359 if self.idle_rqb is None:
1360 continue
1361 if self.idle_timeout > time.time():
1362 continue
1363 if __debug__: self._log(2, 'server IDLE timedout')
1364 line = IDLE_TIMEOUT_RESPONSE
1365
1366 if line is None:
1367 break
1368
1369 if not isinstance(line, str):
1370 typ, val = line
1371 break
1372
1373 try:
1374 self._put_response(line)
1375 except:
1376 typ, val = self.error, 'program error: %s - %s' % sys.exc_info()[:2]
1377 break
1378
1379 self.Terminate = True
1380
1381 while not self.ouq.empty():
1382 try:
1383 self.ouq.get_nowait().abort(typ, val)
1384 except Queue.Empty:
1385 break
1386 self.ouq.put(None)
1387
1388 self.commands_lock.acquire()
1389 for name in self.tagged_commands.keys():
1390 rqb = self.tagged_commands.pop(name)
1391 rqb.abort(typ, val)
1392 self.state_change_free.set()
1393 self.commands_lock.release()
1394
1395 if __debug__: self._log(1, 'finished')
1396
1397
1398 if hasattr(select_module, "poll"):
1399
1400 def _reader(self):
1401
1402 threading.currentThread().setName('redr')
1403
1404 if __debug__: self._log(1, 'starting using poll')
1405
1406 def poll_error(state):
1407 PollErrors = {
1408 select.POLLERR: 'Error',
1409 select.POLLHUP: 'Hang up',
1410 select.POLLNVAL: 'Invalid request: descriptor not open',
1411 }
1412 return ' '.join([PollErrors[s] for s in PollErrors.keys() if (s & state)])
1413
1414 line_part = ''
1415
1416 poll = select.poll()
1417
1418 poll.register(self.read_fd, select.POLLIN)
1419
1420 while not self.Terminate:
1421 if self.state == LOGOUT:
1422 timeout = 1
1423 else:
1424 timeout = None
1425 try:
1426 r = poll.poll(timeout)
1427 if __debug__: self._log(5, 'poll => %s' % `r`)
1428 if not r:
1429 continue # Timeout
1430
1431 fd,state = r[0]
1432
1433 if state & select.POLLIN:
1434 data = self.read(32768) # Drain ssl buffer if present
1435 start = 0
1436 dlen = len(data)
1437 if __debug__: self._log(5, 'rcvd %s' % dlen)
1438 if dlen == 0:
1439 time.sleep(0.1)
1440 while True:
1441 stop = data.find('\n', start)
1442 if stop < 0:
1443 line_part += data[start:]
1444 break
1445 stop += 1
1446 line_part, start, line = \
1447 '', stop, line_part + data[start:stop]
1448 if __debug__: self._log(4, '< %s' % line)
1449 self.inq.put(line)
1450
1451 if state & ~(select.POLLIN):
1452 raise IOError(poll_error(state))
1453 except:
1454 reason = 'socket error: %s - %s' % sys.exc_info()[:2]
1455 if __debug__:
1456 if not self.Terminate:
1457 self._print_log()
1458 if self.debug: self.debug += 4 # Output all
1459 self._log(1, reason)
1460 self.inq.put((self.abort, reason))
1461 break
1462
1463 poll.unregister(self.read_fd)
1464
1465 if __debug__: self._log(1, 'finished')
1466
1467 else:
1468
1469 # No "poll" - use select()
1470
1471 def _reader(self):
1472
1473 threading.currentThread().setName('redr')
1474
1475 if __debug__: self._log(1, 'starting using select')
1476
1477 line_part = ''
1478
1479 while not self.Terminate:
1480 if self.state == LOGOUT:
1481 timeout = 1
1482 else:
1483 timeout = None
1484 try:
1485 r,w,e = select.select([self.read_fd], [], [], timeout)
1486 if __debug__: self._log(5, 'select => %s, %s, %s' % (r,w,e))
1487 if not r: # Timeout
1488 continue
1489
1490 data = self.read(32768) # Drain ssl buffer if present
1491 start = 0
1492 dlen = len(data)
1493 if __debug__: self._log(5, 'rcvd %s' % dlen)
1494 if dlen == 0:
1495 time.sleep(0.1)
1496 while True:
1497 stop = data.find('\n', start)
1498 if stop < 0:
1499 line_part += data[start:]
1500 break
1501 stop += 1
1502 line_part, start, line = \
1503 '', stop, line_part + data[start:stop]
1504 if __debug__: self._log(4, '< %s' % line)
1505 self.inq.put(line)
1506 except:
1507 reason = 'socket error: %s - %s' % sys.exc_info()[:2]
1508 if __debug__:
1509 if not self.Terminate:
1510 self._print_log()
1511 if self.debug: self.debug += 4 # Output all
1512 self._log(1, reason)
1513 self.inq.put((self.abort, reason))
1514 break
1515
1516 if __debug__: self._log(1, 'finished')
1517
1518
1519 def _writer(self):
1520
1521 threading.currentThread().setName('wrtr')
1522
1523 if __debug__: self._log(1, 'starting')
1524
1525 reason = 'Terminated'
1526
1527 while not self.Terminate:
1528 rqb = self.ouq.get()
1529 if rqb is None:
1530 break # Outq flushed
1531
1532 try:
1533 self.send(rqb.data)
1534 if __debug__: self._log(4, '> %s' % rqb.data)
1535 except:
1536 reason = 'socket error: %s - %s' % sys.exc_info()[:2]
1537 if __debug__:
1538 if not self.Terminate:
1539 self._print_log()
1540 if self.debug: self.debug += 4 # Output all
1541 self._log(1, reason)
1542 rqb.abort(self.abort, reason)
1543 break
1544
1545 self.inq.put((self.abort, reason))
1546
1547 if __debug__: self._log(1, 'finished')
1548
1549
1550
1551 # Debugging
1552
1553
1554 if __debug__:
1555
1556 def _init_debug(self, debug=None, debug_file=None):
1557 self.debug = debug is not None and debug or Debug is not None and Debug or 0
1558 self.debug_file = debug_file is not None and debug_file or sys.stderr
1559
1560 self.debug_lock = threading.Lock()
1561 self._cmd_log_len = 20
1562 self._cmd_log_idx = 0
1563 self._cmd_log = {} # Last `_cmd_log_len' interactions
1564 if self.debug:
1565 self._mesg('imaplib2 version %s' % __version__)
1566 self._mesg('imaplib2 debug level %s' % self.debug)
1567
1568
1569 def _dump_ur(self, lvl):
1570 if lvl > self.debug:
1571 return
1572
1573 l = self.untagged_responses.items()
1574 if not l:
1575 return
1576
1577 t = '\n\t\t'
1578 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1579 self.debug_lock.acquire()
1580 self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1581 self.debug_lock.release()
1582
1583
1584 def _log(self, lvl, line):
1585 if lvl > self.debug:
1586 return
1587
1588 if line[-2:] == CRLF:
1589 line = line[:-2] + '\\r\\n'
1590
1591 tn = threading.currentThread().getName()
1592
1593 if self.debug >= 4:
1594 self.debug_lock.acquire()
1595 self._mesg(line, tn)
1596 self.debug_lock.release()
1597 return
1598
1599 # Keep log of last `_cmd_log_len' interactions for debugging.
1600 self._cmd_log[self._cmd_log_idx] = (line, tn, time.time())
1601 self._cmd_log_idx += 1
1602 if self._cmd_log_idx >= self._cmd_log_len:
1603 self._cmd_log_idx = 0
1604
1605
1606 def _mesg(self, s, tn=None, secs=None):
1607 if secs is None:
1608 secs = time.time()
1609 if tn is None:
1610 tn = threading.currentThread().getName()
1611 tm = time.strftime('%M:%S', time.localtime(secs))
1612 self.debug_file.write(' %s.%02d %s %s\n' % (tm, (secs*100)%100, tn, s))
1613 self.debug_file.flush()
1614
1615
1616 def _print_log(self):
1617 self.debug_lock.acquire()
1618 i, n = self._cmd_log_idx, self._cmd_log_len
1619 if n: self._mesg('last %d imaplib2 reports:' % n)
1620 while n:
1621 try:
1622 self._mesg(*self._cmd_log[i])
1623 except:
1624 pass
1625 i += 1
1626 if i >= self._cmd_log_len:
1627 i = 0
1628 n -= 1
1629 self.debug_lock.release()
1630
1631
1632
1633 class IMAP4_SSL(IMAP4):
1634
1635 """IMAP4 client class over SSL connection
1636
1637 Instantiate with:
1638 IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, debug=None, debug_file=None)
1639
1640 host - host's name (default: localhost);
1641 port - port number (default: standard IMAP4 SSL port);
1642 keyfile - PEM formatted file that contains your private key (default: None);
1643 certfile - PEM formatted certificate chain file (default: None);
1644 debug - debug level (default: 0 - no debug);
1645 debug_file - debug stream (default: sys.stderr).
1646
1647 For more documentation see the docstring of the parent class IMAP4.
1648 """
1649
1650
1651 def __init__(self, host=None, port=None, keyfile=None, certfile=None, debug=None, debug_file=None):
1652 self.keyfile = keyfile
1653 self.certfile = certfile
1654 IMAP4.__init__(self, host, port, debug, debug_file)
1655
1656
1657 def open(self, host=None, port=None):
1658 """open(host=None, port=None)
1659 Setup secure connection to remote server on "host:port"
1660 (default: localhost:standard IMAP4 SSL port).
1661 This connection will be used by the routines:
1662 read, send, shutdown, socket, ssl."""
1663
1664 self.host = host is not None and host or ''
1665 self.port = port is not None and port or IMAP4_SSL_PORT
1666 self.sock = self.open_socket()
1667 self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile)
1668
1669 self.read_fd = self.sock.fileno()
1670
1671
1672 def read(self, size):
1673 """data = read(size)
1674 Read at most 'size' bytes from remote."""
1675
1676 return self.sslobj.read(size)
1677
1678
1679 def send(self, data):
1680 """send(data)
1681 Send 'data' to remote."""
1682
1683 # NB: socket.ssl needs a "sendall" method to match socket objects.
1684 bytes = len(data)
1685 while bytes > 0:
1686 sent = self.sslobj.write(data)
1687 if sent == bytes:
1688 break # avoid copy
1689 data = data[sent:]
1690 bytes = bytes - sent
1691
1692
1693 def ssl(self):
1694 """ssl = ssl()
1695 Return socket.ssl instance used to communicate with the IMAP4 server."""
1696
1697 return self.sslobj
1698
1699
1700
1701 class IMAP4_stream(IMAP4):
1702
1703 """IMAP4 client class over a stream
1704
1705 Instantiate with:
1706 IMAP4_stream(command, debug=None, debug_file=None)
1707
1708 command - string that can be passed to os.popen2();
1709 debug - debug level (default: 0 - no debug);
1710 debug_file - debug stream (default: sys.stderr).
1711
1712 For more documentation see the docstring of the parent class IMAP4.
1713 """
1714
1715
1716 def __init__(self, command, debug=None, debug_file=None):
1717 self.command = command
1718 self.host = command
1719 self.port = None
1720 self.sock = None
1721 self.writefile, self.readfile = None, None
1722 self.read_fd = None
1723 IMAP4.__init__(self, debug=debug, debug_file=debug_file)
1724
1725
1726 def open(self, host=None, port=None):
1727 """open(host=None, port=None)
1728 Setup a stream connection via 'self.command'.
1729 This connection will be used by the routines:
1730 read, send, shutdown, socket."""
1731
1732 self.writefile, self.readfile = os.popen2(self.command)
1733 self.read_fd = self.readfile.fileno()
1734
1735
1736 def read(self, size):
1737 """Read 'size' bytes from remote."""
1738
1739 return os.read(self.read_fd, size)
1740
1741
1742 def send(self, data):
1743 """Send data to remote."""
1744
1745 self.writefile.write(data)
1746 self.writefile.flush()
1747
1748
1749 def shutdown(self):
1750 """Close I/O established in "open"."""
1751
1752 self.readfile.close()
1753 self.writefile.close()
1754
1755
1756
1757 class _Authenticator(object):
1758
1759 """Private class to provide en/de-coding
1760 for base64 authentication conversation."""
1761
1762 def __init__(self, mechinst):
1763 self.mech = mechinst # Callable object to provide/process data
1764
1765 def process(self, data, rqb):
1766 ret = self.mech(self.decode(data))
1767 if ret is None:
1768 return '*' # Abort conversation
1769 return self.encode(ret)
1770
1771 def encode(self, inp):
1772 #
1773 # Invoke binascii.b2a_base64 iteratively with
1774 # short even length buffers, strip the trailing
1775 # line feed from the result and append. "Even"
1776 # means a number that factors to both 6 and 8,
1777 # so when it gets to the end of the 8-bit input
1778 # there's no partial 6-bit output.
1779 #
1780 oup = ''
1781 while inp:
1782 if len(inp) > 48:
1783 t = inp[:48]
1784 inp = inp[48:]
1785 else:
1786 t = inp
1787 inp = ''
1788 e = binascii.b2a_base64(t)
1789 if e:
1790 oup = oup + e[:-1]
1791 return oup
1792
1793 def decode(self, inp):
1794 if not inp:
1795 return ''
1796 return binascii.a2b_base64(inp)
1797
1798
1799
1800
1801 class _IdleCont(object):
1802
1803 """When process is called, server is in IDLE state
1804 and will send asynchronous changes."""
1805
1806 def __init__(self, parent, timeout):
1807 self.parent = parent
1808 self.timeout = timeout is not None and timeout or IDLE_TIMEOUT
1809 self.parent.idle_timeout = self.timeout + time.time()
1810
1811 def process(self, data, rqb):
1812 self.parent.idle_rqb = rqb
1813 self.parent.idle_timeout = self.timeout + time.time()
1814 if __debug__: self.parent._log(2, 'server IDLE started, timeout in %.2f secs' % self.timeout)
1815 return None
1816
1817
1818
1819 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1820 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1821
1822 InternalDate = re.compile(r'.*INTERNALDATE "'
1823 r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
1824 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
1825 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
1826 r'"')
1827
1828
1829 def Internaldate2Time(resp):
1830
1831 """time_tuple = Internaldate2Time(resp)
1832 Convert IMAP4 INTERNALDATE to UT."""
1833
1834 mo = InternalDate.match(resp)
1835 if not mo:
1836 return None
1837
1838 mon = Mon2num[mo.group('mon')]
1839 zonen = mo.group('zonen')
1840
1841 day = int(mo.group('day'))
1842 year = int(mo.group('year'))
1843 hour = int(mo.group('hour'))
1844 min = int(mo.group('min'))
1845 sec = int(mo.group('sec'))
1846 zoneh = int(mo.group('zoneh'))
1847 zonem = int(mo.group('zonem'))
1848
1849 # INTERNALDATE timezone must be subtracted to get UT
1850
1851 zone = (zoneh*60 + zonem)*60
1852 if zonen == '-':
1853 zone = -zone
1854
1855 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1856
1857 utc = time.mktime(tt)
1858
1859 # Following is necessary because the time module has no 'mkgmtime'.
1860 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1861
1862 lt = time.localtime(utc)
1863 if time.daylight and lt[-1]:
1864 zone = zone + time.altzone
1865 else:
1866 zone = zone + time.timezone
1867
1868 return time.localtime(utc - zone)
1869
1870 Internaldate2tuple = Internaldate2Time # (Backward compatible)
1871
1872
1873
1874 def Time2Internaldate(date_time):
1875
1876 """'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time)
1877 Convert 'date_time' to IMAP4 INTERNALDATE representation."""
1878
1879 if isinstance(date_time, (int, float)):
1880 tt = time.localtime(date_time)
1881 elif isinstance(date_time, (tuple, time.struct_time)):
1882 tt = date_time
1883 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1884 return date_time # Assume in correct format
1885 else:
1886 raise ValueError("date_time not of a known type")
1887
1888 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1889 if dt[0] == '0':
1890 dt = ' ' + dt[1:]
1891 if time.daylight and tt[-1]:
1892 zone = -time.altzone
1893 else:
1894 zone = -time.timezone
1895 return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
1896
1897
1898
1899 FLAGS_cre = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
1900
1901 def ParseFlags(resp):
1902
1903 """('flag', ...) = ParseFlags(line)
1904 Convert IMAP4 flags response to python tuple."""
1905
1906 mo = FLAGS_cre.match(resp)
1907 if not mo:
1908 return ()
1909
1910 return tuple(mo.group('flags').split())
1911
1912
1913
1914 if __name__ == '__main__':
1915
1916 # To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]',
1917 # or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1918 # or as 'python imaplib2.py -l "keyfile[:certfile]" [IMAP4_SSL_server_hostname]'
1919
1920 import getopt, getpass
1921
1922 try:
1923 optlist, args = getopt.getopt(sys.argv[1:], 'd:l:s:p:')
1924 except getopt.error, val:
1925 optlist, args = (), ()
1926
1927 debug, port, stream_command, keyfile, certfile = (None,)*5
1928 for opt,val in optlist:
1929 if opt == '-d':
1930 debug = int(val)
1931 elif opt == '-l':
1932 try:
1933 keyfile,certfile = val.split(':')
1934 except ValueError:
1935 keyfile,certfile = val,val
1936 elif opt == '-p':
1937 port = int(val)
1938 elif opt == '-s':
1939 stream_command = val
1940 if not args: args = (stream_command,)
1941
1942 if not args: args = ('',)
1943 if not port: port = (keyfile is not None) and IMAP4_SSL_PORT or IMAP4_PORT
1944
1945 host = args[0]
1946
1947 USER = getpass.getuser()
1948
1949 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \
1950 % {'user':USER, 'lf':'\n', 'data':open(__file__).read()}
1951 test_seq1 = [
1952 ('list', ('""', '%')),
1953 ('create', ('/tmp/imaplib2_test.0',)),
1954 ('rename', ('/tmp/imaplib2_test.0', '/tmp/imaplib2_test.1')),
1955 ('CREATE', ('/tmp/imaplib2_test.2',)),
1956 ('append', ('/tmp/imaplib2_test.2', None, None, test_mesg)),
1957 ('list', ('/tmp', 'imaplib2_test*')),
1958 ('select', ('/tmp/imaplib2_test.2',)),
1959 ('search', (None, 'SUBJECT', 'IMAP4 test')),
1960 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1961 ('store', ('1', 'FLAGS', '(\Deleted)')),
1962 ('namespace', ()),
1963 ('expunge', ()),
1964 ('recent', ()),
1965 ('close', ()),
1966 ]
1967
1968 test_seq2 = (
1969 ('select', ()),
1970 ('response',('UIDVALIDITY',)),
1971 ('response', ('EXISTS',)),
1972 ('append', (None, None, None, test_mesg)),
1973 ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')),
1974 ('uid', ('SEARCH', 'ALL')),
1975 ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')),
1976 ('recent', ()),
1977 )
1978
1979 AsyncError = None
1980
1981 def responder((response, cb_arg, error)):
1982 global AsyncError
1983 cmd, args = cb_arg
1984 if error is not None:
1985 AsyncError = error
1986 M._mesg('[cb] ERROR %s %.100s => %s' % (cmd, args, error))
1987 return
1988 typ, dat = response
1989 M._mesg('[cb] %s %.100s => %s %.100s' % (cmd, args, typ, dat))
1990 if typ == 'NO':
1991 AsyncError = (Exception, dat[0])
1992
1993 def run(cmd, args, cb=None):
1994 if AsyncError:
1995 M.logout()
1996 typ, val = AsyncError
1997 raise typ(val)
1998 M._mesg('%s %.100s' % (cmd, args))
1999 try:
2000 if cb is not None:
2001 typ, dat = getattr(M, cmd)(callback=responder, cb_arg=(cmd, args), *args)
2002 if M.debug:
2003 M._mesg('%s %.100s => %s %.100s' % (cmd, args, typ, dat))
2004 else:
2005 typ, dat = getattr(M, cmd)(*args)
2006 M._mesg('%s %.100s => %s %.100s' % (cmd, args, typ, dat))
2007 except:
2008 M.logout()
2009 raise
2010 if typ == 'NO':
2011 M.logout()
2012 raise Exception(dat[0])
2013 return dat
2014
2015 try:
2016 threading.currentThread().setName('main')
2017
2018 if keyfile is not None:
2019 if not keyfile: keyfile = None
2020 if not certfile: certfile = None
2021 M = IMAP4_SSL(host=host, port=port, keyfile=keyfile, certfile=certfile, debug=debug)
2022 elif stream_command:
2023 M = IMAP4_stream(stream_command, debug=debug)
2024 else:
2025 M = IMAP4(host=host, port=port, debug=debug)
2026 if M.state != 'AUTH': # Login needed
2027 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
2028 test_seq1.insert(0, ('login', (USER, PASSWD)))
2029 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
2030 M._mesg('CAPABILITIES = %r' % (M.capabilities,))
2031
2032 for cmd,args in test_seq1:
2033 run(cmd, args, cb=1)
2034
2035 for ml in run('list', ('/tmp/', 'imaplib2_test%')):
2036 mo = re.match(r'.*"([^"]+)"$', ml)
2037 if mo: path = mo.group(1)
2038 else: path = ml.split()[-1]
2039 run('delete', (path,), cb=1)
2040
2041 for cmd,args in test_seq2:
2042 if (cmd,args) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')):
2043 run(cmd, args, cb=1)
2044 continue
2045
2046 dat = run(cmd, args)
2047 uid = dat[-1].split()
2048 if not uid: continue
2049 run('uid', ('FETCH', uid[-1],
2050 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'), cb=1)
2051 run('uid', ('STORE', uid[-1], 'FLAGS', '(\Deleted)'), cb=1)
2052 run('expunge', (), cb=1)
2053
2054 run('idle', (3,))
2055 run('logout', ())
2056
2057 if debug:
2058 print
2059 M._print_log()
2060
2061 print '\nAll tests OK.'
2062
2063 except:
2064 print '\nTests failed.'
2065
2066 if not debug:
2067 print '''
2068 If you would like to see debugging output,
2069 try: %s -d5
2070 ''' % sys.argv[0]
2071
2072 raise