1 """IMAP4 client.
2
3 Based on RFC 2060.
4
5 Public class: IMAP4
6 Public variable: Debug
7 Public functions: Internaldate2tuple
8 Int2AP
9 ParseFlags
10 Time2Internaldate
11 """
12
13
14
15
16
17
18
19
20
21
22
23 __version__ = "2.58"
24
25 import binascii, os, random, re, socket, sys, time
26
27 __all__ = ["IMAP4", "IMAP4_SSL", "IMAP4_stream", "Internaldate2tuple",
28 "Int2AP", "ParseFlags", "Time2Internaldate"]
29
30
31
32 CRLF = '\r\n'
33 Debug = 0
34 IMAP4_PORT = 143
35 IMAP4_SSL_PORT = 993
36 AllowedVersions = ('IMAP4REV1', 'IMAP4')
37
38
39
40 Commands = {
41
42 'APPEND': ('AUTH', 'SELECTED'),
43 'AUTHENTICATE': ('NONAUTH',),
44 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
45 'CHECK': ('SELECTED',),
46 'CLOSE': ('SELECTED',),
47 'COPY': ('SELECTED',),
48 'CREATE': ('AUTH', 'SELECTED'),
49 'DELETE': ('AUTH', 'SELECTED'),
50 'DELETEACL': ('AUTH', 'SELECTED'),
51 'EXAMINE': ('AUTH', 'SELECTED'),
52 'EXPUNGE': ('SELECTED',),
53 'FETCH': ('SELECTED',),
54 'GETACL': ('AUTH', 'SELECTED'),
55 'GETANNOTATION':('AUTH', 'SELECTED'),
56 'GETQUOTA': ('AUTH', 'SELECTED'),
57 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
58 'MYRIGHTS': ('AUTH', 'SELECTED'),
59 'LIST': ('AUTH', 'SELECTED'),
60 'LOGIN': ('NONAUTH',),
61 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
62 'LSUB': ('AUTH', 'SELECTED'),
63 'NAMESPACE': ('AUTH', 'SELECTED'),
64 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
65 'PARTIAL': ('SELECTED',),
66 'PROXYAUTH': ('AUTH',),
67 'RENAME': ('AUTH', 'SELECTED'),
68 'SEARCH': ('SELECTED',),
69 'SELECT': ('AUTH', 'SELECTED'),
70 'SETACL': ('AUTH', 'SELECTED'),
71 'SETANNOTATION':('AUTH', 'SELECTED'),
72 'SETQUOTA': ('AUTH', 'SELECTED'),
73 'SORT': ('SELECTED',),
74 'STATUS': ('AUTH', 'SELECTED'),
75 'STORE': ('SELECTED',),
76 'SUBSCRIBE': ('AUTH', 'SELECTED'),
77 'THREAD': ('SELECTED',),
78 'UID': ('SELECTED',),
79 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
80 }
81
82
83
84 Continuation = re.compile(r'\+( (?P<data>.*))?')
85 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
86 InternalDate = re.compile(r'.*INTERNALDATE "'
87 r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
88 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
89 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
90 r'"')
91 Literal = re.compile(r'.*{(?P<size>\d+)}$')
92 MapCRLF = re.compile(r'\r\n|\r|\n')
93 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
94 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
95 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
96
97
98
100
101 """IMAP4 client class.
102
103 Instantiate with: IMAP4([host[, port]])
104
105 host - host's name (default: localhost);
106 port - port number (default: standard IMAP4 port).
107
108 All IMAP4rev1 commands are supported by methods of the same
109 name (in lower-case).
110
111 All arguments to commands are converted to strings, except for
112 AUTHENTICATE, and the last argument to APPEND which is passed as
113 an IMAP4 literal. If necessary (the string contains any
114 non-printing characters or white-space and isn't enclosed with
115 either parentheses or double quotes) each string is quoted.
116 However, the 'password' argument to the LOGIN command is always
117 quoted. If you want to avoid having an argument string quoted
118 (eg: the 'flags' argument to STORE) then enclose the string in
119 parentheses (eg: "(\Deleted)").
120
121 Each command returns a tuple: (type, [data, ...]) where 'type'
122 is usually 'OK' or 'NO', and 'data' is either the text from the
123 tagged response, or untagged results from command. Each 'data'
124 is either a string, or a tuple. If a tuple, then the first part
125 is the header of the response, and the second part contains
126 the data (ie: 'literal' value).
127
128 Errors raise the exception class <instance>.error("<reason>").
129 IMAP4 server errors raise <instance>.abort("<reason>"),
130 which is a sub-class of 'error'. Mailbox status changes
131 from READ-WRITE to READ-ONLY raise the exception class
132 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
133
134 "error" exceptions imply a program error.
135 "abort" exceptions imply the connection should be reset, and
136 the command re-tried.
137 "readonly" exceptions imply the command should be re-tried.
138
139 Note: to use this module, you must read the RFCs pertaining to the
140 IMAP4 protocol, as the semantics of the arguments to each IMAP4
141 command are left to the invoker, not to mention the results. Also,
142 most IMAP servers implement a sub-set of the commands available here.
143 """
144
145 - class error(Exception): pass
146 - class abort(error): pass
148
149 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
150
152 self.debug = Debug
153 self.state = 'LOGOUT'
154 self.literal = None
155 self.tagged_commands = {}
156 self.untagged_responses = {}
157 self.continuation_response = ''
158 self.is_readonly = False
159 self.tagnum = 0
160
161
162
163 self.open(host, port)
164
165
166
167
168 self.tagpre = Int2AP(random.randint(4096, 65535))
169 self.tagre = re.compile(r'(?P<tag>'
170 + self.tagpre
171 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
172
173
174
175
176 if __debug__:
177 self._cmd_log_len = 10
178 self._cmd_log_idx = 0
179 self._cmd_log = {}
180 if self.debug >= 1:
181 self._mesg('imaplib version %s' % __version__)
182 self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
183
184 self.welcome = self._get_response()
185 if 'PREAUTH' in self.untagged_responses:
186 self.state = 'AUTH'
187 elif 'OK' in self.untagged_responses:
188 self.state = 'NONAUTH'
189 else:
190 raise self.error(self.welcome)
191
192 typ, dat = self.capability()
193 if dat == [None]:
194 raise self.error('no CAPABILITY response from server')
195 self.capabilities = tuple(dat[-1].upper().split())
196
197 if __debug__:
198 if self.debug >= 3:
199 self._mesg('CAPABILITIES: %r' % (self.capabilities,))
200
201 for version in AllowedVersions:
202 if not version in self.capabilities:
203 continue
204 self.PROTOCOL_VERSION = version
205 return
206
207 raise self.error('server not IMAP4 compliant')
208
209
211
212 if attr in Commands:
213 return getattr(self, attr.lower())
214 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
215
216
217
218
219
220
222 """Setup connection to remote server on "host:port"
223 (default: localhost:standard IMAP4 port).
224 This connection will be used by the routines:
225 read, readline, send, shutdown.
226 """
227 self.host = host
228 self.port = port
229 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
230 self.sock.connect((host, port))
231 self.file = self.sock.makefile('rb')
232
233
234 - def read(self, size):
235 """Read 'size' bytes from remote."""
236 return self.file.read(size)
237
238
240 """Read line from remote."""
241 return self.file.readline()
242
243
244 - def send(self, data):
245 """Send data to remote."""
246 self.sock.sendall(data)
247
248
250 """Close I/O established in "open"."""
251 self.file.close()
252 self.sock.close()
253
254
256 """Return socket instance used to connect to IMAP4 server.
257
258 socket = <instance>.socket()
259 """
260 return self.sock
261
262
263
264
265
266
268 """Return most recent 'RECENT' responses if any exist,
269 else prompt server for an update using the 'NOOP' command.
270
271 (typ, [data]) = <instance>.recent()
272
273 'data' is None if no new messages,
274 else list of RECENT responses, most recent last.
275 """
276 name = 'RECENT'
277 typ, dat = self._untagged_response('OK', [None], name)
278 if dat[-1]:
279 return typ, dat
280 typ, dat = self.noop()
281 return self._untagged_response(typ, dat, name)
282
283
285 """Return data for response 'code' if received, or None.
286
287 Old value for response 'code' is cleared.
288
289 (code, [data]) = <instance>.response(code)
290 """
291 return self._untagged_response(code, [None], code.upper())
292
293
294
295
296
297
298 - def append(self, mailbox, flags, date_time, message):
299 """Append message to named mailbox.
300
301 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
302
303 All args except `message' can be None.
304 """
305 name = 'APPEND'
306 if not mailbox:
307 mailbox = 'INBOX'
308 if flags:
309 if (flags[0],flags[-1]) != ('(',')'):
310 flags = '(%s)' % flags
311 else:
312 flags = None
313 if date_time:
314 date_time = Time2Internaldate(date_time)
315 else:
316 date_time = None
317 self.literal = MapCRLF.sub(CRLF, message)
318 return self._simple_command(name, mailbox, flags, date_time)
319
320
322 """Authenticate command - requires response processing.
323
324 'mechanism' specifies which authentication mechanism is to
325 be used - it must appear in <instance>.capabilities in the
326 form AUTH=<mechanism>.
327
328 'authobject' must be a callable object:
329
330 data = authobject(response)
331
332 It will be called to process server continuation responses.
333 It should return data that will be encoded and sent to server.
334 It should return None if the client abort response '*' should
335 be sent instead.
336 """
337 mech = mechanism.upper()
338
339
340
341
342 self.literal = _Authenticator(authobject).process
343 typ, dat = self._simple_command('AUTHENTICATE', mech)
344 if typ != 'OK':
345 raise self.error(dat[-1])
346 self.state = 'AUTH'
347 return typ, dat
348
349
351 """(typ, [data]) = <instance>.capability()
352 Fetch capabilities list from server."""
353
354 name = 'CAPABILITY'
355 typ, dat = self._simple_command(name)
356 return self._untagged_response(typ, dat, name)
357
358
360 """Checkpoint mailbox on server.
361
362 (typ, [data]) = <instance>.check()
363 """
364 return self._simple_command('CHECK')
365
366
368 """Close currently selected mailbox.
369
370 Deleted messages are removed from writable mailbox.
371 This is the recommended command before 'LOGOUT'.
372
373 (typ, [data]) = <instance>.close()
374 """
375 try:
376 typ, dat = self._simple_command('CLOSE')
377 finally:
378 self.state = 'AUTH'
379 return typ, dat
380
381
382 - def copy(self, message_set, new_mailbox):
383 """Copy 'message_set' messages onto end of 'new_mailbox'.
384
385 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
386 """
387 return self._simple_command('COPY', message_set, new_mailbox)
388
389
391 """Create new mailbox.
392
393 (typ, [data]) = <instance>.create(mailbox)
394 """
395 return self._simple_command('CREATE', mailbox)
396
397
399 """Delete old mailbox.
400
401 (typ, [data]) = <instance>.delete(mailbox)
402 """
403 return self._simple_command('DELETE', mailbox)
404
406 """Delete the ACLs (remove any rights) set for who on mailbox.
407
408 (typ, [data]) = <instance>.deleteacl(mailbox, who)
409 """
410 return self._simple_command('DELETEACL', mailbox, who)
411
413 """Permanently remove deleted items from selected mailbox.
414
415 Generates 'EXPUNGE' response for each deleted message.
416
417 (typ, [data]) = <instance>.expunge()
418
419 'data' is list of 'EXPUNGE'd message numbers in order received.
420 """
421 name = 'EXPUNGE'
422 typ, dat = self._simple_command(name)
423 return self._untagged_response(typ, dat, name)
424
425
426 - def fetch(self, message_set, message_parts):
427 """Fetch (parts of) messages.
428
429 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
430
431 'message_parts' should be a string of selected parts
432 enclosed in parentheses, eg: "(UID BODY[TEXT])".
433
434 'data' are tuples of message part envelope and data.
435 """
436 name = 'FETCH'
437 typ, dat = self._simple_command(name, message_set, message_parts)
438 return self._untagged_response(typ, dat, name)
439
440
442 """Get the ACLs for a mailbox.
443
444 (typ, [data]) = <instance>.getacl(mailbox)
445 """
446 typ, dat = self._simple_command('GETACL', mailbox)
447 return self._untagged_response(typ, dat, 'ACL')
448
449
451 """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
452 Retrieve ANNOTATIONs."""
453
454 typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
455 return self._untagged_response(typ, dat, 'ANNOTATION')
456
457
459 """Get the quota root's resource usage and limits.
460
461 Part of the IMAP4 QUOTA extension defined in rfc2087.
462
463 (typ, [data]) = <instance>.getquota(root)
464 """
465 typ, dat = self._simple_command('GETQUOTA', root)
466 return self._untagged_response(typ, dat, 'QUOTA')
467
468
470 """Get the list of quota roots for the named mailbox.
471
472 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
473 """
474 typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
475 typ, quota = self._untagged_response(typ, dat, 'QUOTA')
476 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
477 return typ, [quotaroot, quota]
478
479
480 - def list(self, directory='""', pattern='*'):
481 """List mailbox names in directory matching pattern.
482
483 (typ, [data]) = <instance>.list(directory='""', pattern='*')
484
485 'data' is list of LIST responses.
486 """
487 name = 'LIST'
488 typ, dat = self._simple_command(name, directory, pattern)
489 return self._untagged_response(typ, dat, name)
490
491
492 - def login(self, user, password):
493 """Identify client using plaintext password.
494
495 (typ, [data]) = <instance>.login(user, password)
496
497 NB: 'password' will be quoted.
498 """
499 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
500 if typ != 'OK':
501 raise self.error(dat[-1])
502 self.state = 'AUTH'
503 return typ, dat
504
505
507 """ Force use of CRAM-MD5 authentication.
508
509 (typ, [data]) = <instance>.login_cram_md5(user, password)
510 """
511 self.user, self.password = user, password
512 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
513
514
516 """ Authobject to use with CRAM-MD5 authentication. """
517 import hmac
518 return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
519
520
522 """Shutdown connection to server.
523
524 (typ, [data]) = <instance>.logout()
525
526 Returns server 'BYE' response.
527 """
528 self.state = 'LOGOUT'
529 try: typ, dat = self._simple_command('LOGOUT')
530 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
531 self.shutdown()
532 if 'BYE' in self.untagged_responses:
533 return 'BYE', self.untagged_responses['BYE']
534 return typ, dat
535
536
537 - def lsub(self, directory='""', pattern='*'):
538 """List 'subscribed' mailbox names in directory matching pattern.
539
540 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
541
542 'data' are tuples of message part envelope and data.
543 """
544 name = 'LSUB'
545 typ, dat = self._simple_command(name, directory, pattern)
546 return self._untagged_response(typ, dat, name)
547
549 """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
550
551 (typ, [data]) = <instance>.myrights(mailbox)
552 """
553 typ,dat = self._simple_command('MYRIGHTS', mailbox)
554 return self._untagged_response(typ, dat, 'MYRIGHTS')
555
557 """ Returns IMAP namespaces ala rfc2342
558
559 (typ, [data, ...]) = <instance>.namespace()
560 """
561 name = 'NAMESPACE'
562 typ, dat = self._simple_command(name)
563 return self._untagged_response(typ, dat, name)
564
565
567 """Send NOOP command.
568
569 (typ, [data]) = <instance>.noop()
570 """
571 if __debug__:
572 if self.debug >= 3:
573 self._dump_ur(self.untagged_responses)
574 return self._simple_command('NOOP')
575
576
577 - def partial(self, message_num, message_part, start, length):
578 """Fetch truncated part of a message.
579
580 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
581
582 'data' is tuple of message part envelope and data.
583 """
584 name = 'PARTIAL'
585 typ, dat = self._simple_command(name, message_num, message_part, start, length)
586 return self._untagged_response(typ, dat, 'FETCH')
587
588
590 """Assume authentication as "user".
591
592 Allows an authorised administrator to proxy into any user's
593 mailbox.
594
595 (typ, [data]) = <instance>.proxyauth(user)
596 """
597
598 name = 'PROXYAUTH'
599 return self._simple_command('PROXYAUTH', user)
600
601
602 - def rename(self, oldmailbox, newmailbox):
603 """Rename old mailbox name to new.
604
605 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
606 """
607 return self._simple_command('RENAME', oldmailbox, newmailbox)
608
609
610 - def search(self, charset, *criteria):
611 """Search mailbox for matching messages.
612
613 (typ, [data]) = <instance>.search(charset, criterion, ...)
614
615 'data' is space separated list of matching message numbers.
616 """
617 name = 'SEARCH'
618 if charset:
619 typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
620 else:
621 typ, dat = self._simple_command(name, *criteria)
622 return self._untagged_response(typ, dat, name)
623
624
625 - def select(self, mailbox='INBOX', readonly=False):
626 """Select a mailbox.
627
628 Flush all untagged responses.
629
630 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
631
632 'data' is count of messages in mailbox ('EXISTS' response).
633
634 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
635 other responses should be obtained via <instance>.response('FLAGS') etc.
636 """
637 self.untagged_responses = {}
638 self.is_readonly = readonly
639 if readonly:
640 name = 'EXAMINE'
641 else:
642 name = 'SELECT'
643 typ, dat = self._simple_command(name, mailbox)
644 if typ != 'OK':
645 self.state = 'AUTH'
646 return typ, dat
647 self.state = 'SELECTED'
648 if 'READ-ONLY' in self.untagged_responses \
649 and not readonly:
650 if __debug__:
651 if self.debug >= 1:
652 self._dump_ur(self.untagged_responses)
653 raise self.readonly('%s is not writable' % mailbox)
654 return typ, self.untagged_responses.get('EXISTS', [None])
655
656
657 - def setacl(self, mailbox, who, what):
658 """Set a mailbox acl.
659
660 (typ, [data]) = <instance>.setacl(mailbox, who, what)
661 """
662 return self._simple_command('SETACL', mailbox, who, what)
663
664
666 """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
667 Set ANNOTATIONs."""
668
669 typ, dat = self._simple_command('SETANNOTATION', *args)
670 return self._untagged_response(typ, dat, 'ANNOTATION')
671
672
674 """Set the quota root's resource limits.
675
676 (typ, [data]) = <instance>.setquota(root, limits)
677 """
678 typ, dat = self._simple_command('SETQUOTA', root, limits)
679 return self._untagged_response(typ, dat, 'QUOTA')
680
681
682 - def sort(self, sort_criteria, charset, *search_criteria):
683 """IMAP4rev1 extension SORT command.
684
685 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
686 """
687 name = 'SORT'
688
689
690 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
691 sort_criteria = '(%s)' % sort_criteria
692 typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
693 return self._untagged_response(typ, dat, name)
694
695
696 - def status(self, mailbox, names):
697 """Request named status conditions for mailbox.
698
699 (typ, [data]) = <instance>.status(mailbox, names)
700 """
701 name = 'STATUS'
702
703
704 typ, dat = self._simple_command(name, mailbox, names)
705 return self._untagged_response(typ, dat, name)
706
707
708 - def store(self, message_set, command, flags):
709 """Alters flag dispositions for messages in mailbox.
710
711 (typ, [data]) = <instance>.store(message_set, command, flags)
712 """
713 if (flags[0],flags[-1]) != ('(',')'):
714 flags = '(%s)' % flags
715 typ, dat = self._simple_command('STORE', message_set, command, flags)
716 return self._untagged_response(typ, dat, 'FETCH')
717
718
720 """Subscribe to new mailbox.
721
722 (typ, [data]) = <instance>.subscribe(mailbox)
723 """
724 return self._simple_command('SUBSCRIBE', mailbox)
725
726
727 - def thread(self, threading_algorithm, charset, *search_criteria):
728 """IMAPrev1 extension THREAD command.
729
730 (type, [data]) = <instance>.thread(threading_alogrithm, charset, search_criteria, ...)
731 """
732 name = 'THREAD'
733 typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
734 return self._untagged_response(typ, dat, name)
735
736
737 - def uid(self, command, *args):
738 """Execute "command arg ..." with messages identified by UID,
739 rather than message number.
740
741 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
742
743 Returns response appropriate to 'command'.
744 """
745 command = command.upper()
746 if not command in Commands:
747 raise self.error("Unknown IMAP4 UID command: %s" % command)
748 if self.state not in Commands[command]:
749 raise self.error('command %s illegal in state %s'
750 % (command, self.state))
751 name = 'UID'
752 typ, dat = self._simple_command(name, command, *args)
753 if command in ('SEARCH', 'SORT'):
754 name = command
755 else:
756 name = 'FETCH'
757 return self._untagged_response(typ, dat, name)
758
759
761 """Unsubscribe from old mailbox.
762
763 (typ, [data]) = <instance>.unsubscribe(mailbox)
764 """
765 return self._simple_command('UNSUBSCRIBE', mailbox)
766
767
768 - def xatom(self, name, *args):
769 """Allow simple extension commands
770 notified by server in CAPABILITY response.
771
772 Assumes command is legal in current state.
773
774 (typ, [data]) = <instance>.xatom(name, arg, ...)
775
776 Returns response appropriate to extension command `name'.
777 """
778 name = name.upper()
779
780
781 if not name in Commands:
782 Commands[name] = (self.state,)
783 return self._simple_command(name, *args)
784
785
786
787
788
789
791
792 if dat is None: dat = ''
793 ur = self.untagged_responses
794 if __debug__:
795 if self.debug >= 5:
796 self._mesg('untagged_responses[%s] %s += ["%s"]' %
797 (typ, len(ur.get(typ,'')), dat))
798 if typ in ur:
799 ur[typ].append(dat)
800 else:
801 ur[typ] = [dat]
802
803
805 bye = self.untagged_responses.get('BYE')
806 if bye:
807 raise self.abort(bye[-1])
808
809
811
812 if self.state not in Commands[name]:
813 self.literal = None
814 raise self.error(
815 'command %s illegal in state %s' % (name, self.state))
816
817 for typ in ('OK', 'NO', 'BAD'):
818 if typ in self.untagged_responses:
819 del self.untagged_responses[typ]
820
821 if 'READ-ONLY' in self.untagged_responses \
822 and not self.is_readonly:
823 raise self.readonly('mailbox status changed to READ-ONLY')
824
825 tag = self._new_tag()
826 data = '%s %s' % (tag, name)
827 for arg in args:
828 if arg is None: continue
829 data = '%s %s' % (data, self._checkquote(arg))
830
831 literal = self.literal
832 if literal is not None:
833 self.literal = None
834 if type(literal) is type(self._command):
835 literator = literal
836 else:
837 literator = None
838 data = '%s {%s}' % (data, len(literal))
839
840 if __debug__:
841 if self.debug >= 4:
842 self._mesg('> %s' % data)
843 else:
844 self._log('> %s' % data)
845
846 try:
847 self.send('%s%s' % (data, CRLF))
848 except (socket.error, OSError), val:
849 raise self.abort('socket error: %s' % val)
850
851 if literal is None:
852 return tag
853
854 while 1:
855
856
857 while self._get_response():
858 if self.tagged_commands[tag]:
859 return tag
860
861
862
863 if literator:
864 literal = literator(self.continuation_response)
865
866 if __debug__:
867 if self.debug >= 4:
868 self._mesg('write literal size %s' % len(literal))
869
870 try:
871 self.send(literal)
872 self.send(CRLF)
873 except (socket.error, OSError), val:
874 raise self.abort('socket error: %s' % val)
875
876 if not literator:
877 break
878
879 return tag
880
881
883 self._check_bye()
884 try:
885 typ, data = self._get_tagged_response(tag)
886 except self.abort, val:
887 raise self.abort('command: %s => %s' % (name, val))
888 except self.error, val:
889 raise self.error('command: %s => %s' % (name, val))
890 self._check_bye()
891 if typ == 'BAD':
892 raise self.error('%s command error: %s %s' % (name, typ, data))
893 return typ, data
894
895
897
898
899
900
901
902
903 resp = self._get_line()
904
905
906
907 if self._match(self.tagre, resp):
908 tag = self.mo.group('tag')
909 if not tag in self.tagged_commands:
910 raise self.abort('unexpected tagged response: %s' % resp)
911
912 typ = self.mo.group('type')
913 dat = self.mo.group('data')
914 self.tagged_commands[tag] = (typ, [dat])
915 else:
916 dat2 = None
917
918
919
920 if not self._match(Untagged_response, resp):
921 if self._match(Untagged_status, resp):
922 dat2 = self.mo.group('data2')
923
924 if self.mo is None:
925
926
927 if self._match(Continuation, resp):
928 self.continuation_response = self.mo.group('data')
929 return None
930
931 raise self.abort("unexpected response: '%s'" % resp)
932
933 typ = self.mo.group('type')
934 dat = self.mo.group('data')
935 if dat is None: dat = ''
936 if dat2: dat = dat + ' ' + dat2
937
938
939
940 while self._match(Literal, dat):
941
942
943
944 size = int(self.mo.group('size'))
945 if __debug__:
946 if self.debug >= 4:
947 self._mesg('read literal size %s' % size)
948 data = self.read(size)
949
950
951
952 self._append_untagged(typ, (dat, data))
953
954
955
956 dat = self._get_line()
957
958 self._append_untagged(typ, dat)
959
960
961
962 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
963 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
964
965 if __debug__:
966 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
967 self._mesg('%s response: %s' % (typ, dat))
968
969 return resp
970
971
973
974 while 1:
975 result = self.tagged_commands[tag]
976 if result is not None:
977 del self.tagged_commands[tag]
978 return result
979
980
981
982
983
984
985 try:
986 self._get_response()
987 except self.abort, val:
988 if __debug__:
989 if self.debug >= 1:
990 self.print_log()
991 raise
992
993
995
996 line = self.readline()
997 if not line:
998 raise self.abort('socket error: EOF')
999
1000
1001
1002 line = line[:-2]
1003 if __debug__:
1004 if self.debug >= 4:
1005 self._mesg('< %s' % line)
1006 else:
1007 self._log('< %s' % line)
1008 return line
1009
1010
1012
1013
1014
1015
1016 self.mo = cre.match(s)
1017 if __debug__:
1018 if self.mo is not None and self.debug >= 5:
1019 self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
1020 return self.mo is not None
1021
1022
1024
1025 tag = '%s%s' % (self.tagpre, self.tagnum)
1026 self.tagnum = self.tagnum + 1
1027 self.tagged_commands[tag] = None
1028 return tag
1029
1030
1032
1033
1034
1035
1036 if type(arg) is not type(''):
1037 return arg
1038 if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
1039 return arg
1040 if arg and self.mustquote.search(arg) is None:
1041 return arg
1042 return self._quote(arg)
1043
1044
1046
1047 arg = arg.replace('\\', '\\\\')
1048 arg = arg.replace('"', '\\"')
1049
1050 return '"%s"' % arg
1051
1052
1054
1055 return self._command_complete(name, self._command(name, *args))
1056
1057
1059
1060 if typ == 'NO':
1061 return typ, dat
1062 if not name in self.untagged_responses:
1063 return typ, [None]
1064 data = self.untagged_responses.pop(name)
1065 if __debug__:
1066 if self.debug >= 5:
1067 self._mesg('untagged_responses[%s] => %s' % (name, data))
1068 return typ, data
1069
1070
1071 if __debug__:
1072
1073 - def _mesg(self, s, secs=None):
1074 if secs is None:
1075 secs = time.time()
1076 tm = time.strftime('%M:%S', time.localtime(secs))
1077 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1078 sys.stderr.flush()
1079
1081
1082 l = dict.items()
1083 if not l: return
1084 t = '\n\t\t'
1085 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1086 self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1087
1088 - def _log(self, line):
1089
1090 self._cmd_log[self._cmd_log_idx] = (line, time.time())
1091 self._cmd_log_idx += 1
1092 if self._cmd_log_idx >= self._cmd_log_len:
1093 self._cmd_log_idx = 0
1094
1096 self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
1097 i, n = self._cmd_log_idx, self._cmd_log_len
1098 while n:
1099 try:
1100 self._mesg(*self._cmd_log[i])
1101 except:
1102 pass
1103 i += 1
1104 if i >= self._cmd_log_len:
1105 i = 0
1106 n -= 1
1107
1108
1109
1111
1112 """IMAP4 client class over SSL connection
1113
1114 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1115
1116 host - host's name (default: localhost);
1117 port - port number (default: standard IMAP4 SSL port).
1118 keyfile - PEM formatted file that contains your private key (default: None);
1119 certfile - PEM formatted certificate chain file (default: None);
1120
1121 for more documentation see the docstring of the parent class IMAP4.
1122 """
1123
1124
1126 self.keyfile = keyfile
1127 self.certfile = certfile
1128 IMAP4.__init__(self, host, port)
1129
1130
1132 """Setup connection to remote server on "host:port".
1133 (default: localhost:standard IMAP4 SSL port).
1134 This connection will be used by the routines:
1135 read, readline, send, shutdown.
1136 """
1137 self.host = host
1138 self.port = port
1139 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1140 self.sock.connect((host, port))
1141 self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile)
1142
1143
1144 - def read(self, size):
1145 """Read 'size' bytes from remote."""
1146
1147 chunks = []
1148 read = 0
1149 while read < size:
1150 data = self.sslobj.read(min(size-read, 16384))
1151 read += len(data)
1152 chunks.append(data)
1153
1154 return ''.join(chunks)
1155
1156
1158 """Read line from remote."""
1159
1160 line = []
1161 while 1:
1162 char = self.sslobj.read(1)
1163 line.append(char)
1164 if char == "\n": return ''.join(line)
1165
1166
1167 - def send(self, data):
1168 """Send data to remote."""
1169
1170 bytes = len(data)
1171 while bytes > 0:
1172 sent = self.sslobj.write(data)
1173 if sent == bytes:
1174 break
1175 data = data[sent:]
1176 bytes = bytes - sent
1177
1178
1180 """Close I/O established in "open"."""
1181 self.sock.close()
1182
1183
1185 """Return socket instance used to connect to IMAP4 server.
1186
1187 socket = <instance>.socket()
1188 """
1189 return self.sock
1190
1191
1193 """Return SSLObject instance used to communicate with the IMAP4 server.
1194
1195 ssl = <instance>.socket.ssl()
1196 """
1197 return self.sslobj
1198
1199
1200
1202
1203 """IMAP4 client class over a stream
1204
1205 Instantiate with: IMAP4_stream(command)
1206
1207 where "command" is a string that can be passed to os.popen2()
1208
1209 for more documentation see the docstring of the parent class IMAP4.
1210 """
1211
1212
1216
1217
1218 - def open(self, host = None, port = None):
1219 """Setup a stream connection.
1220 This connection will be used by the routines:
1221 read, readline, send, shutdown.
1222 """
1223 self.host = None
1224 self.port = None
1225 self.sock = None
1226 self.file = None
1227 self.writefile, self.readfile = os.popen2(self.command)
1228
1229
1230 - def read(self, size):
1231 """Read 'size' bytes from remote."""
1232 return self.readfile.read(size)
1233
1234
1236 """Read line from remote."""
1237 return self.readfile.readline()
1238
1239
1240 - def send(self, data):
1241 """Send data to remote."""
1242 self.writefile.write(data)
1243 self.writefile.flush()
1244
1245
1247 """Close I/O established in "open"."""
1248 self.readfile.close()
1249 self.writefile.close()
1250
1251
1252
1254
1255 """Private class to provide en/decoding
1256 for base64-based authentication conversation.
1257 """
1258
1260 self.mech = mechinst
1261
1263 ret = self.mech(self.decode(data))
1264 if ret is None:
1265 return '*'
1266 return self.encode(ret)
1267
1269
1270
1271
1272
1273
1274
1275
1276
1277 oup = ''
1278 while inp:
1279 if len(inp) > 48:
1280 t = inp[:48]
1281 inp = inp[48:]
1282 else:
1283 t = inp
1284 inp = ''
1285 e = binascii.b2a_base64(t)
1286 if e:
1287 oup = oup + e[:-1]
1288 return oup
1289
1291 if not inp:
1292 return ''
1293 return binascii.a2b_base64(inp)
1294
1295
1296
1297 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1298 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1299
1301 """Convert IMAP4 INTERNALDATE to UT.
1302
1303 Returns Python time module tuple.
1304 """
1305
1306 mo = InternalDate.match(resp)
1307 if not mo:
1308 return None
1309
1310 mon = Mon2num[mo.group('mon')]
1311 zonen = mo.group('zonen')
1312
1313 day = int(mo.group('day'))
1314 year = int(mo.group('year'))
1315 hour = int(mo.group('hour'))
1316 min = int(mo.group('min'))
1317 sec = int(mo.group('sec'))
1318 zoneh = int(mo.group('zoneh'))
1319 zonem = int(mo.group('zonem'))
1320
1321
1322
1323 zone = (zoneh*60 + zonem)*60
1324 if zonen == '-':
1325 zone = -zone
1326
1327 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1328
1329 utc = time.mktime(tt)
1330
1331
1332
1333
1334 lt = time.localtime(utc)
1335 if time.daylight and lt[-1]:
1336 zone = zone + time.altzone
1337 else:
1338 zone = zone + time.timezone
1339
1340 return time.localtime(utc - zone)
1341
1342
1343
1345
1346 """Convert integer to A-P string representation."""
1347
1348 val = ''; AP = 'ABCDEFGHIJKLMNOP'
1349 num = int(abs(num))
1350 while num:
1351 num, mod = divmod(num, 16)
1352 val = AP[mod] + val
1353 return val
1354
1355
1356
1358
1359 """Convert IMAP4 flags response to python tuple."""
1360
1361 mo = Flags.match(resp)
1362 if not mo:
1363 return ()
1364
1365 return tuple(mo.group('flags').split())
1366
1367
1369
1370 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1371
1372 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1373 """
1374
1375 if isinstance(date_time, (int, float)):
1376 tt = time.localtime(date_time)
1377 elif isinstance(date_time, (tuple, time.struct_time)):
1378 tt = date_time
1379 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1380 return date_time
1381 else:
1382 raise ValueError("date_time not of a known type")
1383
1384 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1385 if dt[0] == '0':
1386 dt = ' ' + dt[1:]
1387 if time.daylight and tt[-1]:
1388 zone = -time.altzone
1389 else:
1390 zone = -time.timezone
1391 return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
1392
1393
1394
1395 if __name__ == '__main__':
1396
1397
1398
1399
1400
1401 import getopt, getpass
1402
1403 try:
1404 optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
1405 except getopt.error, val:
1406 optlist, args = (), ()
1407
1408 stream_command = None
1409 for opt,val in optlist:
1410 if opt == '-d':
1411 Debug = int(val)
1412 elif opt == '-s':
1413 stream_command = val
1414 if not args: args = (stream_command,)
1415
1416 if not args: args = ('',)
1417
1418 host = args[0]
1419
1420 USER = getpass.getuser()
1421 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1422
1423 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
1424 test_seq1 = (
1425 ('login', (USER, PASSWD)),
1426 ('create', ('/tmp/xxx 1',)),
1427 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1428 ('CREATE', ('/tmp/yyz 2',)),
1429 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1430 ('list', ('/tmp', 'yy*')),
1431 ('select', ('/tmp/yyz 2',)),
1432 ('search', (None, 'SUBJECT', 'test')),
1433 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1434 ('store', ('1', 'FLAGS', '(\Deleted)')),
1435 ('namespace', ()),
1436 ('expunge', ()),
1437 ('recent', ()),
1438 ('close', ()),
1439 )
1440
1441 test_seq2 = (
1442 ('select', ()),
1443 ('response',('UIDVALIDITY',)),
1444 ('uid', ('SEARCH', 'ALL')),
1445 ('response', ('EXISTS',)),
1446 ('append', (None, None, None, test_mesg)),
1447 ('recent', ()),
1448 ('logout', ()),
1449 )
1450
1451 - def run(cmd, args):
1452 M._mesg('%s %s' % (cmd, args))
1453 typ, dat = getattr(M, cmd)(*args)
1454 M._mesg('%s => %s %s' % (cmd, typ, dat))
1455 if typ == 'NO': raise dat[0]
1456 return dat
1457
1458 try:
1459 if stream_command:
1460 M = IMAP4_stream(stream_command)
1461 else:
1462 M = IMAP4(host)
1463 if M.state == 'AUTH':
1464 test_seq1 = test_seq1[1:]
1465 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1466 M._mesg('CAPABILITIES = %r' % (M.capabilities,))
1467
1468 for cmd,args in test_seq1:
1469 run(cmd, args)
1470
1471 for ml in run('list', ('/tmp/', 'yy%')):
1472 mo = re.match(r'.*"([^"]+)"$', ml)
1473 if mo: path = mo.group(1)
1474 else: path = ml.split()[-1]
1475 run('delete', (path,))
1476
1477 for cmd,args in test_seq2:
1478 dat = run(cmd, args)
1479
1480 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1481 continue
1482
1483 uid = dat[-1].split()
1484 if not uid: continue
1485 run('uid', ('FETCH', '%s' % uid[-1],
1486 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1487
1488 print '\nAll tests OK.'
1489
1490 except:
1491 print '\nTests failed.'
1492
1493 if not Debug:
1494 print '''
1495 If you would like to see debugging output,
1496 try: %s -d5
1497 ''' % sys.argv[0]
1498
1499 raise
1500