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