blob: c6e2addf561f2a44bf8f175b6b62deb826ad0510 [file] [log] [blame]
Guido van Rossumb1f08121998-06-25 02:22:16 +00001
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00002"""IMAP4 client.
3
4Based on RFC 2060.
5
6Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
7
Guido van Rossumeda960a1998-06-18 14:24:28 +00008Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
9
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000010Public class: IMAP4
11Public variable: Debug
12Public functions: Internaldate2tuple
13 Int2AP
14 ParseFlags
15 Time2Internaldate
16"""
Guido van Rossumb1f08121998-06-25 02:22:16 +000017
18__version__ = "2.11"
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000019
Guido van Rossumeda960a1998-06-18 14:24:28 +000020import binascii, re, socket, string, time, random
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000021
22# Globals
23
24CRLF = '\r\n'
25Debug = 0
26IMAP4_PORT = 143
Guido van Rossum38d8f4e1998-04-11 01:22:34 +000027AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000028
29# Commands
30
31Commands = {
32 # name valid states
33 'APPEND': ('AUTH', 'SELECTED'),
34 'AUTHENTICATE': ('NONAUTH',),
35 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
36 'CHECK': ('SELECTED',),
37 'CLOSE': ('SELECTED',),
38 'COPY': ('SELECTED',),
39 'CREATE': ('AUTH', 'SELECTED'),
40 'DELETE': ('AUTH', 'SELECTED'),
41 'EXAMINE': ('AUTH', 'SELECTED'),
42 'EXPUNGE': ('SELECTED',),
43 'FETCH': ('SELECTED',),
44 'LIST': ('AUTH', 'SELECTED'),
45 'LOGIN': ('NONAUTH',),
46 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
47 'LSUB': ('AUTH', 'SELECTED'),
48 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
Guido van Rossumeda960a1998-06-18 14:24:28 +000049 'PARTIAL': ('SELECTED',),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000050 'RENAME': ('AUTH', 'SELECTED'),
51 'SEARCH': ('SELECTED',),
52 'SELECT': ('AUTH', 'SELECTED'),
53 'STATUS': ('AUTH', 'SELECTED'),
54 'STORE': ('SELECTED',),
55 'SUBSCRIBE': ('AUTH', 'SELECTED'),
56 'UID': ('SELECTED',),
57 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
58 }
59
60# Patterns to match server responses
61
Guido van Rossumeda960a1998-06-18 14:24:28 +000062Continuation = re.compile(r'\+( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000063Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
64InternalDate = re.compile(r'.*INTERNALDATE "'
65 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
66 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
67 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
68 r'"')
69Literal = re.compile(r'(?P<data>.*) {(?P<size>\d+)}$')
70Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
Guido van Rossumeda960a1998-06-18 14:24:28 +000071Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000072Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
73
74
75
76class IMAP4:
77
78 """IMAP4 client class.
79
80 Instantiate with: IMAP4([host[, port]])
81
82 host - host's name (default: localhost);
83 port - port number (default: standard IMAP4 port).
84
85 All IMAP4rev1 commands are supported by methods of the same
Guido van Rossum6884af71998-05-29 13:34:03 +000086 name (in lower-case).
87
88 All arguments to commands are converted to strings, except for
Guido van Rossumb1f08121998-06-25 02:22:16 +000089 AUTHENTICATE, and the last argument to APPEND which is passed as
90 an IMAP4 literal. If necessary (the string contains
91 white-space and isn't enclosed with either parentheses or
92 double quotes) each string is quoted.
Guido van Rossum6884af71998-05-29 13:34:03 +000093
94 Each command returns a tuple: (type, [data, ...]) where 'type'
95 is usually 'OK' or 'NO', and 'data' is either the text from the
96 tagged response, or untagged results from command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000097
98 Errors raise the exception class <instance>.error("<reason>").
99 IMAP4 server errors raise <instance>.abort("<reason>"),
100 which is a sub-class of 'error'.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000101
102 Note: to use this module, you must read the RFCs pertaining
103 to the IMAP4 protocol, as the semantics of the arguments to
104 each IMAP4 command are left to the invoker, not to mention
105 the results.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000106 """
107
108 class error(Exception): pass # Logical errors - debug required
109 class abort(error): pass # Service errors - close and retry
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000110
111
112 def __init__(self, host = '', port = IMAP4_PORT):
113 self.host = host
114 self.port = port
115 self.debug = Debug
116 self.state = 'LOGOUT'
Guido van Rossum6884af71998-05-29 13:34:03 +0000117 self.literal = None # A literal argument to a command
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000118 self.tagged_commands = {} # Tagged commands awaiting response
119 self.untagged_responses = {} # {typ: [data, ...], ...}
120 self.continuation_response = '' # Last continuation response
121 self.tagnum = 0
122
123 # Open socket to server.
124
Guido van Rossumeda960a1998-06-18 14:24:28 +0000125 self.open(host, port)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000126
127 # Create unique tag for this session,
128 # and compile tagged response matcher.
129
Guido van Rossum6884af71998-05-29 13:34:03 +0000130 self.tagpre = Int2AP(random.randint(0, 31999))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000131 self.tagre = re.compile(r'(?P<tag>'
132 + self.tagpre
133 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
134
135 # Get server welcome message,
136 # request and store CAPABILITY response.
137
138 if __debug__ and self.debug >= 1:
139 print '\tnew IMAP4 connection, tag=%s' % self.tagpre
140
141 self.welcome = self._get_response()
142 if self.untagged_responses.has_key('PREAUTH'):
143 self.state = 'AUTH'
144 elif self.untagged_responses.has_key('OK'):
145 self.state = 'NONAUTH'
146# elif self.untagged_responses.has_key('BYE'):
147 else:
148 raise self.error(self.welcome)
149
150 cap = 'CAPABILITY'
151 self._simple_command(cap)
152 if not self.untagged_responses.has_key(cap):
153 raise self.error('no CAPABILITY response from server')
154 self.capabilities = tuple(string.split(self.untagged_responses[cap][-1]))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000155
156 if __debug__ and self.debug >= 3:
157 print '\tCAPABILITIES: %s' % `self.capabilities`
158
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000159 for version in AllowedVersions:
160 if not version in self.capabilities:
161 continue
162 self.PROTOCOL_VERSION = version
Guido van Rossumb1f08121998-06-25 02:22:16 +0000163 return
164
165 raise self.error('server not IMAP4 compliant')
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000166
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000167
Guido van Rossumeda960a1998-06-18 14:24:28 +0000168 def open(self, host, port):
169 """Setup 'self.sock' and 'self.file'."""
170 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
171 self.sock.connect(self.host, self.port)
172 self.file = self.sock.makefile('r')
173
174
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000175 def __getattr__(self, attr):
176 """Allow UPPERCASE variants of all following IMAP4 commands."""
177 if Commands.has_key(attr):
178 return eval("self.%s" % string.lower(attr))
179 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
180
181
182 # Public methods
183
184
185 def append(self, mailbox, flags, date_time, message):
186 """Append message to named mailbox.
187
188 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
189 """
190 name = 'APPEND'
191 if flags:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000192 if (flags[0],flags[-1]) != ('(',')'):
193 flags = '(%s)' % flags
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000194 else:
195 flags = None
196 if date_time:
197 date_time = Time2Internaldate(date_time)
198 else:
199 date_time = None
Guido van Rossum6884af71998-05-29 13:34:03 +0000200 self.literal = message
201 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000202
203
Guido van Rossumeda960a1998-06-18 14:24:28 +0000204 def authenticate(self, mechanism, authobject):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000205 """Authenticate command - requires response processing.
206
Guido van Rossumeda960a1998-06-18 14:24:28 +0000207 'mechanism' specifies which authentication mechanism is to
208 be used - it must appear in <instance>.capabilities in the
209 form AUTH=<mechanism>.
210
211 'authobject' must be a callable object:
212
213 data = authobject(response)
214
215 It will be called to process server continuation responses.
216 It should return data that will be encoded and sent to server.
217 It should return None if the client abort response '*' should
218 be sent instead.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000219 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000220 mech = string.upper(mechanism)
221 cap = 'AUTH=%s' % mech
222 if not cap in self.capabilities:
223 raise self.error("Server doesn't allow %s authentication." % mech)
224 self.literal = _Authenticator(authobject).process
225 typ, dat = self._simple_command('AUTHENTICATE', mech)
226 if typ != 'OK':
227 raise self.error(dat)
228 self.state = 'AUTH'
229 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000230
231
232 def check(self):
233 """Checkpoint mailbox on server.
234
235 (typ, [data]) = <instance>.check()
236 """
237 return self._simple_command('CHECK')
238
239
240 def close(self):
241 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000242
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000243 Deleted messages are removed from writable mailbox.
244 This is the recommended command before 'LOGOUT'.
245
246 (typ, [data]) = <instance>.close()
247 """
248 try:
249 try: typ, dat = self._simple_command('CLOSE')
250 except EOFError: typ, dat = None, [None]
251 finally:
252 self.state = 'AUTH'
253 return typ, dat
254
255
256 def copy(self, message_set, new_mailbox):
257 """Copy 'message_set' messages onto end of 'new_mailbox'.
258
259 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
260 """
261 return self._simple_command('COPY', message_set, new_mailbox)
262
263
264 def create(self, mailbox):
265 """Create new mailbox.
266
267 (typ, [data]) = <instance>.create(mailbox)
268 """
269 return self._simple_command('CREATE', mailbox)
270
271
272 def delete(self, mailbox):
273 """Delete old mailbox.
274
275 (typ, [data]) = <instance>.delete(mailbox)
276 """
277 return self._simple_command('DELETE', mailbox)
278
279
280 def expunge(self):
281 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000282
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000283 Generates 'EXPUNGE' response for each deleted message.
284
285 (typ, [data]) = <instance>.expunge()
286
287 'data' is list of 'EXPUNGE'd message numbers in order received.
288 """
289 name = 'EXPUNGE'
290 typ, dat = self._simple_command(name)
291 return self._untagged_response(typ, name)
292
293
294 def fetch(self, message_set, message_parts):
295 """Fetch (parts of) messages.
296
297 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
298
299 'data' are tuples of message part envelope and data.
300 """
301 name = 'FETCH'
302 typ, dat = self._simple_command(name, message_set, message_parts)
303 return self._untagged_response(typ, name)
304
305
306 def list(self, directory='""', pattern='*'):
307 """List mailbox names in directory matching pattern.
308
309 (typ, [data]) = <instance>.list(directory='""', pattern='*')
310
311 'data' is list of LIST responses.
312 """
313 name = 'LIST'
314 typ, dat = self._simple_command(name, directory, pattern)
315 return self._untagged_response(typ, name)
316
317
318 def login(self, user, password):
319 """Identify client using plaintext password.
320
321 (typ, [data]) = <instance>.list(user, password)
322 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000323 typ, dat = self._simple_command('LOGIN', user, password)
324 if typ != 'OK':
325 raise self.error(dat)
326 self.state = 'AUTH'
327 return typ, dat
328
329
330 def logout(self):
331 """Shutdown connection to server.
332
333 (typ, [data]) = <instance>.logout()
334
335 Returns server 'BYE' response.
336 """
337 self.state = 'LOGOUT'
338 try: typ, dat = self._simple_command('LOGOUT')
339 except EOFError: typ, dat = None, [None]
340 self.file.close()
341 self.sock.close()
342 if self.untagged_responses.has_key('BYE'):
343 return 'BYE', self.untagged_responses['BYE']
344 return typ, dat
345
346
347 def lsub(self, directory='""', pattern='*'):
348 """List 'subscribed' mailbox names in directory matching pattern.
349
350 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
351
352 'data' are tuples of message part envelope and data.
353 """
354 name = 'LSUB'
355 typ, dat = self._simple_command(name, directory, pattern)
356 return self._untagged_response(typ, name)
357
358
Guido van Rossum6884af71998-05-29 13:34:03 +0000359 def noop(self):
360 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000361
Guido van Rossum6884af71998-05-29 13:34:03 +0000362 (typ, data) = <instance>.noop()
363 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000364 if __debug__ and self.debug >= 3:
365 print '\tuntagged responses: %s' % `self.untagged_responses`
Guido van Rossum6884af71998-05-29 13:34:03 +0000366 return self._simple_command('NOOP')
367
368
Guido van Rossumeda960a1998-06-18 14:24:28 +0000369 def partial(self, message_num, message_part, start, length):
370 """Fetch truncated part of a message.
371
372 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
373
374 'data' is tuple of message part envelope and data.
375 """
376 name = 'PARTIAL'
377 typ, dat = self._simple_command(name, message_num, message_part, start, length)
378 return self._untagged_response(typ, 'FETCH')
379
380
Guido van Rossum6884af71998-05-29 13:34:03 +0000381 def recent(self):
Guido van Rossumeda960a1998-06-18 14:24:28 +0000382 """Return most recent 'RECENT' responses if any exist,
Guido van Rossum6884af71998-05-29 13:34:03 +0000383 else prompt server for an update using the 'NOOP' command,
384 and flush all untagged responses.
Guido van Rossum46586821998-05-18 14:39:42 +0000385
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000386 (typ, [data]) = <instance>.recent()
387
388 'data' is None if no new messages,
Guido van Rossumeda960a1998-06-18 14:24:28 +0000389 else list of RECENT responses, most recent last.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000390 """
391 name = 'RECENT'
392 typ, dat = self._untagged_response('OK', name)
393 if dat[-1]:
394 return typ, dat
Guido van Rossum46586821998-05-18 14:39:42 +0000395 self.untagged_responses = {}
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000396 typ, dat = self._simple_command('NOOP')
397 return self._untagged_response(typ, name)
398
399
400 def rename(self, oldmailbox, newmailbox):
401 """Rename old mailbox name to new.
402
403 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
404 """
405 return self._simple_command('RENAME', oldmailbox, newmailbox)
406
407
408 def response(self, code):
409 """Return data for response 'code' if received, or None.
410
Guido van Rossum46586821998-05-18 14:39:42 +0000411 Old value for response 'code' is cleared.
412
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000413 (code, [data]) = <instance>.response(code)
414 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000415 return self._untagged_response(code, string.upper(code))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000416
417
418 def search(self, charset, criteria):
419 """Search mailbox for matching messages.
420
421 (typ, [data]) = <instance>.search(charset, criteria)
422
423 'data' is space separated list of matching message numbers.
424 """
425 name = 'SEARCH'
426 if charset:
427 charset = 'CHARSET ' + charset
428 typ, dat = self._simple_command(name, charset, criteria)
429 return self._untagged_response(typ, name)
430
431
432 def select(self, mailbox='INBOX', readonly=None):
433 """Select a mailbox.
434
Guido van Rossum46586821998-05-18 14:39:42 +0000435 Flush all untagged responses.
436
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000437 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
438
439 'data' is count of messages in mailbox ('EXISTS' response).
440 """
441 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
Guido van Rossum46586821998-05-18 14:39:42 +0000442 self.untagged_responses = {} # Flush old responses.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000443 if readonly:
444 name = 'EXAMINE'
445 else:
446 name = 'SELECT'
447 typ, dat = self._simple_command(name, mailbox)
448 if typ == 'OK':
449 self.state = 'SELECTED'
450 elif typ == 'NO':
451 self.state = 'AUTH'
452 if not readonly and not self.untagged_responses.has_key('READ-WRITE'):
453 raise self.error('%s is not writable' % mailbox)
454 return typ, self.untagged_responses.get('EXISTS', [None])
455
456
Guido van Rossumeda960a1998-06-18 14:24:28 +0000457 def socket(self):
458 """Return socket instance used to connect to IMAP4 server.
459
460 socket = <instance>.socket()
461 """
462 return self.sock
463
464
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000465 def status(self, mailbox, names):
466 """Request named status conditions for mailbox.
467
468 (typ, [data]) = <instance>.status(mailbox, names)
469 """
470 name = 'STATUS'
Guido van Rossumbe14e691998-04-11 03:11:51 +0000471 if self.PROTOCOL_VERSION == 'IMAP4':
472 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000473 typ, dat = self._simple_command(name, mailbox, names)
474 return self._untagged_response(typ, name)
475
476
477 def store(self, message_set, command, flag_list):
478 """Alters flag dispositions for messages in mailbox.
479
480 (typ, [data]) = <instance>.store(message_set, command, flag_list)
481 """
Guido van Rossum46586821998-05-18 14:39:42 +0000482 typ, dat = self._simple_command('STORE', message_set, command, flag_list)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000483 return self._untagged_response(typ, 'FETCH')
484
485
486 def subscribe(self, mailbox):
487 """Subscribe to new mailbox.
488
489 (typ, [data]) = <instance>.subscribe(mailbox)
490 """
491 return self._simple_command('SUBSCRIBE', mailbox)
492
493
Guido van Rossum46586821998-05-18 14:39:42 +0000494 def uid(self, command, *args):
495 """Execute "command arg ..." with messages identified by UID,
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000496 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000497
Guido van Rossum46586821998-05-18 14:39:42 +0000498 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000499
500 Returns response appropriate to 'command'.
501 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000502 command = string.upper(command)
503 if not Commands.has_key(command):
504 raise self.error("Unknown IMAP4 UID command: %s" % command)
505 if self.state not in Commands[command]:
506 raise self.error('command %s illegal in state %s'
507 % (command, self.state))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000508 name = 'UID'
Guido van Rossumeda960a1998-06-18 14:24:28 +0000509 typ, dat = apply(self._simple_command, (name, command) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000510 if command == 'SEARCH':
511 name = 'SEARCH'
512 else:
513 name = 'FETCH'
514 typ, dat2 = self._untagged_response(typ, name)
515 if dat2[-1]: dat = dat2
516 return typ, dat
517
518
519 def unsubscribe(self, mailbox):
520 """Unsubscribe from old mailbox.
521
522 (typ, [data]) = <instance>.unsubscribe(mailbox)
523 """
524 return self._simple_command('UNSUBSCRIBE', mailbox)
525
526
Guido van Rossum46586821998-05-18 14:39:42 +0000527 def xatom(self, name, *args):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000528 """Allow simple extension commands
529 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000530
Guido van Rossum46586821998-05-18 14:39:42 +0000531 (typ, [data]) = <instance>.xatom(name, arg, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000532 """
533 if name[0] != 'X' or not name in self.capabilities:
534 raise self.error('unknown extension command: %s' % name)
Guido van Rossum46586821998-05-18 14:39:42 +0000535 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000536
537
538
539 # Private methods
540
541
542 def _append_untagged(self, typ, dat):
543
Guido van Rossumeda960a1998-06-18 14:24:28 +0000544 ur = self.untagged_responses
545 if ur.has_key(typ):
546 ur[typ].append(dat)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000547 else:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000548 ur[typ] = [dat]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000549 if __debug__ and self.debug >= 5:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000550 print '\tuntagged_responses[%s] %s += %s' % (typ, len(`ur[typ]`), _trunc(20, `dat`))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000551
552
Guido van Rossum6884af71998-05-29 13:34:03 +0000553 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000554
555 if self.state not in Commands[name]:
Guido van Rossum6884af71998-05-29 13:34:03 +0000556 self.literal = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000557 raise self.error(
558 'command %s illegal in state %s' % (name, self.state))
559
Guido van Rossumeda960a1998-06-18 14:24:28 +0000560 if self.untagged_responses.has_key('OK'):
561 del self.untagged_responses['OK']
562
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000563 tag = self._new_tag()
564 data = '%s %s' % (tag, name)
Guido van Rossum6884af71998-05-29 13:34:03 +0000565 for d in args:
Guido van Rossum46586821998-05-18 14:39:42 +0000566 if d is None: continue
567 if type(d) is type(''):
568 l = len(string.split(d))
569 else:
570 l = 1
571 if l == 0 or l > 1 and (d[0],d[-1]) not in (('(',')'),('"','"')):
572 data = '%s "%s"' % (data, d)
573 else:
574 data = '%s %s' % (data, d)
Guido van Rossum6884af71998-05-29 13:34:03 +0000575
576 literal = self.literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000577 if literal is not None:
Guido van Rossum6884af71998-05-29 13:34:03 +0000578 self.literal = None
Guido van Rossumeda960a1998-06-18 14:24:28 +0000579 if type(literal) is type(self._command):
580 literator = literal
581 else:
582 literator = None
583 data = '%s {%s}' % (data, len(literal))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000584
585 try:
586 self.sock.send('%s%s' % (data, CRLF))
587 except socket.error, val:
588 raise self.abort('socket error: %s' % val)
589
590 if __debug__ and self.debug >= 4:
591 print '\t> %s' % data
592
593 if literal is None:
594 return tag
595
Guido van Rossumeda960a1998-06-18 14:24:28 +0000596 while 1:
597 # Wait for continuation response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000598
Guido van Rossumeda960a1998-06-18 14:24:28 +0000599 while self._get_response():
600 if self.tagged_commands[tag]: # BAD/NO?
601 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000602
Guido van Rossumeda960a1998-06-18 14:24:28 +0000603 # Send literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000604
Guido van Rossumeda960a1998-06-18 14:24:28 +0000605 if literator:
606 literal = literator(self.continuation_response)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000607
Guido van Rossumeda960a1998-06-18 14:24:28 +0000608 if __debug__ and self.debug >= 4:
609 print '\twrite literal size %s' % len(literal)
610
611 try:
612 self.sock.send(literal)
613 self.sock.send(CRLF)
614 except socket.error, val:
615 raise self.abort('socket error: %s' % val)
616
617 if not literator:
618 break
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000619
620 return tag
621
622
623 def _command_complete(self, name, tag):
624 try:
625 typ, data = self._get_tagged_response(tag)
626 except self.abort, val:
627 raise self.abort('command: %s => %s' % (name, val))
628 except self.error, val:
629 raise self.error('command: %s => %s' % (name, val))
630 if self.untagged_responses.has_key('BYE') and name != 'LOGOUT':
631 raise self.abort(self.untagged_responses['BYE'][-1])
632 if typ == 'BAD':
633 raise self.error('%s command error: %s %s' % (name, typ, data))
634 return typ, data
635
636
637 def _get_response(self):
638
639 # Read response and store.
640 #
641 # Returns None for continuation responses,
Guido van Rossum46586821998-05-18 14:39:42 +0000642 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000643
Guido van Rossum46586821998-05-18 14:39:42 +0000644 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000645
646 # Command completion response?
647
648 if self._match(self.tagre, resp):
649 tag = self.mo.group('tag')
650 if not self.tagged_commands.has_key(tag):
651 raise self.abort('unexpected tagged response: %s' % resp)
652
653 typ = self.mo.group('type')
654 dat = self.mo.group('data')
655 self.tagged_commands[tag] = (typ, [dat])
656 else:
657 dat2 = None
658
659 # '*' (untagged) responses?
660
661 if not self._match(Untagged_response, resp):
662 if self._match(Untagged_status, resp):
663 dat2 = self.mo.group('data2')
664
665 if self.mo is None:
666 # Only other possibility is '+' (continuation) rsponse...
667
668 if self._match(Continuation, resp):
669 self.continuation_response = self.mo.group('data')
670 return None # NB: indicates continuation
671
Guido van Rossumeda960a1998-06-18 14:24:28 +0000672 raise self.abort("unexpected response: '%s'" % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000673
674 typ = self.mo.group('type')
675 dat = self.mo.group('data')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000676 if dat is None: dat = '' # Null untagged response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000677 if dat2: dat = dat + ' ' + dat2
678
679 # Is there a literal to come?
680
681 while self._match(Literal, dat):
682
683 # Read literal direct from connection.
684
685 size = string.atoi(self.mo.group('size'))
686 if __debug__ and self.debug >= 4:
687 print '\tread literal size %s' % size
688 data = self.file.read(size)
689
690 # Store response with literal as tuple
691
692 self._append_untagged(typ, (dat, data))
693
694 # Read trailer - possibly containing another literal
695
Guido van Rossum46586821998-05-18 14:39:42 +0000696 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000697
698 self._append_untagged(typ, dat)
699
700 # Bracketed response information?
701
702 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
703 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
704
705 return resp
706
707
708 def _get_tagged_response(self, tag):
709
710 while 1:
711 result = self.tagged_commands[tag]
712 if result is not None:
713 del self.tagged_commands[tag]
714 return result
715 self._get_response()
716
717
718 def _get_line(self):
719
720 line = self.file.readline()
721 if not line:
722 raise EOFError
723
724 # Protocol mandates all lines terminated by CRLF
725
Guido van Rossum46586821998-05-18 14:39:42 +0000726 line = line[:-2]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000727 if __debug__ and self.debug >= 4:
Guido van Rossum46586821998-05-18 14:39:42 +0000728 print '\t< %s' % line
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000729 return line
730
731
732 def _match(self, cre, s):
733
734 # Run compiled regular expression match method on 's'.
735 # Save result, return success.
736
737 self.mo = cre.match(s)
738 if __debug__ and self.mo is not None and self.debug >= 5:
739 print "\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)
740 return self.mo is not None
741
742
743 def _new_tag(self):
744
745 tag = '%s%s' % (self.tagpre, self.tagnum)
746 self.tagnum = self.tagnum + 1
747 self.tagged_commands[tag] = None
748 return tag
749
750
Guido van Rossum46586821998-05-18 14:39:42 +0000751 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000752
Guido van Rossum46586821998-05-18 14:39:42 +0000753 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000754
755
756 def _untagged_response(self, typ, name):
757
758 if not self.untagged_responses.has_key(name):
759 return typ, [None]
760 data = self.untagged_responses[name]
Guido van Rossum46586821998-05-18 14:39:42 +0000761 if __debug__ and self.debug >= 5:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000762 print '\tuntagged_responses[%s] => %s' % (name, _trunc(20, `data`))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000763 del self.untagged_responses[name]
764 return typ, data
765
766
767
Guido van Rossumeda960a1998-06-18 14:24:28 +0000768class _Authenticator:
769
770 """Private class to provide en/decoding
771 for base64-based authentication conversation.
772 """
773
774 def __init__(self, mechinst):
775 self.mech = mechinst # Callable object to provide/process data
776
777 def process(self, data):
778 ret = self.mech(self.decode(data))
779 if ret is None:
780 return '*' # Abort conversation
781 return self.encode(ret)
782
783 def encode(self, inp):
784 #
785 # Invoke binascii.b2a_base64 iteratively with
786 # short even length buffers, strip the trailing
787 # line feed from the result and append. "Even"
788 # means a number that factors to both 6 and 8,
789 # so when it gets to the end of the 8-bit input
790 # there's no partial 6-bit output.
791 #
792 oup = ''
793 while inp:
794 if len(inp) > 48:
795 t = inp[:48]
796 inp = inp[48:]
797 else:
798 t = inp
799 inp = ''
800 e = binascii.b2a_base64(t)
801 if e:
802 oup = oup + e[:-1]
803 return oup
804
805 def decode(self, inp):
806 if not inp:
807 return ''
808 return binascii.a2b_base64(inp)
809
810
811
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000812Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
813 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
814
815def Internaldate2tuple(resp):
816
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000817 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000818
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000819 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000820 """
821
822 mo = InternalDate.match(resp)
823 if not mo:
824 return None
825
826 mon = Mon2num[mo.group('mon')]
827 zonen = mo.group('zonen')
828
829 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
830 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
831
832 # INTERNALDATE timezone must be subtracted to get UT
833
834 zone = (zoneh*60 + zonem)*60
835 if zonen == '-':
836 zone = -zone
837
838 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
839
840 utc = time.mktime(tt)
841
842 # Following is necessary because the time module has no 'mkgmtime'.
843 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
844
845 lt = time.localtime(utc)
846 if time.daylight and lt[-1]:
847 zone = zone + time.altzone
848 else:
849 zone = zone + time.timezone
850
851 return time.localtime(utc - zone)
852
853
854
855def Int2AP(num):
856
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000857 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000858
859 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000860 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000861 while num:
862 num, mod = divmod(num, 16)
863 val = AP[mod] + val
864 return val
865
866
867
868def ParseFlags(resp):
869
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000870 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000871
872 mo = Flags.match(resp)
873 if not mo:
874 return ()
875
876 return tuple(string.split(mo.group('flags')))
877
878
879def Time2Internaldate(date_time):
880
881 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
882
883 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
884 """
885
886 dttype = type(date_time)
887 if dttype is type(1):
888 tt = time.localtime(date_time)
889 elif dttype is type(()):
890 tt = date_time
891 elif dttype is type(""):
892 return date_time # Assume in correct format
893 else: raise ValueError
894
895 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
896 if dt[0] == '0':
897 dt = ' ' + dt[1:]
898 if time.daylight and tt[-1]:
899 zone = -time.altzone
900 else:
901 zone = -time.timezone
902 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
903
904
905
Guido van Rossumeda960a1998-06-18 14:24:28 +0000906if __debug__:
907
908 def _trunc(m, s):
909 if len(s) <= m: return s
910 return '%.*s..' % (m, s)
911
912
913
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000914if __debug__ and __name__ == '__main__':
915
Guido van Rossumb1f08121998-06-25 02:22:16 +0000916 import getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +0000917
Guido van Rossumb1f08121998-06-25 02:22:16 +0000918 host = ''
919 if sys.argv[1:]: host = sys.argv[1]
920
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000921 USER = getpass.getuser()
Guido van Rossumb1f08121998-06-25 02:22:16 +0000922 PASSWD = getpass.getpass("IMAP password for %s: " % (host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000923
924 test_seq1 = (
925 ('login', (USER, PASSWD)),
Guido van Rossum46586821998-05-18 14:39:42 +0000926 ('create', ('/tmp/xxx 1',)),
927 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
928 ('CREATE', ('/tmp/yyz 2',)),
929 ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
930 ('select', ('/tmp/yyz 2',)),
Guido van Rossumeda960a1998-06-18 14:24:28 +0000931 ('search', (None, '(TO zork)')),
932 ('partial', ('1', 'RFC822', 1, 1024)),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000933 ('store', ('1', 'FLAGS', '(\Deleted)')),
934 ('expunge', ()),
Guido van Rossum46586821998-05-18 14:39:42 +0000935 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000936 ('close', ()),
937 )
938
939 test_seq2 = (
940 ('select', ()),
941 ('response',('UIDVALIDITY',)),
942 ('uid', ('SEARCH', 'ALL')),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000943 ('response', ('EXISTS',)),
Guido van Rossum46586821998-05-18 14:39:42 +0000944 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000945 ('logout', ()),
946 )
947
948 def run(cmd, args):
949 typ, dat = apply(eval('M.%s' % cmd), args)
950 print ' %s %s\n => %s %s' % (cmd, args, typ, dat)
951 return dat
952
Guido van Rossumeda960a1998-06-18 14:24:28 +0000953 Debug = 5
Guido van Rossumd6596931998-05-29 18:08:48 +0000954 M = IMAP4(host)
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000955 print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000956
957 for cmd,args in test_seq1:
958 run(cmd, args)
959
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000960 for ml in run('list', ('/tmp/', 'yy%')):
Guido van Rossum46586821998-05-18 14:39:42 +0000961 mo = re.match(r'.*"([^"]+)"$', ml)
962 if mo: path = mo.group(1)
963 else: path = string.split(ml)[-1]
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000964 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000965
966 for cmd,args in test_seq2:
967 dat = run(cmd, args)
968
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000969 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
970 continue
971
Guido van Rossumeda960a1998-06-18 14:24:28 +0000972 uid = string.split(dat[-1])[-1]
Guido van Rossum46586821998-05-18 14:39:42 +0000973 run('uid', ('FETCH', '%s' % uid,
Guido van Rossumeda960a1998-06-18 14:24:28 +0000974 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))