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