root / tags / v1_0_2_Build_904 / extensions / extScripting / scripts / jython / Lib / imaplib.py @ 10724
History | View | Annotate | Download (33.2 KB)
1 | 5782 | jmvivo | """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 |