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