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