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