source: python/vendor/Python-2.6.5/Lib/imaplib.py

Last change on this file was 2, checked in by Yuri Dario, 15 years ago

Initial import for vendor code.

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