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