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