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