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