svn-gvsig-desktop / tags / v1_0_2_Build_905 / extensions / extScripting / scripts / jython / Lib / imaplib.py @ 10767
History | View | Annotate | Download (33.2 KB)
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 |
|
18 |
__version__ = "2.40"
|
19 |
|
20 |
import binascii, re, socket, time, random, sys |
21 |
|
22 |
__all__ = ["IMAP4", "Internaldate2tuple", |
23 |
"Int2AP", "ParseFlags", "Time2Internaldate"] |
24 |
|
25 |
# Globals
|
26 |
|
27 |
CRLF = '\r\n'
|
28 |
Debug = 0
|
29 |
IMAP4_PORT = 143
|
30 |
AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first |
31 |
|
32 |
# Commands
|
33 |
|
34 |
Commands = { |
35 |
# name valid states
|
36 |
'APPEND': ('AUTH', 'SELECTED'), |
37 |
'AUTHENTICATE': ('NONAUTH',), |
38 |
'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), |
39 |
'CHECK': ('SELECTED',), |
40 |
'CLOSE': ('SELECTED',), |
41 |
'COPY': ('SELECTED',), |
42 |
'CREATE': ('AUTH', 'SELECTED'), |
43 |
'DELETE': ('AUTH', 'SELECTED'), |
44 |
'EXAMINE': ('AUTH', 'SELECTED'), |
45 |
'EXPUNGE': ('SELECTED',), |
46 |
'FETCH': ('SELECTED',), |
47 |
'LIST': ('AUTH', 'SELECTED'), |
48 |
'LOGIN': ('NONAUTH',), |
49 |
'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), |
50 |
'LSUB': ('AUTH', 'SELECTED'), |
51 |
'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), |
52 |
'PARTIAL': ('SELECTED',), |
53 |
'RENAME': ('AUTH', 'SELECTED'), |
54 |
'SEARCH': ('SELECTED',), |
55 |
'SELECT': ('AUTH', 'SELECTED'), |
56 |
'STATUS': ('AUTH', 'SELECTED'), |
57 |
'STORE': ('SELECTED',), |
58 |
'SUBSCRIBE': ('AUTH', 'SELECTED'), |
59 |
'UID': ('SELECTED',), |
60 |
'UNSUBSCRIBE': ('AUTH', 'SELECTED'), |
61 |
} |
62 |
|
63 |
# Patterns to match server responses
|
64 |
|
65 |
Continuation = re.compile(r'\+( (?P<data>.*))?')
|
66 |
Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
|
67 |
InternalDate = re.compile(r'.*INTERNALDATE "'
|
68 |
r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
|
69 |
r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
|
70 |
r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
|
71 |
r'"')
|
72 |
Literal = re.compile(r'.*{(?P<size>\d+)}$')
|
73 |
Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
|
74 |
Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
|
75 |
Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
|
76 |
|
77 |
|
78 |
|
79 |
class IMAP4: |
80 |
|
81 |
"""IMAP4 client class.
|
82 |
|
83 |
Instantiate with: IMAP4([host[, port]])
|
84 |
|
85 |
host - host's name (default: localhost);
|
86 |
port - port number (default: standard IMAP4 port).
|
87 |
|
88 |
All IMAP4rev1 commands are supported by methods of the same
|
89 |
name (in lower-case).
|
90 |
|
91 |
All arguments to commands are converted to strings, except for
|
92 |
AUTHENTICATE, and the last argument to APPEND which is passed as
|
93 |
an IMAP4 literal. If necessary (the string contains any
|
94 |
non-printing characters or white-space and isn't enclosed with
|
95 |
either parentheses or double quotes) each string is quoted.
|
96 |
However, the 'password' argument to the LOGIN command is always
|
97 |
quoted. If you want to avoid having an argument string quoted
|
98 |
(eg: the 'flags' argument to STORE) then enclose the string in
|
99 |
parentheses (eg: "(\Deleted)").
|
100 |
|
101 |
Each command returns a tuple: (type, [data, ...]) where 'type'
|
102 |
is usually 'OK' or 'NO', and 'data' is either the text from the
|
103 |
tagged response, or untagged results from command.
|
104 |
|
105 |
Errors raise the exception class <instance>.error("<reason>").
|
106 |
IMAP4 server errors raise <instance>.abort("<reason>"),
|
107 |
which is a sub-class of 'error'. Mailbox status changes
|
108 |
from READ-WRITE to READ-ONLY raise the exception class
|
109 |
<instance>.readonly("<reason>"), which is a sub-class of 'abort'.
|
110 |
|
111 |
"error" exceptions imply a program error.
|
112 |
"abort" exceptions imply the connection should be reset, and
|
113 |
the command re-tried.
|
114 |
"readonly" exceptions imply the command should be re-tried.
|
115 |
|
116 |
Note: to use this module, you must read the RFCs pertaining
|
117 |
to the IMAP4 protocol, as the semantics of the arguments to
|
118 |
each IMAP4 command are left to the invoker, not to mention
|
119 |
the results.
|
120 |
"""
|
121 |
|
122 |
class error(Exception): pass # Logical errors - debug required |
123 |
class abort(error): pass # Service errors - close and retry |
124 |
class readonly(abort): pass # Mailbox status changed to READ-ONLY |
125 |
|
126 |
mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
|
127 |
|
128 |
def __init__(self, host = '', port = IMAP4_PORT): |
129 |
self.host = host
|
130 |
self.port = port
|
131 |
self.debug = Debug
|
132 |
self.state = 'LOGOUT' |
133 |
self.literal = None # A literal argument to a command |
134 |
self.tagged_commands = {} # Tagged commands awaiting response |
135 |
self.untagged_responses = {} # {typ: [data, ...], ...} |
136 |
self.continuation_response = '' # Last continuation response |
137 |
self.is_readonly = None # READ-ONLY desired state |
138 |
self.tagnum = 0 |
139 |
|
140 |
# Open socket to server.
|
141 |
|
142 |
self.open(host, port)
|
143 |
|
144 |
# Create unique tag for this session,
|
145 |
# and compile tagged response matcher.
|
146 |
|
147 |
self.tagpre = Int2AP(random.randint(0, 31999)) |
148 |
self.tagre = re.compile(r'(?P<tag>' |
149 |
+ self.tagpre
|
150 |
+ r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
|
151 |
|
152 |
# Get server welcome message,
|
153 |
# request and store CAPABILITY response.
|
154 |
|
155 |
if __debug__:
|
156 |
if self.debug >= 1: |
157 |
_mesg('new IMAP4 connection, tag=%s' % self.tagpre) |
158 |
|
159 |
self.welcome = self._get_response() |
160 |
if self.untagged_responses.has_key('PREAUTH'): |
161 |
self.state = 'AUTH' |
162 |
elif self.untagged_responses.has_key('OK'): |
163 |
self.state = 'NONAUTH' |
164 |
else:
|
165 |
raise self.error(self.welcome) |
166 |
|
167 |
cap = 'CAPABILITY'
|
168 |
self._simple_command(cap)
|
169 |
if not self.untagged_responses.has_key(cap): |
170 |
raise self.error('no CAPABILITY response from server') |
171 |
self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split()) |
172 |
|
173 |
if __debug__:
|
174 |
if self.debug >= 3: |
175 |
_mesg('CAPABILITIES: %s' % `self.capabilities`) |
176 |
|
177 |
for version in AllowedVersions: |
178 |
if not version in self.capabilities: |
179 |
continue
|
180 |
self.PROTOCOL_VERSION = version
|
181 |
return
|
182 |
|
183 |
raise self.error('server not IMAP4 compliant') |
184 |
|
185 |
|
186 |
def __getattr__(self, attr): |
187 |
# Allow UPPERCASE variants of IMAP4 command methods.
|
188 |
if Commands.has_key(attr):
|
189 |
return eval("self.%s" % attr.lower()) |
190 |
raise AttributeError("Unknown IMAP4 command: '%s'" % attr) |
191 |
|
192 |
|
193 |
|
194 |
# Public methods
|
195 |
|
196 |
|
197 |
def open(self, host, port): |
198 |
"""Setup 'self.sock' and 'self.file'."""
|
199 |
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
200 |
self.sock.connect((self.host, self.port)) |
201 |
self.file = self.sock.makefile('rb') |
202 |
|
203 |
|
204 |
def recent(self): |
205 |
"""Return most recent 'RECENT' responses if any exist,
|
206 |
else prompt server for an update using the 'NOOP' command.
|
207 |
|
208 |
(typ, [data]) = <instance>.recent()
|
209 |
|
210 |
'data' is None if no new messages,
|
211 |
else list of RECENT responses, most recent last.
|
212 |
"""
|
213 |
name = 'RECENT'
|
214 |
typ, dat = self._untagged_response('OK', [None], name) |
215 |
if dat[-1]: |
216 |
return typ, dat
|
217 |
typ, dat = self.noop() # Prod server for response |
218 |
return self._untagged_response(typ, dat, name) |
219 |
|
220 |
|
221 |
def response(self, code): |
222 |
"""Return data for response 'code' if received, or None.
|
223 |
|
224 |
Old value for response 'code' is cleared.
|
225 |
|
226 |
(code, [data]) = <instance>.response(code)
|
227 |
"""
|
228 |
return self._untagged_response(code, [None], code.upper()) |
229 |
|
230 |
|
231 |
def socket(self): |
232 |
"""Return socket instance used to connect to IMAP4 server.
|
233 |
|
234 |
socket = <instance>.socket()
|
235 |
"""
|
236 |
return self.sock |
237 |
|
238 |
|
239 |
|
240 |
# IMAP4 commands
|
241 |
|
242 |
|
243 |
def append(self, mailbox, flags, date_time, message): |
244 |
"""Append message to named mailbox.
|
245 |
|
246 |
(typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
|
247 |
|
248 |
All args except `message' can be None.
|
249 |
"""
|
250 |
name = 'APPEND'
|
251 |
if not mailbox: |
252 |
mailbox = 'INBOX'
|
253 |
if flags:
|
254 |
if (flags[0],flags[-1]) != ('(',')'): |
255 |
flags = '(%s)' % flags
|
256 |
else:
|
257 |
flags = None
|
258 |
if date_time:
|
259 |
date_time = Time2Internaldate(date_time) |
260 |
else:
|
261 |
date_time = None
|
262 |
self.literal = message
|
263 |
return self._simple_command(name, mailbox, flags, date_time) |
264 |
|
265 |
|
266 |
def authenticate(self, mechanism, authobject): |
267 |
"""Authenticate command - requires response processing.
|
268 |
|
269 |
'mechanism' specifies which authentication mechanism is to
|
270 |
be used - it must appear in <instance>.capabilities in the
|
271 |
form AUTH=<mechanism>.
|
272 |
|
273 |
'authobject' must be a callable object:
|
274 |
|
275 |
data = authobject(response)
|
276 |
|
277 |
It will be called to process server continuation responses.
|
278 |
It should return data that will be encoded and sent to server.
|
279 |
It should return None if the client abort response '*' should
|
280 |
be sent instead.
|
281 |
"""
|
282 |
mech = mechanism.upper() |
283 |
cap = 'AUTH=%s' % mech
|
284 |
if not cap in self.capabilities: |
285 |
raise self.error("Server doesn't allow %s authentication." % mech) |
286 |
self.literal = _Authenticator(authobject).process
|
287 |
typ, dat = self._simple_command('AUTHENTICATE', mech) |
288 |
if typ != 'OK': |
289 |
raise self.error(dat[-1]) |
290 |
self.state = 'AUTH' |
291 |
return typ, dat
|
292 |
|
293 |
|
294 |
def check(self): |
295 |
"""Checkpoint mailbox on server.
|
296 |
|
297 |
(typ, [data]) = <instance>.check()
|
298 |
"""
|
299 |
return self._simple_command('CHECK') |
300 |
|
301 |
|
302 |
def close(self): |
303 |
"""Close currently selected mailbox.
|
304 |
|
305 |
Deleted messages are removed from writable mailbox.
|
306 |
This is the recommended command before 'LOGOUT'.
|
307 |
|
308 |
(typ, [data]) = <instance>.close()
|
309 |
"""
|
310 |
try:
|
311 |
typ, dat = self._simple_command('CLOSE') |
312 |
finally:
|
313 |
self.state = 'AUTH' |
314 |
return typ, dat
|
315 |
|
316 |
|
317 |
def copy(self, message_set, new_mailbox): |
318 |
"""Copy 'message_set' messages onto end of 'new_mailbox'.
|
319 |
|
320 |
(typ, [data]) = <instance>.copy(message_set, new_mailbox)
|
321 |
"""
|
322 |
return self._simple_command('COPY', message_set, new_mailbox) |
323 |
|
324 |
|
325 |
def create(self, mailbox): |
326 |
"""Create new mailbox.
|
327 |
|
328 |
(typ, [data]) = <instance>.create(mailbox)
|
329 |
"""
|
330 |
return self._simple_command('CREATE', mailbox) |
331 |
|
332 |
|
333 |
def delete(self, mailbox): |
334 |
"""Delete old mailbox.
|
335 |
|
336 |
(typ, [data]) = <instance>.delete(mailbox)
|
337 |
"""
|
338 |
return self._simple_command('DELETE', mailbox) |
339 |
|
340 |
|
341 |
def expunge(self): |
342 |
"""Permanently remove deleted items from selected mailbox.
|
343 |
|
344 |
Generates 'EXPUNGE' response for each deleted message.
|
345 |
|
346 |
(typ, [data]) = <instance>.expunge()
|
347 |
|
348 |
'data' is list of 'EXPUNGE'd message numbers in order received.
|
349 |
"""
|
350 |
name = 'EXPUNGE'
|
351 |
typ, dat = self._simple_command(name)
|
352 |
return self._untagged_response(typ, dat, name) |
353 |
|
354 |
|
355 |
def fetch(self, message_set, message_parts): |
356 |
"""Fetch (parts of) messages.
|
357 |
|
358 |
(typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
|
359 |
|
360 |
'message_parts' should be a string of selected parts
|
361 |
enclosed in parentheses, eg: "(UID BODY[TEXT])".
|
362 |
|
363 |
'data' are tuples of message part envelope and data.
|
364 |
"""
|
365 |
name = 'FETCH'
|
366 |
typ, dat = self._simple_command(name, message_set, message_parts)
|
367 |
return self._untagged_response(typ, dat, name) |
368 |
|
369 |
|
370 |
def list(self, directory='""', pattern='*'): |
371 |
"""List mailbox names in directory matching pattern.
|
372 |
|
373 |
(typ, [data]) = <instance>.list(directory='""', pattern='*')
|
374 |
|
375 |
'data' is list of LIST responses.
|
376 |
"""
|
377 |
name = 'LIST'
|
378 |
typ, dat = self._simple_command(name, directory, pattern)
|
379 |
return self._untagged_response(typ, dat, name) |
380 |
|
381 |
|
382 |
def login(self, user, password): |
383 |
"""Identify client using plaintext password.
|
384 |
|
385 |
(typ, [data]) = <instance>.login(user, password)
|
386 |
|
387 |
NB: 'password' will be quoted.
|
388 |
"""
|
389 |
#if not 'AUTH=LOGIN' in self.capabilities:
|
390 |
# raise self.error("Server doesn't allow LOGIN authentication." % mech)
|
391 |
typ, dat = self._simple_command('LOGIN', user, self._quote(password)) |
392 |
if typ != 'OK': |
393 |
raise self.error(dat[-1]) |
394 |
self.state = 'AUTH' |
395 |
return typ, dat
|
396 |
|
397 |
|
398 |
def logout(self): |
399 |
"""Shutdown connection to server.
|
400 |
|
401 |
(typ, [data]) = <instance>.logout()
|
402 |
|
403 |
Returns server 'BYE' response.
|
404 |
"""
|
405 |
self.state = 'LOGOUT' |
406 |
try: typ, dat = self._simple_command('LOGOUT') |
407 |
except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] |
408 |
self.file.close()
|
409 |
self.sock.close()
|
410 |
if self.untagged_responses.has_key('BYE'): |
411 |
return 'BYE', self.untagged_responses['BYE'] |
412 |
return typ, dat
|
413 |
|
414 |
|
415 |
def lsub(self, directory='""', pattern='*'): |
416 |
"""List 'subscribed' mailbox names in directory matching pattern.
|
417 |
|
418 |
(typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
|
419 |
|
420 |
'data' are tuples of message part envelope and data.
|
421 |
"""
|
422 |
name = 'LSUB'
|
423 |
typ, dat = self._simple_command(name, directory, pattern)
|
424 |
return self._untagged_response(typ, dat, name) |
425 |
|
426 |
|
427 |
def noop(self): |
428 |
"""Send NOOP command.
|
429 |
|
430 |
(typ, data) = <instance>.noop()
|
431 |
"""
|
432 |
if __debug__:
|
433 |
if self.debug >= 3: |
434 |
_dump_ur(self.untagged_responses)
|
435 |
return self._simple_command('NOOP') |
436 |
|
437 |
|
438 |
def partial(self, message_num, message_part, start, length): |
439 |
"""Fetch truncated part of a message.
|
440 |
|
441 |
(typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
|
442 |
|
443 |
'data' is tuple of message part envelope and data.
|
444 |
"""
|
445 |
name = 'PARTIAL'
|
446 |
typ, dat = self._simple_command(name, message_num, message_part, start, length)
|
447 |
return self._untagged_response(typ, dat, 'FETCH') |
448 |
|
449 |
|
450 |
def rename(self, oldmailbox, newmailbox): |
451 |
"""Rename old mailbox name to new.
|
452 |
|
453 |
(typ, data) = <instance>.rename(oldmailbox, newmailbox)
|
454 |
"""
|
455 |
return self._simple_command('RENAME', oldmailbox, newmailbox) |
456 |
|
457 |
|
458 |
def search(self, charset, *criteria): |
459 |
"""Search mailbox for matching messages.
|
460 |
|
461 |
(typ, [data]) = <instance>.search(charset, criterium, ...)
|
462 |
|
463 |
'data' is space separated list of matching message numbers.
|
464 |
"""
|
465 |
name = 'SEARCH'
|
466 |
if charset:
|
467 |
charset = 'CHARSET ' + charset
|
468 |
typ, dat = apply(self._simple_command, (name, charset) + criteria) |
469 |
return self._untagged_response(typ, dat, name) |
470 |
|
471 |
|
472 |
def select(self, mailbox='INBOX', readonly=None): |
473 |
"""Select a mailbox.
|
474 |
|
475 |
Flush all untagged responses.
|
476 |
|
477 |
(typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
|
478 |
|
479 |
'data' is count of messages in mailbox ('EXISTS' response).
|
480 |
"""
|
481 |
# Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
|
482 |
self.untagged_responses = {} # Flush old responses. |
483 |
self.is_readonly = readonly
|
484 |
if readonly:
|
485 |
name = 'EXAMINE'
|
486 |
else:
|
487 |
name = 'SELECT'
|
488 |
typ, dat = self._simple_command(name, mailbox)
|
489 |
if typ != 'OK': |
490 |
self.state = 'AUTH' # Might have been 'SELECTED' |
491 |
return typ, dat
|
492 |
self.state = 'SELECTED' |
493 |
if self.untagged_responses.has_key('READ-ONLY') \ |
494 |
and not readonly: |
495 |
if __debug__:
|
496 |
if self.debug >= 1: |
497 |
_dump_ur(self.untagged_responses)
|
498 |
raise self.readonly('%s is not writable' % mailbox) |
499 |
return typ, self.untagged_responses.get('EXISTS', [None]) |
500 |
|
501 |
|
502 |
def status(self, mailbox, names): |
503 |
"""Request named status conditions for mailbox.
|
504 |
|
505 |
(typ, [data]) = <instance>.status(mailbox, names)
|
506 |
"""
|
507 |
name = 'STATUS'
|
508 |
if self.PROTOCOL_VERSION == 'IMAP4': |
509 |
raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name) |
510 |
typ, dat = self._simple_command(name, mailbox, names)
|
511 |
return self._untagged_response(typ, dat, name) |
512 |
|
513 |
|
514 |
def store(self, message_set, command, flags): |
515 |
"""Alters flag dispositions for messages in mailbox.
|
516 |
|
517 |
(typ, [data]) = <instance>.store(message_set, command, flags)
|
518 |
"""
|
519 |
if (flags[0],flags[-1]) != ('(',')'): |
520 |
flags = '(%s)' % flags # Avoid quoting the flags |
521 |
typ, dat = self._simple_command('STORE', message_set, command, flags) |
522 |
return self._untagged_response(typ, dat, 'FETCH') |
523 |
|
524 |
|
525 |
def subscribe(self, mailbox): |
526 |
"""Subscribe to new mailbox.
|
527 |
|
528 |
(typ, [data]) = <instance>.subscribe(mailbox)
|
529 |
"""
|
530 |
return self._simple_command('SUBSCRIBE', mailbox) |
531 |
|
532 |
|
533 |
def uid(self, command, *args): |
534 |
"""Execute "command arg ..." with messages identified by UID,
|
535 |
rather than message number.
|
536 |
|
537 |
(typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
|
538 |
|
539 |
Returns response appropriate to 'command'.
|
540 |
"""
|
541 |
command = command.upper() |
542 |
if not Commands.has_key(command): |
543 |
raise self.error("Unknown IMAP4 UID command: %s" % command) |
544 |
if self.state not in Commands[command]: |
545 |
raise self.error('command %s illegal in state %s' |
546 |
% (command, self.state))
|
547 |
name = 'UID'
|
548 |
typ, dat = apply(self._simple_command, (name, command) + args) |
549 |
if command == 'SEARCH': |
550 |
name = 'SEARCH'
|
551 |
else:
|
552 |
name = 'FETCH'
|
553 |
return self._untagged_response(typ, dat, name) |
554 |
|
555 |
|
556 |
def unsubscribe(self, mailbox): |
557 |
"""Unsubscribe from old mailbox.
|
558 |
|
559 |
(typ, [data]) = <instance>.unsubscribe(mailbox)
|
560 |
"""
|
561 |
return self._simple_command('UNSUBSCRIBE', mailbox) |
562 |
|
563 |
|
564 |
def xatom(self, name, *args): |
565 |
"""Allow simple extension commands
|
566 |
notified by server in CAPABILITY response.
|
567 |
|
568 |
(typ, [data]) = <instance>.xatom(name, arg, ...)
|
569 |
"""
|
570 |
if name[0] != 'X' or not name in self.capabilities: |
571 |
raise self.error('unknown extension command: %s' % name) |
572 |
return apply(self._simple_command, (name,) + args) |
573 |
|
574 |
|
575 |
|
576 |
# Private methods
|
577 |
|
578 |
|
579 |
def _append_untagged(self, typ, dat): |
580 |
|
581 |
if dat is None: dat = '' |
582 |
ur = self.untagged_responses
|
583 |
if __debug__:
|
584 |
if self.debug >= 5: |
585 |
_mesg('untagged_responses[%s] %s += ["%s"]' %
|
586 |
(typ, len(ur.get(typ,'')), dat)) |
587 |
if ur.has_key(typ):
|
588 |
ur[typ].append(dat) |
589 |
else:
|
590 |
ur[typ] = [dat] |
591 |
|
592 |
|
593 |
def _check_bye(self): |
594 |
bye = self.untagged_responses.get('BYE') |
595 |
if bye:
|
596 |
raise self.abort(bye[-1]) |
597 |
|
598 |
|
599 |
def _command(self, name, *args): |
600 |
|
601 |
if self.state not in Commands[name]: |
602 |
self.literal = None |
603 |
raise self.error( |
604 |
'command %s illegal in state %s' % (name, self.state)) |
605 |
|
606 |
for typ in ('OK', 'NO', 'BAD'): |
607 |
if self.untagged_responses.has_key(typ): |
608 |
del self.untagged_responses[typ] |
609 |
|
610 |
if self.untagged_responses.has_key('READ-ONLY') \ |
611 |
and not self.is_readonly: |
612 |
raise self.readonly('mailbox status changed to READ-ONLY') |
613 |
|
614 |
tag = self._new_tag()
|
615 |
data = '%s %s' % (tag, name)
|
616 |
for arg in args: |
617 |
if arg is None: continue |
618 |
data = '%s %s' % (data, self._checkquote(arg)) |
619 |
|
620 |
literal = self.literal
|
621 |
if literal is not None: |
622 |
self.literal = None |
623 |
if type(literal) is type(self._command): |
624 |
literator = literal |
625 |
else:
|
626 |
literator = None
|
627 |
data = '%s {%s}' % (data, len(literal)) |
628 |
|
629 |
if __debug__:
|
630 |
if self.debug >= 4: |
631 |
_mesg('> %s' % data)
|
632 |
else:
|
633 |
_log('> %s' % data)
|
634 |
|
635 |
try:
|
636 |
self.sock.send('%s%s' % (data, CRLF)) |
637 |
except socket.error, val:
|
638 |
raise self.abort('socket error: %s' % val) |
639 |
|
640 |
if literal is None: |
641 |
return tag
|
642 |
|
643 |
while 1: |
644 |
# Wait for continuation response
|
645 |
|
646 |
while self._get_response(): |
647 |
if self.tagged_commands[tag]: # BAD/NO? |
648 |
return tag
|
649 |
|
650 |
# Send literal
|
651 |
|
652 |
if literator:
|
653 |
literal = literator(self.continuation_response)
|
654 |
|
655 |
if __debug__:
|
656 |
if self.debug >= 4: |
657 |
_mesg('write literal size %s' % len(literal)) |
658 |
|
659 |
try:
|
660 |
self.sock.send(literal)
|
661 |
self.sock.send(CRLF)
|
662 |
except socket.error, val:
|
663 |
raise self.abort('socket error: %s' % val) |
664 |
|
665 |
if not literator: |
666 |
break
|
667 |
|
668 |
return tag
|
669 |
|
670 |
|
671 |
def _command_complete(self, name, tag): |
672 |
self._check_bye()
|
673 |
try:
|
674 |
typ, data = self._get_tagged_response(tag)
|
675 |
except self.abort, val: |
676 |
raise self.abort('command: %s => %s' % (name, val)) |
677 |
except self.error, val: |
678 |
raise self.error('command: %s => %s' % (name, val)) |
679 |
self._check_bye()
|
680 |
if typ == 'BAD': |
681 |
raise self.error('%s command error: %s %s' % (name, typ, data)) |
682 |
return typ, data
|
683 |
|
684 |
|
685 |
def _get_response(self): |
686 |
|
687 |
# Read response and store.
|
688 |
#
|
689 |
# Returns None for continuation responses,
|
690 |
# otherwise first response line received.
|
691 |
|
692 |
resp = self._get_line()
|
693 |
|
694 |
# Command completion response?
|
695 |
|
696 |
if self._match(self.tagre, resp): |
697 |
tag = self.mo.group('tag') |
698 |
if not self.tagged_commands.has_key(tag): |
699 |
raise self.abort('unexpected tagged response: %s' % resp) |
700 |
|
701 |
typ = self.mo.group('type') |
702 |
dat = self.mo.group('data') |
703 |
self.tagged_commands[tag] = (typ, [dat])
|
704 |
else:
|
705 |
dat2 = None
|
706 |
|
707 |
# '*' (untagged) responses?
|
708 |
|
709 |
if not self._match(Untagged_response, resp): |
710 |
if self._match(Untagged_status, resp): |
711 |
dat2 = self.mo.group('data2') |
712 |
|
713 |
if self.mo is None: |
714 |
# Only other possibility is '+' (continuation) response...
|
715 |
|
716 |
if self._match(Continuation, resp): |
717 |
self.continuation_response = self.mo.group('data') |
718 |
return None # NB: indicates continuation |
719 |
|
720 |
raise self.abort("unexpected response: '%s'" % resp) |
721 |
|
722 |
typ = self.mo.group('type') |
723 |
dat = self.mo.group('data') |
724 |
if dat is None: dat = '' # Null untagged response |
725 |
if dat2: dat = dat + ' ' + dat2 |
726 |
|
727 |
# Is there a literal to come?
|
728 |
|
729 |
while self._match(Literal, dat): |
730 |
|
731 |
# Read literal direct from connection.
|
732 |
|
733 |
size = int(self.mo.group('size')) |
734 |
if __debug__:
|
735 |
if self.debug >= 4: |
736 |
_mesg('read literal size %s' % size)
|
737 |
data = self.file.read(size)
|
738 |
|
739 |
# Store response with literal as tuple
|
740 |
|
741 |
self._append_untagged(typ, (dat, data))
|
742 |
|
743 |
# Read trailer - possibly containing another literal
|
744 |
|
745 |
dat = self._get_line()
|
746 |
|
747 |
self._append_untagged(typ, dat)
|
748 |
|
749 |
# Bracketed response information?
|
750 |
|
751 |
if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat): |
752 |
self._append_untagged(self.mo.group('type'), self.mo.group('data')) |
753 |
|
754 |
if __debug__:
|
755 |
if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'): |
756 |
_mesg('%s response: %s' % (typ, dat))
|
757 |
|
758 |
return resp
|
759 |
|
760 |
|
761 |
def _get_tagged_response(self, tag): |
762 |
|
763 |
while 1: |
764 |
result = self.tagged_commands[tag]
|
765 |
if result is not None: |
766 |
del self.tagged_commands[tag] |
767 |
return result
|
768 |
|
769 |
# Some have reported "unexpected response" exceptions.
|
770 |
# Note that ignoring them here causes loops.
|
771 |
# Instead, send me details of the unexpected response and
|
772 |
# I'll update the code in `_get_response()'.
|
773 |
|
774 |
try:
|
775 |
self._get_response()
|
776 |
except self.abort, val: |
777 |
if __debug__:
|
778 |
if self.debug >= 1: |
779 |
print_log() |
780 |
raise
|
781 |
|
782 |
|
783 |
def _get_line(self): |
784 |
|
785 |
line = self.file.readline()
|
786 |
if not line: |
787 |
raise self.abort('socket error: EOF') |
788 |
|
789 |
# Protocol mandates all lines terminated by CRLF
|
790 |
|
791 |
line = line[:-2]
|
792 |
if __debug__:
|
793 |
if self.debug >= 4: |
794 |
_mesg('< %s' % line)
|
795 |
else:
|
796 |
_log('< %s' % line)
|
797 |
return line
|
798 |
|
799 |
|
800 |
def _match(self, cre, s): |
801 |
|
802 |
# Run compiled regular expression match method on 's'.
|
803 |
# Save result, return success.
|
804 |
|
805 |
self.mo = cre.match(s)
|
806 |
if __debug__:
|
807 |
if self.mo is not None and self.debug >= 5: |
808 |
_mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)) |
809 |
return self.mo is not None |
810 |
|
811 |
|
812 |
def _new_tag(self): |
813 |
|
814 |
tag = '%s%s' % (self.tagpre, self.tagnum) |
815 |
self.tagnum = self.tagnum + 1 |
816 |
self.tagged_commands[tag] = None |
817 |
return tag
|
818 |
|
819 |
|
820 |
def _checkquote(self, arg): |
821 |
|
822 |
# Must quote command args if non-alphanumeric chars present,
|
823 |
# and not already quoted.
|
824 |
|
825 |
if type(arg) is not type(''): |
826 |
return arg
|
827 |
if (arg[0],arg[-1]) in (('(',')'),('"','"')): |
828 |
return arg
|
829 |
if self.mustquote.search(arg) is None: |
830 |
return arg
|
831 |
return self._quote(arg) |
832 |
|
833 |
|
834 |
def _quote(self, arg): |
835 |
|
836 |
arg = arg.replace('\\', '\\\\') |
837 |
arg = arg.replace('"', '\\"') |
838 |
|
839 |
return '"%s"' % arg |
840 |
|
841 |
|
842 |
def _simple_command(self, name, *args): |
843 |
|
844 |
return self._command_complete(name, apply(self._command, (name,) + args)) |
845 |
|
846 |
|
847 |
def _untagged_response(self, typ, dat, name): |
848 |
|
849 |
if typ == 'NO': |
850 |
return typ, dat
|
851 |
if not self.untagged_responses.has_key(name): |
852 |
return typ, [None] |
853 |
data = self.untagged_responses[name]
|
854 |
if __debug__:
|
855 |
if self.debug >= 5: |
856 |
_mesg('untagged_responses[%s] => %s' % (name, data))
|
857 |
del self.untagged_responses[name] |
858 |
return typ, data
|
859 |
|
860 |
|
861 |
|
862 |
class _Authenticator: |
863 |
|
864 |
"""Private class to provide en/decoding
|
865 |
for base64-based authentication conversation.
|
866 |
"""
|
867 |
|
868 |
def __init__(self, mechinst): |
869 |
self.mech = mechinst # Callable object to provide/process data |
870 |
|
871 |
def process(self, data): |
872 |
ret = self.mech(self.decode(data)) |
873 |
if ret is None: |
874 |
return '*' # Abort conversation |
875 |
return self.encode(ret) |
876 |
|
877 |
def encode(self, inp): |
878 |
#
|
879 |
# Invoke binascii.b2a_base64 iteratively with
|
880 |
# short even length buffers, strip the trailing
|
881 |
# line feed from the result and append. "Even"
|
882 |
# means a number that factors to both 6 and 8,
|
883 |
# so when it gets to the end of the 8-bit input
|
884 |
# there's no partial 6-bit output.
|
885 |
#
|
886 |
oup = ''
|
887 |
while inp:
|
888 |
if len(inp) > 48: |
889 |
t = inp[:48]
|
890 |
inp = inp[48:]
|
891 |
else:
|
892 |
t = inp |
893 |
inp = ''
|
894 |
e = binascii.b2a_base64(t) |
895 |
if e:
|
896 |
oup = oup + e[:-1]
|
897 |
return oup
|
898 |
|
899 |
def decode(self, inp): |
900 |
if not inp: |
901 |
return '' |
902 |
return binascii.a2b_base64(inp)
|
903 |
|
904 |
|
905 |
|
906 |
Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, |
907 |
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} |
908 |
|
909 |
def Internaldate2tuple(resp): |
910 |
"""Convert IMAP4 INTERNALDATE to UT.
|
911 |
|
912 |
Returns Python time module tuple.
|
913 |
"""
|
914 |
|
915 |
mo = InternalDate.match(resp) |
916 |
if not mo: |
917 |
return None |
918 |
|
919 |
mon = Mon2num[mo.group('mon')]
|
920 |
zonen = mo.group('zonen')
|
921 |
|
922 |
day = int(mo.group('day')) |
923 |
year = int(mo.group('year')) |
924 |
hour = int(mo.group('hour')) |
925 |
min = int(mo.group('min')) |
926 |
sec = int(mo.group('sec')) |
927 |
zoneh = int(mo.group('zoneh')) |
928 |
zonem = int(mo.group('zonem')) |
929 |
|
930 |
# INTERNALDATE timezone must be subtracted to get UT
|
931 |
|
932 |
zone = (zoneh*60 + zonem)*60 |
933 |
if zonen == '-': |
934 |
zone = -zone |
935 |
|
936 |
tt = (year, mon, day, hour, min, sec, -1, -1, -1) |
937 |
|
938 |
utc = time.mktime(tt) |
939 |
|
940 |
# Following is necessary because the time module has no 'mkgmtime'.
|
941 |
# 'mktime' assumes arg in local timezone, so adds timezone/altzone.
|
942 |
|
943 |
lt = time.localtime(utc) |
944 |
if time.daylight and lt[-1]: |
945 |
zone = zone + time.altzone |
946 |
else:
|
947 |
zone = zone + time.timezone |
948 |
|
949 |
return time.localtime(utc - zone)
|
950 |
|
951 |
|
952 |
|
953 |
def Int2AP(num): |
954 |
|
955 |
"""Convert integer to A-P string representation."""
|
956 |
|
957 |
val = ''; AP = 'ABCDEFGHIJKLMNOP' |
958 |
num = int(abs(num)) |
959 |
while num:
|
960 |
num, mod = divmod(num, 16) |
961 |
val = AP[mod] + val |
962 |
return val
|
963 |
|
964 |
|
965 |
|
966 |
def ParseFlags(resp): |
967 |
|
968 |
"""Convert IMAP4 flags response to python tuple."""
|
969 |
|
970 |
mo = Flags.match(resp) |
971 |
if not mo: |
972 |
return ()
|
973 |
|
974 |
return tuple(mo.group('flags').split()) |
975 |
|
976 |
|
977 |
def Time2Internaldate(date_time): |
978 |
|
979 |
"""Convert 'date_time' to IMAP4 INTERNALDATE representation.
|
980 |
|
981 |
Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
|
982 |
"""
|
983 |
|
984 |
dttype = type(date_time)
|
985 |
if dttype is type(1) or dttype is type(1.1): |
986 |
tt = time.localtime(date_time) |
987 |
elif dttype is type(()): |
988 |
tt = date_time |
989 |
elif dttype is type(""): |
990 |
return date_time # Assume in correct format |
991 |
else: raise ValueError |
992 |
|
993 |
dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
|
994 |
if dt[0] == '0': |
995 |
dt = ' ' + dt[1:] |
996 |
if time.daylight and tt[-1]: |
997 |
zone = -time.altzone |
998 |
else:
|
999 |
zone = -time.timezone |
1000 |
return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"' |
1001 |
|
1002 |
|
1003 |
|
1004 |
if __debug__:
|
1005 |
|
1006 |
def _mesg(s, secs=None): |
1007 |
if secs is None: |
1008 |
secs = time.time() |
1009 |
tm = time.strftime('%M:%S', time.localtime(secs))
|
1010 |
sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s)) |
1011 |
sys.stderr.flush() |
1012 |
|
1013 |
def _dump_ur(dict): |
1014 |
# Dump untagged responses (in `dict').
|
1015 |
l = dict.items()
|
1016 |
if not l: return |
1017 |
t = '\n\t\t'
|
1018 |
l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l) |
1019 |
_mesg('untagged responses dump:%s%s' % (t, t.join(l)))
|
1020 |
|
1021 |
_cmd_log = [] # Last `_cmd_log_len' interactions
|
1022 |
_cmd_log_len = 10
|
1023 |
|
1024 |
def _log(line): |
1025 |
# Keep log of last `_cmd_log_len' interactions for debugging.
|
1026 |
if len(_cmd_log) == _cmd_log_len: |
1027 |
del _cmd_log[0] |
1028 |
_cmd_log.append((time.time(), line)) |
1029 |
|
1030 |
def print_log(): |
1031 |
_mesg('last %d IMAP4 interactions:' % len(_cmd_log)) |
1032 |
for secs,line in _cmd_log: |
1033 |
_mesg(line, secs) |
1034 |
|
1035 |
|
1036 |
|
1037 |
if __name__ == '__main__': |
1038 |
|
1039 |
import getopt, getpass, sys |
1040 |
|
1041 |
try:
|
1042 |
optlist, args = getopt.getopt(sys.argv[1:], 'd:') |
1043 |
except getopt.error, val:
|
1044 |
pass
|
1045 |
|
1046 |
for opt,val in optlist: |
1047 |
if opt == '-d': |
1048 |
Debug = int(val)
|
1049 |
|
1050 |
if not args: args = ('',) |
1051 |
|
1052 |
host = args[0]
|
1053 |
|
1054 |
USER = getpass.getuser() |
1055 |
PASSWD = getpass.getpass("IMAP password for %s on %s:" % (USER, host or "localhost")) |
1056 |
|
1057 |
test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
|
1058 |
test_seq1 = ( |
1059 |
('login', (USER, PASSWD)),
|
1060 |
('create', ('/tmp/xxx 1',)), |
1061 |
('rename', ('/tmp/xxx 1', '/tmp/yyy')), |
1062 |
('CREATE', ('/tmp/yyz 2',)), |
1063 |
('append', ('/tmp/yyz 2', None, None, test_mesg)), |
1064 |
('list', ('/tmp', 'yy*')), |
1065 |
('select', ('/tmp/yyz 2',)), |
1066 |
('search', (None, 'SUBJECT', 'test')), |
1067 |
('partial', ('1', 'RFC822', 1, 1024)), |
1068 |
('store', ('1', 'FLAGS', '(\Deleted)')), |
1069 |
('expunge', ()),
|
1070 |
('recent', ()),
|
1071 |
('close', ()),
|
1072 |
) |
1073 |
|
1074 |
test_seq2 = ( |
1075 |
('select', ()),
|
1076 |
('response',('UIDVALIDITY',)), |
1077 |
('uid', ('SEARCH', 'ALL')), |
1078 |
('response', ('EXISTS',)), |
1079 |
('append', (None, None, None, test_mesg)), |
1080 |
('recent', ()),
|
1081 |
('logout', ()),
|
1082 |
) |
1083 |
|
1084 |
def run(cmd, args): |
1085 |
_mesg('%s %s' % (cmd, args))
|
1086 |
typ, dat = apply(eval('M.%s' % cmd), args) |
1087 |
_mesg('%s => %s %s' % (cmd, typ, dat))
|
1088 |
return dat
|
1089 |
|
1090 |
try:
|
1091 |
M = IMAP4(host) |
1092 |
_mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
|
1093 |
|
1094 |
for cmd,args in test_seq1: |
1095 |
run(cmd, args) |
1096 |
|
1097 |
for ml in run('list', ('/tmp/', 'yy%')): |
1098 |
mo = re.match(r'.*"([^"]+)"$', ml)
|
1099 |
if mo: path = mo.group(1) |
1100 |
else: path = ml.split()[-1] |
1101 |
run('delete', (path,))
|
1102 |
|
1103 |
for cmd,args in test_seq2: |
1104 |
dat = run(cmd, args) |
1105 |
|
1106 |
if (cmd,args) != ('uid', ('SEARCH', 'ALL')): |
1107 |
continue
|
1108 |
|
1109 |
uid = dat[-1].split()
|
1110 |
if not uid: continue |
1111 |
run('uid', ('FETCH', '%s' % uid[-1], |
1112 |
'(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
|
1113 |
|
1114 |
print '\nAll tests OK.' |
1115 |
|
1116 |
except:
|
1117 |
print '\nTests failed.' |
1118 |
|
1119 |
if not Debug: |
1120 |
print ''' |
1121 |
If you would like to see debugging output,
|
1122 |
try: %s -d5
|
1123 |
''' % sys.argv[0] |
1124 |
|
1125 |
raise
|