blob: 8bab8d8582bea7cc347569813871c75a9b431098 [file] [log] [blame]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001"""IMAP4 client.
2
3Based on RFC 2060.
4
5Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
6
Guido van Rossumeda960a1998-06-18 14:24:28 +00007Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
8
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00009Public class: IMAP4
10Public variable: Debug
11Public functions: Internaldate2tuple
12 Int2AP
13 ParseFlags
14 Time2Internaldate
15"""
Guido van Rossumeda960a1998-06-18 14:24:28 +000016#
17# $Header$
18#
19__version__ = "$Revision$"
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000020
Guido van Rossumeda960a1998-06-18 14:24:28 +000021import binascii, re, socket, string, time, random
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000022
23# Globals
24
25CRLF = '\r\n'
26Debug = 0
27IMAP4_PORT = 143
Guido van Rossum38d8f4e1998-04-11 01:22:34 +000028AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000029
30# Commands
31
32Commands = {
33 # name valid states
34 'APPEND': ('AUTH', 'SELECTED'),
35 'AUTHENTICATE': ('NONAUTH',),
36 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
37 'CHECK': ('SELECTED',),
38 'CLOSE': ('SELECTED',),
39 'COPY': ('SELECTED',),
40 'CREATE': ('AUTH', 'SELECTED'),
41 'DELETE': ('AUTH', 'SELECTED'),
42 'EXAMINE': ('AUTH', 'SELECTED'),
43 'EXPUNGE': ('SELECTED',),
44 'FETCH': ('SELECTED',),
45 'LIST': ('AUTH', 'SELECTED'),
46 'LOGIN': ('NONAUTH',),
47 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
48 'LSUB': ('AUTH', 'SELECTED'),
49 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
Guido van Rossumeda960a1998-06-18 14:24:28 +000050 'PARTIAL': ('SELECTED',),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000051 'RENAME': ('AUTH', 'SELECTED'),
52 'SEARCH': ('SELECTED',),
53 'SELECT': ('AUTH', 'SELECTED'),
54 'STATUS': ('AUTH', 'SELECTED'),
55 'STORE': ('SELECTED',),
56 'SUBSCRIBE': ('AUTH', 'SELECTED'),
57 'UID': ('SELECTED',),
58 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
59 }
60
61# Patterns to match server responses
62
Guido van Rossumeda960a1998-06-18 14:24:28 +000063Continuation = re.compile(r'\+( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000064Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
65InternalDate = re.compile(r'.*INTERNALDATE "'
66 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
67 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
68 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
69 r'"')
70Literal = re.compile(r'(?P<data>.*) {(?P<size>\d+)}$')
71Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
Guido van Rossumeda960a1998-06-18 14:24:28 +000072Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000073Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
74
75
76
77class IMAP4:
78
79 """IMAP4 client class.
80
81 Instantiate with: IMAP4([host[, port]])
82
83 host - host's name (default: localhost);
84 port - port number (default: standard IMAP4 port).
85
86 All IMAP4rev1 commands are supported by methods of the same
Guido van Rossum6884af71998-05-29 13:34:03 +000087 name (in lower-case).
88
89 All arguments to commands are converted to strings, except for
90 the last argument to APPEND which is passed as an IMAP4
Guido van Rossumeda960a1998-06-18 14:24:28 +000091 literal. If necessary (the string contains white-space and
92 isn't enclosed with either parentheses or double quotes) each
93 string is quoted.
Guido van Rossum6884af71998-05-29 13:34:03 +000094
95 Each command returns a tuple: (type, [data, ...]) where 'type'
96 is usually 'OK' or 'NO', and 'data' is either the text from the
97 tagged response, or untagged results from command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000098
99 Errors raise the exception class <instance>.error("<reason>").
100 IMAP4 server errors raise <instance>.abort("<reason>"),
101 which is a sub-class of 'error'.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000102
103 Note: to use this module, you must read the RFCs pertaining
104 to the IMAP4 protocol, as the semantics of the arguments to
105 each IMAP4 command are left to the invoker, not to mention
106 the results.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000107 """
108
109 class error(Exception): pass # Logical errors - debug required
110 class abort(error): pass # Service errors - close and retry
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000111
112
113 def __init__(self, host = '', port = IMAP4_PORT):
114 self.host = host
115 self.port = port
116 self.debug = Debug
117 self.state = 'LOGOUT'
Guido van Rossum6884af71998-05-29 13:34:03 +0000118 self.literal = None # A literal argument to a command
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000119 self.tagged_commands = {} # Tagged commands awaiting response
120 self.untagged_responses = {} # {typ: [data, ...], ...}
121 self.continuation_response = '' # Last continuation response
122 self.tagnum = 0
123
124 # Open socket to server.
125
Guido van Rossumeda960a1998-06-18 14:24:28 +0000126 self.open(host, port)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000127
128 # Create unique tag for this session,
129 # and compile tagged response matcher.
130
Guido van Rossum6884af71998-05-29 13:34:03 +0000131 self.tagpre = Int2AP(random.randint(0, 31999))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000132 self.tagre = re.compile(r'(?P<tag>'
133 + self.tagpre
134 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
135
136 # Get server welcome message,
137 # request and store CAPABILITY response.
138
139 if __debug__ and self.debug >= 1:
140 print '\tnew IMAP4 connection, tag=%s' % self.tagpre
141
142 self.welcome = self._get_response()
143 if self.untagged_responses.has_key('PREAUTH'):
144 self.state = 'AUTH'
145 elif self.untagged_responses.has_key('OK'):
146 self.state = 'NONAUTH'
147# elif self.untagged_responses.has_key('BYE'):
148 else:
149 raise self.error(self.welcome)
150
151 cap = 'CAPABILITY'
152 self._simple_command(cap)
153 if not self.untagged_responses.has_key(cap):
154 raise self.error('no CAPABILITY response from server')
155 self.capabilities = tuple(string.split(self.untagged_responses[cap][-1]))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000156
157 if __debug__ and self.debug >= 3:
158 print '\tCAPABILITIES: %s' % `self.capabilities`
159
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000160 self.PROTOCOL_VERSION = None
161 for version in AllowedVersions:
162 if not version in self.capabilities:
163 continue
164 self.PROTOCOL_VERSION = version
165 break
166 if not self.PROTOCOL_VERSION:
167 raise self.error('server not IMAP4 compliant')
168
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000169
Guido van Rossumeda960a1998-06-18 14:24:28 +0000170 def open(self, host, port):
171 """Setup 'self.sock' and 'self.file'."""
172 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
173 self.sock.connect(self.host, self.port)
174 self.file = self.sock.makefile('r')
175
176
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000177 def __getattr__(self, attr):
178 """Allow UPPERCASE variants of all following IMAP4 commands."""
179 if Commands.has_key(attr):
180 return eval("self.%s" % string.lower(attr))
181 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
182
183
184 # Public methods
185
186
187 def append(self, mailbox, flags, date_time, message):
188 """Append message to named mailbox.
189
190 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
191 """
192 name = 'APPEND'
193 if flags:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000194 if (flags[0],flags[-1]) != ('(',')'):
195 flags = '(%s)' % flags
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000196 else:
197 flags = None
198 if date_time:
199 date_time = Time2Internaldate(date_time)
200 else:
201 date_time = None
Guido van Rossum6884af71998-05-29 13:34:03 +0000202 self.literal = message
203 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000204
205
Guido van Rossumeda960a1998-06-18 14:24:28 +0000206 def authenticate(self, mechanism, authobject):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000207 """Authenticate command - requires response processing.
208
Guido van Rossumeda960a1998-06-18 14:24:28 +0000209 'mechanism' specifies which authentication mechanism is to
210 be used - it must appear in <instance>.capabilities in the
211 form AUTH=<mechanism>.
212
213 'authobject' must be a callable object:
214
215 data = authobject(response)
216
217 It will be called to process server continuation responses.
218 It should return data that will be encoded and sent to server.
219 It should return None if the client abort response '*' should
220 be sent instead.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000221 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000222 mech = string.upper(mechanism)
223 cap = 'AUTH=%s' % mech
224 if not cap in self.capabilities:
225 raise self.error("Server doesn't allow %s authentication." % mech)
226 self.literal = _Authenticator(authobject).process
227 typ, dat = self._simple_command('AUTHENTICATE', mech)
228 if typ != 'OK':
229 raise self.error(dat)
230 self.state = 'AUTH'
231 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000232
233
234 def check(self):
235 """Checkpoint mailbox on server.
236
237 (typ, [data]) = <instance>.check()
238 """
239 return self._simple_command('CHECK')
240
241
242 def close(self):
243 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000244
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000245 Deleted messages are removed from writable mailbox.
246 This is the recommended command before 'LOGOUT'.
247
248 (typ, [data]) = <instance>.close()
249 """
250 try:
251 try: typ, dat = self._simple_command('CLOSE')
252 except EOFError: typ, dat = None, [None]
253 finally:
254 self.state = 'AUTH'
255 return typ, dat
256
257
258 def copy(self, message_set, new_mailbox):
259 """Copy 'message_set' messages onto end of 'new_mailbox'.
260
261 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
262 """
263 return self._simple_command('COPY', message_set, new_mailbox)
264
265
266 def create(self, mailbox):
267 """Create new mailbox.
268
269 (typ, [data]) = <instance>.create(mailbox)
270 """
271 return self._simple_command('CREATE', mailbox)
272
273
274 def delete(self, mailbox):
275 """Delete old mailbox.
276
277 (typ, [data]) = <instance>.delete(mailbox)
278 """
279 return self._simple_command('DELETE', mailbox)
280
281
282 def expunge(self):
283 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000284
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000285 Generates 'EXPUNGE' response for each deleted message.
286
287 (typ, [data]) = <instance>.expunge()
288
289 'data' is list of 'EXPUNGE'd message numbers in order received.
290 """
291 name = 'EXPUNGE'
292 typ, dat = self._simple_command(name)
293 return self._untagged_response(typ, name)
294
295
296 def fetch(self, message_set, message_parts):
297 """Fetch (parts of) messages.
298
299 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
300
301 'data' are tuples of message part envelope and data.
302 """
303 name = 'FETCH'
304 typ, dat = self._simple_command(name, message_set, message_parts)
305 return self._untagged_response(typ, name)
306
307
308 def list(self, directory='""', pattern='*'):
309 """List mailbox names in directory matching pattern.
310
311 (typ, [data]) = <instance>.list(directory='""', pattern='*')
312
313 'data' is list of LIST responses.
314 """
315 name = 'LIST'
316 typ, dat = self._simple_command(name, directory, pattern)
317 return self._untagged_response(typ, name)
318
319
320 def login(self, user, password):
321 """Identify client using plaintext password.
322
323 (typ, [data]) = <instance>.list(user, password)
324 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000325 typ, dat = self._simple_command('LOGIN', user, password)
326 if typ != 'OK':
327 raise self.error(dat)
328 self.state = 'AUTH'
329 return typ, dat
330
331
332 def logout(self):
333 """Shutdown connection to server.
334
335 (typ, [data]) = <instance>.logout()
336
337 Returns server 'BYE' response.
338 """
339 self.state = 'LOGOUT'
340 try: typ, dat = self._simple_command('LOGOUT')
341 except EOFError: typ, dat = None, [None]
342 self.file.close()
343 self.sock.close()
344 if self.untagged_responses.has_key('BYE'):
345 return 'BYE', self.untagged_responses['BYE']
346 return typ, dat
347
348
349 def lsub(self, directory='""', pattern='*'):
350 """List 'subscribed' mailbox names in directory matching pattern.
351
352 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
353
354 'data' are tuples of message part envelope and data.
355 """
356 name = 'LSUB'
357 typ, dat = self._simple_command(name, directory, pattern)
358 return self._untagged_response(typ, name)
359
360
Guido van Rossum6884af71998-05-29 13:34:03 +0000361 def noop(self):
362 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000363
Guido van Rossum6884af71998-05-29 13:34:03 +0000364 (typ, data) = <instance>.noop()
365 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000366 if __debug__ and self.debug >= 3:
367 print '\tuntagged responses: %s' % `self.untagged_responses`
Guido van Rossum6884af71998-05-29 13:34:03 +0000368 return self._simple_command('NOOP')
369
370
Guido van Rossumeda960a1998-06-18 14:24:28 +0000371 def partial(self, message_num, message_part, start, length):
372 """Fetch truncated part of a message.
373
374 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
375
376 'data' is tuple of message part envelope and data.
377 """
378 name = 'PARTIAL'
379 typ, dat = self._simple_command(name, message_num, message_part, start, length)
380 return self._untagged_response(typ, 'FETCH')
381
382
Guido van Rossum6884af71998-05-29 13:34:03 +0000383 def recent(self):
Guido van Rossumeda960a1998-06-18 14:24:28 +0000384 """Return most recent 'RECENT' responses if any exist,
Guido van Rossum6884af71998-05-29 13:34:03 +0000385 else prompt server for an update using the 'NOOP' command,
386 and flush all untagged responses.
Guido van Rossum46586821998-05-18 14:39:42 +0000387
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000388 (typ, [data]) = <instance>.recent()
389
390 'data' is None if no new messages,
Guido van Rossumeda960a1998-06-18 14:24:28 +0000391 else list of RECENT responses, most recent last.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000392 """
393 name = 'RECENT'
394 typ, dat = self._untagged_response('OK', name)
395 if dat[-1]:
396 return typ, dat
Guido van Rossum46586821998-05-18 14:39:42 +0000397 self.untagged_responses = {}
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000398 typ, dat = self._simple_command('NOOP')
399 return self._untagged_response(typ, name)
400
401
402 def rename(self, oldmailbox, newmailbox):
403 """Rename old mailbox name to new.
404
405 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
406 """
407 return self._simple_command('RENAME', oldmailbox, newmailbox)
408
409
410 def response(self, code):
411 """Return data for response 'code' if received, or None.
412
Guido van Rossum46586821998-05-18 14:39:42 +0000413 Old value for response 'code' is cleared.
414
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000415 (code, [data]) = <instance>.response(code)
416 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000417 return self._untagged_response(code, string.upper(code))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000418
419
420 def search(self, charset, criteria):
421 """Search mailbox for matching messages.
422
423 (typ, [data]) = <instance>.search(charset, criteria)
424
425 'data' is space separated list of matching message numbers.
426 """
427 name = 'SEARCH'
428 if charset:
429 charset = 'CHARSET ' + charset
430 typ, dat = self._simple_command(name, charset, criteria)
431 return self._untagged_response(typ, name)
432
433
434 def select(self, mailbox='INBOX', readonly=None):
435 """Select a mailbox.
436
Guido van Rossum46586821998-05-18 14:39:42 +0000437 Flush all untagged responses.
438
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000439 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
440
441 'data' is count of messages in mailbox ('EXISTS' response).
442 """
443 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
Guido van Rossum46586821998-05-18 14:39:42 +0000444 self.untagged_responses = {} # Flush old responses.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000445 if readonly:
446 name = 'EXAMINE'
447 else:
448 name = 'SELECT'
449 typ, dat = self._simple_command(name, mailbox)
450 if typ == 'OK':
451 self.state = 'SELECTED'
452 elif typ == 'NO':
453 self.state = 'AUTH'
454 if not readonly and not self.untagged_responses.has_key('READ-WRITE'):
455 raise self.error('%s is not writable' % mailbox)
456 return typ, self.untagged_responses.get('EXISTS', [None])
457
458
Guido van Rossumeda960a1998-06-18 14:24:28 +0000459 def socket(self):
460 """Return socket instance used to connect to IMAP4 server.
461
462 socket = <instance>.socket()
463 """
464 return self.sock
465
466
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000467 def status(self, mailbox, names):
468 """Request named status conditions for mailbox.
469
470 (typ, [data]) = <instance>.status(mailbox, names)
471 """
472 name = 'STATUS'
Guido van Rossumbe14e691998-04-11 03:11:51 +0000473 if self.PROTOCOL_VERSION == 'IMAP4':
474 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000475 typ, dat = self._simple_command(name, mailbox, names)
476 return self._untagged_response(typ, name)
477
478
479 def store(self, message_set, command, flag_list):
480 """Alters flag dispositions for messages in mailbox.
481
482 (typ, [data]) = <instance>.store(message_set, command, flag_list)
483 """
Guido van Rossum46586821998-05-18 14:39:42 +0000484 typ, dat = self._simple_command('STORE', message_set, command, flag_list)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000485 return self._untagged_response(typ, 'FETCH')
486
487
488 def subscribe(self, mailbox):
489 """Subscribe to new mailbox.
490
491 (typ, [data]) = <instance>.subscribe(mailbox)
492 """
493 return self._simple_command('SUBSCRIBE', mailbox)
494
495
Guido van Rossum46586821998-05-18 14:39:42 +0000496 def uid(self, command, *args):
497 """Execute "command arg ..." with messages identified by UID,
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000498 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000499
Guido van Rossum46586821998-05-18 14:39:42 +0000500 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000501
502 Returns response appropriate to 'command'.
503 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000504 command = string.upper(command)
505 if not Commands.has_key(command):
506 raise self.error("Unknown IMAP4 UID command: %s" % command)
507 if self.state not in Commands[command]:
508 raise self.error('command %s illegal in state %s'
509 % (command, self.state))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000510 name = 'UID'
Guido van Rossumeda960a1998-06-18 14:24:28 +0000511 typ, dat = apply(self._simple_command, (name, command) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000512 if command == 'SEARCH':
513 name = 'SEARCH'
514 else:
515 name = 'FETCH'
516 typ, dat2 = self._untagged_response(typ, name)
517 if dat2[-1]: dat = dat2
518 return typ, dat
519
520
521 def unsubscribe(self, mailbox):
522 """Unsubscribe from old mailbox.
523
524 (typ, [data]) = <instance>.unsubscribe(mailbox)
525 """
526 return self._simple_command('UNSUBSCRIBE', mailbox)
527
528
Guido van Rossum46586821998-05-18 14:39:42 +0000529 def xatom(self, name, *args):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000530 """Allow simple extension commands
531 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000532
Guido van Rossum46586821998-05-18 14:39:42 +0000533 (typ, [data]) = <instance>.xatom(name, arg, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000534 """
535 if name[0] != 'X' or not name in self.capabilities:
536 raise self.error('unknown extension command: %s' % name)
Guido van Rossum46586821998-05-18 14:39:42 +0000537 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000538
539
540
541 # Private methods
542
543
544 def _append_untagged(self, typ, dat):
545
Guido van Rossumeda960a1998-06-18 14:24:28 +0000546 ur = self.untagged_responses
547 if ur.has_key(typ):
548 ur[typ].append(dat)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000549 else:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000550 ur[typ] = [dat]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000551 if __debug__ and self.debug >= 5:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000552 print '\tuntagged_responses[%s] %s += %s' % (typ, len(`ur[typ]`), _trunc(20, `dat`))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000553
554
Guido van Rossum6884af71998-05-29 13:34:03 +0000555 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000556
557 if self.state not in Commands[name]:
Guido van Rossum6884af71998-05-29 13:34:03 +0000558 self.literal = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000559 raise self.error(
560 'command %s illegal in state %s' % (name, self.state))
561
Guido van Rossumeda960a1998-06-18 14:24:28 +0000562 if self.untagged_responses.has_key('OK'):
563 del self.untagged_responses['OK']
564
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000565 tag = self._new_tag()
566 data = '%s %s' % (tag, name)
Guido van Rossum6884af71998-05-29 13:34:03 +0000567 for d in args:
Guido van Rossum46586821998-05-18 14:39:42 +0000568 if d is None: continue
569 if type(d) is type(''):
570 l = len(string.split(d))
571 else:
572 l = 1
573 if l == 0 or l > 1 and (d[0],d[-1]) not in (('(',')'),('"','"')):
574 data = '%s "%s"' % (data, d)
575 else:
576 data = '%s %s' % (data, d)
Guido van Rossum6884af71998-05-29 13:34:03 +0000577
578 literal = self.literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000579 if literal is not None:
Guido van Rossum6884af71998-05-29 13:34:03 +0000580 self.literal = None
Guido van Rossumeda960a1998-06-18 14:24:28 +0000581 if type(literal) is type(self._command):
582 literator = literal
583 else:
584 literator = None
585 data = '%s {%s}' % (data, len(literal))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000586
587 try:
588 self.sock.send('%s%s' % (data, CRLF))
589 except socket.error, val:
590 raise self.abort('socket error: %s' % val)
591
592 if __debug__ and self.debug >= 4:
593 print '\t> %s' % data
594
595 if literal is None:
596 return tag
597
Guido van Rossumeda960a1998-06-18 14:24:28 +0000598 while 1:
599 # Wait for continuation response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000600
Guido van Rossumeda960a1998-06-18 14:24:28 +0000601 while self._get_response():
602 if self.tagged_commands[tag]: # BAD/NO?
603 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000604
Guido van Rossumeda960a1998-06-18 14:24:28 +0000605 # Send literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000606
Guido van Rossumeda960a1998-06-18 14:24:28 +0000607 if literator:
608 literal = literator(self.continuation_response)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000609
Guido van Rossumeda960a1998-06-18 14:24:28 +0000610 if __debug__ and self.debug >= 4:
611 print '\twrite literal size %s' % len(literal)
612
613 try:
614 self.sock.send(literal)
615 self.sock.send(CRLF)
616 except socket.error, val:
617 raise self.abort('socket error: %s' % val)
618
619 if not literator:
620 break
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000621
622 return tag
623
624
625 def _command_complete(self, name, tag):
626 try:
627 typ, data = self._get_tagged_response(tag)
628 except self.abort, val:
629 raise self.abort('command: %s => %s' % (name, val))
630 except self.error, val:
631 raise self.error('command: %s => %s' % (name, val))
632 if self.untagged_responses.has_key('BYE') and name != 'LOGOUT':
633 raise self.abort(self.untagged_responses['BYE'][-1])
634 if typ == 'BAD':
635 raise self.error('%s command error: %s %s' % (name, typ, data))
636 return typ, data
637
638
639 def _get_response(self):
640
641 # Read response and store.
642 #
643 # Returns None for continuation responses,
Guido van Rossum46586821998-05-18 14:39:42 +0000644 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000645
Guido van Rossum46586821998-05-18 14:39:42 +0000646 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000647
648 # Command completion response?
649
650 if self._match(self.tagre, resp):
651 tag = self.mo.group('tag')
652 if not self.tagged_commands.has_key(tag):
653 raise self.abort('unexpected tagged response: %s' % resp)
654
655 typ = self.mo.group('type')
656 dat = self.mo.group('data')
657 self.tagged_commands[tag] = (typ, [dat])
658 else:
659 dat2 = None
660
661 # '*' (untagged) responses?
662
663 if not self._match(Untagged_response, resp):
664 if self._match(Untagged_status, resp):
665 dat2 = self.mo.group('data2')
666
667 if self.mo is None:
668 # Only other possibility is '+' (continuation) rsponse...
669
670 if self._match(Continuation, resp):
671 self.continuation_response = self.mo.group('data')
672 return None # NB: indicates continuation
673
Guido van Rossumeda960a1998-06-18 14:24:28 +0000674 raise self.abort("unexpected response: '%s'" % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000675
676 typ = self.mo.group('type')
677 dat = self.mo.group('data')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000678 if dat is None: dat = '' # Null untagged response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000679 if dat2: dat = dat + ' ' + dat2
680
681 # Is there a literal to come?
682
683 while self._match(Literal, dat):
684
685 # Read literal direct from connection.
686
687 size = string.atoi(self.mo.group('size'))
688 if __debug__ and self.debug >= 4:
689 print '\tread literal size %s' % size
690 data = self.file.read(size)
691
692 # Store response with literal as tuple
693
694 self._append_untagged(typ, (dat, data))
695
696 # Read trailer - possibly containing another literal
697
Guido van Rossum46586821998-05-18 14:39:42 +0000698 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000699
700 self._append_untagged(typ, dat)
701
702 # Bracketed response information?
703
704 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
705 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
706
707 return resp
708
709
710 def _get_tagged_response(self, tag):
711
712 while 1:
713 result = self.tagged_commands[tag]
714 if result is not None:
715 del self.tagged_commands[tag]
716 return result
717 self._get_response()
718
719
720 def _get_line(self):
721
722 line = self.file.readline()
723 if not line:
724 raise EOFError
725
726 # Protocol mandates all lines terminated by CRLF
727
Guido van Rossum46586821998-05-18 14:39:42 +0000728 line = line[:-2]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000729 if __debug__ and self.debug >= 4:
Guido van Rossum46586821998-05-18 14:39:42 +0000730 print '\t< %s' % line
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000731 return line
732
733
734 def _match(self, cre, s):
735
736 # Run compiled regular expression match method on 's'.
737 # Save result, return success.
738
739 self.mo = cre.match(s)
740 if __debug__ and self.mo is not None and self.debug >= 5:
741 print "\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)
742 return self.mo is not None
743
744
745 def _new_tag(self):
746
747 tag = '%s%s' % (self.tagpre, self.tagnum)
748 self.tagnum = self.tagnum + 1
749 self.tagged_commands[tag] = None
750 return tag
751
752
Guido van Rossum46586821998-05-18 14:39:42 +0000753 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000754
Guido van Rossum46586821998-05-18 14:39:42 +0000755 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000756
757
758 def _untagged_response(self, typ, name):
759
760 if not self.untagged_responses.has_key(name):
761 return typ, [None]
762 data = self.untagged_responses[name]
Guido van Rossum46586821998-05-18 14:39:42 +0000763 if __debug__ and self.debug >= 5:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000764 print '\tuntagged_responses[%s] => %s' % (name, _trunc(20, `data`))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000765 del self.untagged_responses[name]
766 return typ, data
767
768
769
Guido van Rossumeda960a1998-06-18 14:24:28 +0000770class _Authenticator:
771
772 """Private class to provide en/decoding
773 for base64-based authentication conversation.
774 """
775
776 def __init__(self, mechinst):
777 self.mech = mechinst # Callable object to provide/process data
778
779 def process(self, data):
780 ret = self.mech(self.decode(data))
781 if ret is None:
782 return '*' # Abort conversation
783 return self.encode(ret)
784
785 def encode(self, inp):
786 #
787 # Invoke binascii.b2a_base64 iteratively with
788 # short even length buffers, strip the trailing
789 # line feed from the result and append. "Even"
790 # means a number that factors to both 6 and 8,
791 # so when it gets to the end of the 8-bit input
792 # there's no partial 6-bit output.
793 #
794 oup = ''
795 while inp:
796 if len(inp) > 48:
797 t = inp[:48]
798 inp = inp[48:]
799 else:
800 t = inp
801 inp = ''
802 e = binascii.b2a_base64(t)
803 if e:
804 oup = oup + e[:-1]
805 return oup
806
807 def decode(self, inp):
808 if not inp:
809 return ''
810 return binascii.a2b_base64(inp)
811
812
813
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000814Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
815 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
816
817def Internaldate2tuple(resp):
818
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000819 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000820
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000821 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000822 """
823
824 mo = InternalDate.match(resp)
825 if not mo:
826 return None
827
828 mon = Mon2num[mo.group('mon')]
829 zonen = mo.group('zonen')
830
831 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
832 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
833
834 # INTERNALDATE timezone must be subtracted to get UT
835
836 zone = (zoneh*60 + zonem)*60
837 if zonen == '-':
838 zone = -zone
839
840 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
841
842 utc = time.mktime(tt)
843
844 # Following is necessary because the time module has no 'mkgmtime'.
845 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
846
847 lt = time.localtime(utc)
848 if time.daylight and lt[-1]:
849 zone = zone + time.altzone
850 else:
851 zone = zone + time.timezone
852
853 return time.localtime(utc - zone)
854
855
856
857def Int2AP(num):
858
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000859 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000860
861 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000862 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000863 while num:
864 num, mod = divmod(num, 16)
865 val = AP[mod] + val
866 return val
867
868
869
870def ParseFlags(resp):
871
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000872 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000873
874 mo = Flags.match(resp)
875 if not mo:
876 return ()
877
878 return tuple(string.split(mo.group('flags')))
879
880
881def Time2Internaldate(date_time):
882
883 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
884
885 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
886 """
887
888 dttype = type(date_time)
889 if dttype is type(1):
890 tt = time.localtime(date_time)
891 elif dttype is type(()):
892 tt = date_time
893 elif dttype is type(""):
894 return date_time # Assume in correct format
895 else: raise ValueError
896
897 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
898 if dt[0] == '0':
899 dt = ' ' + dt[1:]
900 if time.daylight and tt[-1]:
901 zone = -time.altzone
902 else:
903 zone = -time.timezone
904 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
905
906
907
Guido van Rossumeda960a1998-06-18 14:24:28 +0000908if __debug__:
909
910 def _trunc(m, s):
911 if len(s) <= m: return s
912 return '%.*s..' % (m, s)
913
914
915
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000916if __debug__ and __name__ == '__main__':
917
Guido van Rossumd6596931998-05-29 18:08:48 +0000918 host = ''
919 import sys
920 if sys.argv[1:]:
921 host = sys.argv[1]
922
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000923 import getpass
924 USER = getpass.getuser()
Guido van Rossumd6596931998-05-29 18:08:48 +0000925 PASSWD = getpass.getpass(
926 "IMAP password for %s: " % (host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000927
928 test_seq1 = (
929 ('login', (USER, PASSWD)),
Guido van Rossum46586821998-05-18 14:39:42 +0000930 ('create', ('/tmp/xxx 1',)),
931 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
932 ('CREATE', ('/tmp/yyz 2',)),
933 ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
934 ('select', ('/tmp/yyz 2',)),
Guido van Rossumeda960a1998-06-18 14:24:28 +0000935 ('search', (None, '(TO zork)')),
936 ('partial', ('1', 'RFC822', 1, 1024)),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000937 ('store', ('1', 'FLAGS', '(\Deleted)')),
938 ('expunge', ()),
Guido van Rossum46586821998-05-18 14:39:42 +0000939 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000940 ('close', ()),
941 )
942
943 test_seq2 = (
944 ('select', ()),
945 ('response',('UIDVALIDITY',)),
946 ('uid', ('SEARCH', 'ALL')),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000947 ('response', ('EXISTS',)),
Guido van Rossum46586821998-05-18 14:39:42 +0000948 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000949 ('logout', ()),
950 )
951
952 def run(cmd, args):
953 typ, dat = apply(eval('M.%s' % cmd), args)
954 print ' %s %s\n => %s %s' % (cmd, args, typ, dat)
955 return dat
956
Guido van Rossumeda960a1998-06-18 14:24:28 +0000957 Debug = 5
Guido van Rossumd6596931998-05-29 18:08:48 +0000958 M = IMAP4(host)
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000959 print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000960
961 for cmd,args in test_seq1:
962 run(cmd, args)
963
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000964 for ml in run('list', ('/tmp/', 'yy%')):
Guido van Rossum46586821998-05-18 14:39:42 +0000965 mo = re.match(r'.*"([^"]+)"$', ml)
966 if mo: path = mo.group(1)
967 else: path = string.split(ml)[-1]
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000968 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000969
970 for cmd,args in test_seq2:
971 dat = run(cmd, args)
972
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000973 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
974 continue
975
Guido van Rossumeda960a1998-06-18 14:24:28 +0000976 uid = string.split(dat[-1])[-1]
Guido van Rossum46586821998-05-18 14:39:42 +0000977 run('uid', ('FETCH', '%s' % uid,
Guido van Rossumeda960a1998-06-18 14:24:28 +0000978 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))