]> code.delx.au - offlineimap/blob - offlineimap/imaplib.py
a4bdc22d6b81273afb25f036987e72e95758dcb8
[offlineimap] / offlineimap / imaplib.py
1 """IMAP4 client.
2
3 Based on RFC 2060.
4
5 Public class: IMAP4
6 Public variable: Debug
7 Public functions: Internaldate2tuple
8 Internaldate2epoch
9 Int2AP
10 ParseFlags
11 Time2Internaldate
12 """
13
14 # Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
15 #
16 # Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
17 # String method conversion by ESR, February 2001.
18 # GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
19 # IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
20 # GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
21 # IMAP4_Tunnel contributed by John Goerzen <jgoerzen@complete.org> July 2002
22
23 __version__ = "2.52"
24
25 import binascii, re, socket, time, random, sys, os
26 from offlineimap.ui import UIBase
27
28 __all__ = ["IMAP4", "Internaldate2tuple", "Internaldate2epoch",
29 "Int2AP", "ParseFlags", "Time2Internaldate"]
30
31 # Globals
32
33 CRLF = '\r\n'
34 Debug = 0
35 IMAP4_PORT = 143
36 IMAP4_SSL_PORT = 993
37 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
38
39 # Commands
40
41 Commands = {
42 # name valid states
43 'APPEND': ('AUTH', 'SELECTED'),
44 'AUTHENTICATE': ('NONAUTH',),
45 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
46 'CHECK': ('SELECTED',),
47 'CLOSE': ('SELECTED',),
48 'COPY': ('SELECTED',),
49 'CREATE': ('AUTH', 'SELECTED'),
50 'DELETE': ('AUTH', 'SELECTED'),
51 'EXAMINE': ('AUTH', 'SELECTED'),
52 'EXPUNGE': ('SELECTED',),
53 'FETCH': ('SELECTED',),
54 'GETACL': ('AUTH', 'SELECTED'),
55 'GETQUOTA': ('AUTH', 'SELECTED'),
56 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
57 'LIST': ('AUTH', 'SELECTED'),
58 'LOGIN': ('NONAUTH',),
59 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
60 'LSUB': ('AUTH', 'SELECTED'),
61 'NAMESPACE': ('AUTH', 'SELECTED'),
62 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
63 'PARTIAL': ('SELECTED',), # NB: obsolete
64 'RENAME': ('AUTH', 'SELECTED'),
65 'SEARCH': ('SELECTED',),
66 'SELECT': ('AUTH', 'SELECTED'),
67 'SETACL': ('AUTH', 'SELECTED'),
68 'SETQUOTA': ('AUTH', 'SELECTED'),
69 'SORT': ('SELECTED',),
70 'STATUS': ('AUTH', 'SELECTED'),
71 'STORE': ('SELECTED',),
72 'SUBSCRIBE': ('AUTH', 'SELECTED'),
73 'UID': ('SELECTED',),
74 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
75 }
76
77 # Patterns to match server responses
78
79 Continuation = re.compile(r'\+( (?P<data>.*))?')
80 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
81 InternalDate = re.compile(r'.*INTERNALDATE "'
82 r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
83 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
84 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
85 r'"')
86 Literal = re.compile(r'.*{(?P<size>\d+)}$')
87 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
88 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
89 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
90
91
92
93 class IMAP4:
94
95 """IMAP4 client class.
96
97 Instantiate with: IMAP4([host[, port]])
98
99 host - host's name (default: localhost);
100 port - port number (default: standard IMAP4 port).
101
102 All IMAP4rev1 commands are supported by methods of the same
103 name (in lower-case).
104
105 All arguments to commands are converted to strings, except for
106 AUTHENTICATE, and the last argument to APPEND which is passed as
107 an IMAP4 literal. If necessary (the string contains any
108 non-printing characters or white-space and isn't enclosed with
109 either parentheses or double quotes) each string is quoted.
110 However, the 'password' argument to the LOGIN command is always
111 quoted. If you want to avoid having an argument string quoted
112 (eg: the 'flags' argument to STORE) then enclose the string in
113 parentheses (eg: "(\Deleted)").
114
115 Each command returns a tuple: (type, [data, ...]) where 'type'
116 is usually 'OK' or 'NO', and 'data' is either the text from the
117 tagged response, or untagged results from command.
118
119 Errors raise the exception class <instance>.error("<reason>").
120 IMAP4 server errors raise <instance>.abort("<reason>"),
121 which is a sub-class of 'error'. Mailbox status changes
122 from READ-WRITE to READ-ONLY raise the exception class
123 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
124
125 "error" exceptions imply a program error.
126 "abort" exceptions imply the connection should be reset, and
127 the command re-tried.
128 "readonly" exceptions imply the command should be re-tried.
129
130 Note: to use this module, you must read the RFCs pertaining
131 to the IMAP4 protocol, as the semantics of the arguments to
132 each IMAP4 command are left to the invoker, not to mention
133 the results.
134 """
135
136 class error(Exception): pass # Logical errors - debug required
137 class abort(error): pass # Service errors - close and retry
138 class readonly(abort): pass # Mailbox status changed to READ-ONLY
139
140 mustquote = re.compile(r"[^\w!#$%&'+,.:;<=>?^`|~-]")
141
142 def __init__(self, host = '', port = IMAP4_PORT):
143 self.debug = Debug
144 self.state = 'LOGOUT'
145 self.literal = None # A literal argument to a command
146 self.tagged_commands = {} # Tagged commands awaiting response
147 self.untagged_responses = {} # {typ: [data, ...], ...}
148 self.continuation_response = '' # Last continuation response
149 self.is_readonly = None # READ-ONLY desired state
150 self.tagnum = 0
151
152 # Open socket to server.
153
154 self.open(host, port)
155
156 # Create unique tag for this session,
157 # and compile tagged response matcher.
158
159 self.tagpre = Int2AP(random.randint(0, 31999))
160 self.tagre = re.compile(r'(?P<tag>'
161 + self.tagpre
162 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
163
164 # Get server welcome message,
165 # request and store CAPABILITY response.
166
167 if __debug__:
168 self._cmd_log_len = 10
169 self._cmd_log_idx = 0
170 self._cmd_log = {} # Last `_cmd_log_len' interactions
171 if self.debug >= 1:
172 self._mesg('imaplib version %s' % __version__)
173 self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
174
175 self.welcome = self._get_response()
176 if 'PREAUTH' in self.untagged_responses:
177 self.state = 'AUTH'
178 elif 'OK' in self.untagged_responses:
179 self.state = 'NONAUTH'
180 else:
181 raise self.error(self.welcome)
182
183 cap = 'CAPABILITY'
184 self._simple_command(cap)
185 if not cap in self.untagged_responses:
186 raise self.error('no CAPABILITY response from server')
187 self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split())
188
189 if __debug__:
190 if self.debug >= 3:
191 self._mesg('CAPABILITIES: %s' % `self.capabilities`)
192
193 for version in AllowedVersions:
194 if not version in self.capabilities:
195 continue
196 self.PROTOCOL_VERSION = version
197 return
198
199 raise self.error('server not IMAP4 compliant')
200
201
202 def __getattr__(self, attr):
203 # Allow UPPERCASE variants of IMAP4 command methods.
204 if attr in Commands:
205 return getattr(self, attr.lower())
206 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
207
208
209
210 # Overridable methods
211
212
213 def open(self, host = '', port = IMAP4_PORT):
214 """Setup connection to remote server on "host:port"
215 (default: localhost:standard IMAP4 port).
216 This connection will be used by the routines:
217 read, readline, send, shutdown.
218 """
219 self.host = host
220 self.port = port
221 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.outfd, self.infd = os.popen2(host, "t", 0)
1053
1054 def read(self, size):
1055 retval = ''
1056 while len(retval) < size:
1057 retval += self.infd.read(size - len(retval))
1058 return retval
1059
1060 def readline(self):
1061 return self.infd.readline()
1062
1063 def send(self, data):
1064 self.outfd.write(data)
1065
1066 def shutdown(self):
1067 self.infd.close()
1068 self.outfd.close()
1069
1070
1071 class sslwrapper:
1072 def __init__(self, sslsock):
1073 self.sslsock = sslsock
1074 self.readbuf = ''
1075
1076 def write(self, s):
1077 return self.sslsock.write(s)
1078
1079 def _read(self, n):
1080 return self.sslsock.read(n)
1081
1082 def read(self, n):
1083 if len(self.readbuf):
1084 # Return the stuff in readbuf, even if less than n.
1085 # It might contain the rest of the line, and if we try to
1086 # read more, might block waiting for data that is not
1087 # coming to arrive.
1088 bytesfrombuf = min(n, len(self.readbuf))
1089 retval = self.readbuf[:bytesfrombuf]
1090 self.readbuf = self.readbuf[bytesfrombuf:]
1091 return retval
1092 retval = self._read(n)
1093 if len(retval) > n:
1094 self.readbuf = retval[n:]
1095 return retval[:n]
1096 return retval
1097
1098 def readline(self):
1099 retval = ''
1100 while 1:
1101 linebuf = self.read(1024)
1102 nlindex = linebuf.find("\n")
1103 if nlindex != -1:
1104 retval += linebuf[:nlindex + 1]
1105 self.readbuf = linebuf[nlindex + 1:] + self.readbuf
1106 return retval
1107 else:
1108 retval += linebuf
1109
1110
1111 class IMAP4_SSL(IMAP4):
1112
1113 """IMAP4 client class over SSL connection
1114
1115 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1116
1117 host - host's name (default: localhost);
1118 port - port number (default: standard IMAP4 SSL port).
1119 keyfile - PEM formatted file that contains your private key (default: None);
1120 certfile - PEM formatted certificate chain file (default: None);
1121
1122 for more documentation see the docstring of the parent class IMAP4.
1123 """
1124
1125
1126 def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
1127 self.keyfile = keyfile
1128 self.certfile = certfile
1129 IMAP4.__init__(self, host, port)
1130
1131
1132 def open(self, host = '', port = IMAP4_SSL_PORT):
1133 """Setup connection to remote server on "host:port".
1134 (default: localhost:standard IMAP4 SSL port).
1135 This connection will be used by the routines:
1136 read, readline, send, shutdown.
1137 """
1138 self.host = host
1139 self.port = port
1140 #This connects to the first ip found ipv4/ipv6
1141 #Added by Adriaan Peeters <apeeters@lashout.net> based on a socket
1142 #example from the python documentation:
1143 #http://www.python.org/doc/lib/socket-example.html
1144 res = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
1145 socket.SOCK_STREAM)
1146 # Try all the addresses in turn until we connect()
1147 last_error = 0
1148 for remote in res:
1149 af, socktype, proto, canonname, sa = remote
1150 self.sock = socket.socket(af, socktype, proto)
1151 last_error = self.sock.connect_ex(sa)
1152 print af
1153 if last_error == 0:
1154 break
1155 else:
1156 self.sock.close()
1157 if last_error != 0:
1158 # FIXME
1159 raise socket.error(last_error)
1160 if sys.version_info[0] <= 2 and sys.version_info[1] <= 2:
1161 self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile)
1162 else:
1163 self.sslobj = socket.ssl(self.sock._sock, self.keyfile, self.certfile)
1164 self.sslobj = sslwrapper(self.sslobj)
1165
1166
1167 def read(self, size):
1168 """Read 'size' bytes from remote."""
1169 retval = ''
1170 while len(retval) < size:
1171 retval += self.sslobj.read(size - len(retval))
1172 return retval
1173
1174
1175 def readline(self):
1176 """Read line from remote."""
1177 return self.sslobj.readline()
1178
1179 def send(self, data):
1180 """Send data to remote."""
1181 byteswritten = 0
1182 bytestowrite = len(data)
1183 while byteswritten < bytestowrite:
1184 byteswritten += self.sslobj.write(data[byteswritten:])
1185
1186
1187 def shutdown(self):
1188 """Close I/O established in "open"."""
1189 self.sock.close()
1190
1191
1192 def socket(self):
1193 """Return socket instance used to connect to IMAP4 server.
1194
1195 socket = <instance>.socket()
1196 """
1197 return self.sock
1198
1199
1200 def ssl(self):
1201 """Return SSLObject instance used to communicate with the IMAP4 server.
1202
1203 ssl = <instance>.socket.ssl()
1204 """
1205 return self.sslobj
1206
1207
1208
1209 class _Authenticator:
1210
1211 """Private class to provide en/decoding
1212 for base64-based authentication conversation.
1213 """
1214
1215 def __init__(self, mechinst):
1216 self.mech = mechinst # Callable object to provide/process data
1217
1218 def process(self, data):
1219 ret = self.mech(self.decode(data))
1220 if ret is None:
1221 return '*' # Abort conversation
1222 return self.encode(ret)
1223
1224 def encode(self, inp):
1225 #
1226 # Invoke binascii.b2a_base64 iteratively with
1227 # short even length buffers, strip the trailing
1228 # line feed from the result and append. "Even"
1229 # means a number that factors to both 6 and 8,
1230 # so when it gets to the end of the 8-bit input
1231 # there's no partial 6-bit output.
1232 #
1233 oup = ''
1234 while inp:
1235 if len(inp) > 48:
1236 t = inp[:48]
1237 inp = inp[48:]
1238 else:
1239 t = inp
1240 inp = ''
1241 e = binascii.b2a_base64(t)
1242 if e:
1243 oup = oup + e[:-1]
1244 return oup
1245
1246 def decode(self, inp):
1247 if not inp:
1248 return ''
1249 return binascii.a2b_base64(inp)
1250
1251
1252
1253 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1254 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1255
1256 def Internaldate2epoch(resp):
1257 """Convert IMAP4 INTERNALDATE to UT.
1258
1259 Returns seconds since the epoch.
1260 """
1261
1262 mo = InternalDate.match(resp)
1263 if not mo:
1264 return None
1265
1266 mon = Mon2num[mo.group('mon')]
1267 zonen = mo.group('zonen')
1268
1269 day = int(mo.group('day'))
1270 year = int(mo.group('year'))
1271 hour = int(mo.group('hour'))
1272 min = int(mo.group('min'))
1273 sec = int(mo.group('sec'))
1274 zoneh = int(mo.group('zoneh'))
1275 zonem = int(mo.group('zonem'))
1276
1277 # INTERNALDATE timezone must be subtracted to get UT
1278
1279 zone = (zoneh*60 + zonem)*60
1280 if zonen == '-':
1281 zone = -zone
1282
1283 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1284
1285 return time.mktime(tt)
1286
1287
1288 def Internaldate2tuple(resp):
1289 """Convert IMAP4 INTERNALDATE to UT.
1290
1291 Returns Python time module tuple.
1292 """
1293
1294 utc = Internaldate2epoch(resp)
1295
1296 # Following is necessary because the time module has no 'mkgmtime'.
1297 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1298
1299 lt = time.localtime(utc)
1300 if time.daylight and lt[-1]:
1301 zone = zone + time.altzone
1302 else:
1303 zone = zone + time.timezone
1304
1305 return time.localtime(utc - zone)
1306
1307
1308
1309 def Int2AP(num):
1310
1311 """Convert integer to A-P string representation."""
1312
1313 val = ''; AP = 'ABCDEFGHIJKLMNOP'
1314 num = int(abs(num))
1315 while num:
1316 num, mod = divmod(num, 16)
1317 val = AP[mod] + val
1318 return val
1319
1320
1321
1322 def ParseFlags(resp):
1323
1324 """Convert IMAP4 flags response to python tuple."""
1325
1326 mo = Flags.match(resp)
1327 if not mo:
1328 return ()
1329
1330 return tuple(mo.group('flags').split())
1331
1332
1333 def Time2Internaldate(date_time):
1334
1335 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1336
1337 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1338 """
1339
1340 if isinstance(date_time, (int, float)):
1341 tt = time.localtime(date_time)
1342 elif isinstance(date_time, (tuple, time.struct_time)):
1343 tt = date_time
1344 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1345 return date_time # Assume in correct format
1346 else:
1347 raise ValueError("date_time not of a known type")
1348
1349 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1350 if dt[0] == '0':
1351 dt = ' ' + dt[1:]
1352 if time.daylight and tt[-1]:
1353 zone = -time.altzone
1354 else:
1355 zone = -time.timezone
1356 return '"' + dt + " %+03d%02d" % divmod(zone/60, 60) + '"'
1357
1358
1359
1360 if __name__ == '__main__':
1361
1362 import getopt, getpass
1363
1364 try:
1365 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1366 except getopt.error, val:
1367 pass
1368
1369 for opt,val in optlist:
1370 if opt == '-d':
1371 Debug = int(val)
1372
1373 if not args: args = ('',)
1374
1375 host = args[0]
1376
1377 USER = getpass.getuser()
1378 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1379
1380 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':CRLF}
1381 test_seq1 = (
1382 ('login', (USER, PASSWD)),
1383 ('create', ('/tmp/xxx 1',)),
1384 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1385 ('CREATE', ('/tmp/yyz 2',)),
1386 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1387 ('list', ('/tmp', 'yy*')),
1388 ('select', ('/tmp/yyz 2',)),
1389 ('search', (None, 'SUBJECT', 'test')),
1390 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1391 ('store', ('1', 'FLAGS', '(\Deleted)')),
1392 ('namespace', ()),
1393 ('expunge', ()),
1394 ('recent', ()),
1395 ('close', ()),
1396 )
1397
1398 test_seq2 = (
1399 ('select', ()),
1400 ('response',('UIDVALIDITY',)),
1401 ('uid', ('SEARCH', 'ALL')),
1402 ('response', ('EXISTS',)),
1403 ('append', (None, None, None, test_mesg)),
1404 ('recent', ()),
1405 ('logout', ()),
1406 )
1407
1408 def run(cmd, args):
1409 M._mesg('%s %s' % (cmd, args))
1410 typ, dat = apply(getattr(M, cmd), args)
1411 M._mesg('%s => %s %s' % (cmd, typ, dat))
1412 return dat
1413
1414 try:
1415 M = IMAP4(host)
1416 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1417 M._mesg('CAPABILITIES = %s' % `M.capabilities`)
1418
1419 for cmd,args in test_seq1:
1420 run(cmd, args)
1421
1422 for ml in run('list', ('/tmp/', 'yy%')):
1423 mo = re.match(r'.*"([^"]+)"$', ml)
1424 if mo: path = mo.group(1)
1425 else: path = ml.split()[-1]
1426 run('delete', (path,))
1427
1428 for cmd,args in test_seq2:
1429 dat = run(cmd, args)
1430
1431 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1432 continue
1433
1434 uid = dat[-1].split()
1435 if not uid: continue
1436 run('uid', ('FETCH', '%s' % uid[-1],
1437 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1438
1439 print '\nAll tests OK.'
1440
1441 except:
1442 print '\nTests failed.'
1443
1444 if not Debug:
1445 print '''
1446 If you would like to see debugging output,
1447 try: %s -d5
1448 ''' % sys.argv[0]
1449
1450 raise