blob: fcb7eb9df99b807662465f3114b0aaffd23817c5 [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
Guido van Rossum26367a01998-09-28 15:34:46 +000018__version__ = "2.15"
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000019
Guido van Rossum26367a01998-09-28 15:34:46 +000020import binascii, re, socket, string, time, random, sys
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>"),
Guido van Rossum26367a01998-09-28 15:34:46 +0000100 which is a sub-class of 'error'. Mailbox status changes
101 from READ-WRITE to READ-ONLY raise the exception class
102 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000103
104 Note: to use this module, you must read the RFCs pertaining
105 to the IMAP4 protocol, as the semantics of the arguments to
106 each IMAP4 command are left to the invoker, not to mention
107 the results.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000108 """
109
110 class error(Exception): pass # Logical errors - debug required
111 class abort(error): pass # Service errors - close and retry
Guido van Rossum26367a01998-09-28 15:34:46 +0000112 class readonly(abort): pass # Mailbox status changed to READ-ONLY
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000113
114
115 def __init__(self, host = '', port = IMAP4_PORT):
116 self.host = host
117 self.port = port
118 self.debug = Debug
119 self.state = 'LOGOUT'
Guido van Rossum6884af71998-05-29 13:34:03 +0000120 self.literal = None # A literal argument to a command
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000121 self.tagged_commands = {} # Tagged commands awaiting response
122 self.untagged_responses = {} # {typ: [data, ...], ...}
123 self.continuation_response = '' # Last continuation response
124 self.tagnum = 0
125
126 # Open socket to server.
127
Guido van Rossumeda960a1998-06-18 14:24:28 +0000128 self.open(host, port)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000129
130 # Create unique tag for this session,
131 # and compile tagged response matcher.
132
Guido van Rossum6884af71998-05-29 13:34:03 +0000133 self.tagpre = Int2AP(random.randint(0, 31999))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000134 self.tagre = re.compile(r'(?P<tag>'
135 + self.tagpre
136 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
137
138 # Get server welcome message,
139 # request and store CAPABILITY response.
140
141 if __debug__ and self.debug >= 1:
Guido van Rossum26367a01998-09-28 15:34:46 +0000142 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000143
144 self.welcome = self._get_response()
145 if self.untagged_responses.has_key('PREAUTH'):
146 self.state = 'AUTH'
147 elif self.untagged_responses.has_key('OK'):
148 self.state = 'NONAUTH'
149# elif self.untagged_responses.has_key('BYE'):
150 else:
151 raise self.error(self.welcome)
152
153 cap = 'CAPABILITY'
154 self._simple_command(cap)
155 if not self.untagged_responses.has_key(cap):
156 raise self.error('no CAPABILITY response from server')
157 self.capabilities = tuple(string.split(self.untagged_responses[cap][-1]))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000158
159 if __debug__ and self.debug >= 3:
Guido van Rossum26367a01998-09-28 15:34:46 +0000160 _mesg('CAPABILITIES: %s' % `self.capabilities`)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000161
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000162 for version in AllowedVersions:
163 if not version in self.capabilities:
164 continue
165 self.PROTOCOL_VERSION = version
Guido van Rossumb1f08121998-06-25 02:22:16 +0000166 return
167
168 raise self.error('server not IMAP4 compliant')
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000169
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000170
Guido van Rossum26367a01998-09-28 15:34:46 +0000171 def __getattr__(self, attr):
172 # Allow UPPERCASE variants of IMAP4 command methods.
173 if Commands.has_key(attr):
174 return eval("self.%s" % string.lower(attr))
175 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
176
177
178
179 # Public methods
180
181
Guido van Rossumeda960a1998-06-18 14:24:28 +0000182 def open(self, host, port):
183 """Setup 'self.sock' and 'self.file'."""
184 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
185 self.sock.connect(self.host, self.port)
186 self.file = self.sock.makefile('r')
187
188
Guido van Rossum26367a01998-09-28 15:34:46 +0000189 def recent(self):
190 """Return most recent 'RECENT' responses if any exist,
191 else prompt server for an update using the 'NOOP' command.
192
193 (typ, [data]) = <instance>.recent()
194
195 'data' is None if no new messages,
196 else list of RECENT responses, most recent last.
197 """
198 name = 'RECENT'
199 typ, dat = self._untagged_response('OK', [None], name)
200 if dat[-1]:
201 return typ, dat
202 typ, dat = self.noop() # Prod server for response
203 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000204
205
Guido van Rossum26367a01998-09-28 15:34:46 +0000206 def response(self, code):
207 """Return data for response 'code' if received, or None.
208
209 Old value for response 'code' is cleared.
210
211 (code, [data]) = <instance>.response(code)
212 """
213 return self._untagged_response(code, [None], string.upper(code))
214
215
216 def socket(self):
217 """Return socket instance used to connect to IMAP4 server.
218
219 socket = <instance>.socket()
220 """
221 return self.sock
222
223
224
225 # IMAP4 commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000226
227
228 def append(self, mailbox, flags, date_time, message):
229 """Append message to named mailbox.
230
231 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
232 """
233 name = 'APPEND'
234 if flags:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000235 if (flags[0],flags[-1]) != ('(',')'):
236 flags = '(%s)' % flags
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000237 else:
238 flags = None
239 if date_time:
240 date_time = Time2Internaldate(date_time)
241 else:
242 date_time = None
Guido van Rossum6884af71998-05-29 13:34:03 +0000243 self.literal = message
244 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000245
246
Guido van Rossumeda960a1998-06-18 14:24:28 +0000247 def authenticate(self, mechanism, authobject):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000248 """Authenticate command - requires response processing.
249
Guido van Rossumeda960a1998-06-18 14:24:28 +0000250 'mechanism' specifies which authentication mechanism is to
251 be used - it must appear in <instance>.capabilities in the
252 form AUTH=<mechanism>.
253
254 'authobject' must be a callable object:
255
256 data = authobject(response)
257
258 It will be called to process server continuation responses.
259 It should return data that will be encoded and sent to server.
260 It should return None if the client abort response '*' should
261 be sent instead.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000262 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000263 mech = string.upper(mechanism)
264 cap = 'AUTH=%s' % mech
265 if not cap in self.capabilities:
266 raise self.error("Server doesn't allow %s authentication." % mech)
267 self.literal = _Authenticator(authobject).process
268 typ, dat = self._simple_command('AUTHENTICATE', mech)
269 if typ != 'OK':
Guido van Rossum26367a01998-09-28 15:34:46 +0000270 raise self.error(dat[-1])
Guido van Rossumeda960a1998-06-18 14:24:28 +0000271 self.state = 'AUTH'
272 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000273
274
275 def check(self):
276 """Checkpoint mailbox on server.
277
278 (typ, [data]) = <instance>.check()
279 """
280 return self._simple_command('CHECK')
281
282
283 def close(self):
284 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000285
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000286 Deleted messages are removed from writable mailbox.
287 This is the recommended command before 'LOGOUT'.
288
289 (typ, [data]) = <instance>.close()
290 """
291 try:
Guido van Rossum26367a01998-09-28 15:34:46 +0000292 typ, dat = self._simple_command('CLOSE')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000293 finally:
294 self.state = 'AUTH'
295 return typ, dat
296
297
298 def copy(self, message_set, new_mailbox):
299 """Copy 'message_set' messages onto end of 'new_mailbox'.
300
301 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
302 """
303 return self._simple_command('COPY', message_set, new_mailbox)
304
305
306 def create(self, mailbox):
307 """Create new mailbox.
308
309 (typ, [data]) = <instance>.create(mailbox)
310 """
311 return self._simple_command('CREATE', mailbox)
312
313
314 def delete(self, mailbox):
315 """Delete old mailbox.
316
317 (typ, [data]) = <instance>.delete(mailbox)
318 """
319 return self._simple_command('DELETE', mailbox)
320
321
322 def expunge(self):
323 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000324
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000325 Generates 'EXPUNGE' response for each deleted message.
326
327 (typ, [data]) = <instance>.expunge()
328
329 'data' is list of 'EXPUNGE'd message numbers in order received.
330 """
331 name = 'EXPUNGE'
332 typ, dat = self._simple_command(name)
Guido van Rossum26367a01998-09-28 15:34:46 +0000333 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000334
335
336 def fetch(self, message_set, message_parts):
337 """Fetch (parts of) messages.
338
339 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
340
341 'data' are tuples of message part envelope and data.
342 """
343 name = 'FETCH'
344 typ, dat = self._simple_command(name, message_set, message_parts)
Guido van Rossum26367a01998-09-28 15:34:46 +0000345 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000346
347
348 def list(self, directory='""', pattern='*'):
349 """List mailbox names in directory matching pattern.
350
351 (typ, [data]) = <instance>.list(directory='""', pattern='*')
352
353 'data' is list of LIST responses.
354 """
355 name = 'LIST'
356 typ, dat = self._simple_command(name, directory, pattern)
Guido van Rossum26367a01998-09-28 15:34:46 +0000357 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000358
359
360 def login(self, user, password):
361 """Identify client using plaintext password.
362
363 (typ, [data]) = <instance>.list(user, password)
364 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000365 typ, dat = self._simple_command('LOGIN', user, password)
366 if typ != 'OK':
Guido van Rossum26367a01998-09-28 15:34:46 +0000367 raise self.error(dat[-1])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000368 self.state = 'AUTH'
369 return typ, dat
370
371
372 def logout(self):
373 """Shutdown connection to server.
374
375 (typ, [data]) = <instance>.logout()
376
377 Returns server 'BYE' response.
378 """
379 self.state = 'LOGOUT'
380 try: typ, dat = self._simple_command('LOGOUT')
Guido van Rossum26367a01998-09-28 15:34:46 +0000381 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000382 self.file.close()
383 self.sock.close()
384 if self.untagged_responses.has_key('BYE'):
385 return 'BYE', self.untagged_responses['BYE']
386 return typ, dat
387
388
389 def lsub(self, directory='""', pattern='*'):
390 """List 'subscribed' mailbox names in directory matching pattern.
391
392 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
393
394 'data' are tuples of message part envelope and data.
395 """
396 name = 'LSUB'
397 typ, dat = self._simple_command(name, directory, pattern)
Guido van Rossum26367a01998-09-28 15:34:46 +0000398 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000399
400
Guido van Rossum6884af71998-05-29 13:34:03 +0000401 def noop(self):
402 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000403
Guido van Rossum6884af71998-05-29 13:34:03 +0000404 (typ, data) = <instance>.noop()
405 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000406 if __debug__ and self.debug >= 3:
Guido van Rossum26367a01998-09-28 15:34:46 +0000407 _dump_ur(self.untagged_responses)
Guido van Rossum6884af71998-05-29 13:34:03 +0000408 return self._simple_command('NOOP')
409
410
Guido van Rossumeda960a1998-06-18 14:24:28 +0000411 def partial(self, message_num, message_part, start, length):
412 """Fetch truncated part of a message.
413
414 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
415
416 'data' is tuple of message part envelope and data.
417 """
418 name = 'PARTIAL'
419 typ, dat = self._simple_command(name, message_num, message_part, start, length)
Guido van Rossum26367a01998-09-28 15:34:46 +0000420 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000421
422
423 def rename(self, oldmailbox, newmailbox):
424 """Rename old mailbox name to new.
425
426 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
427 """
428 return self._simple_command('RENAME', oldmailbox, newmailbox)
429
430
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000431 def search(self, charset, criteria):
432 """Search mailbox for matching messages.
433
434 (typ, [data]) = <instance>.search(charset, criteria)
435
436 'data' is space separated list of matching message numbers.
437 """
438 name = 'SEARCH'
439 if charset:
440 charset = 'CHARSET ' + charset
441 typ, dat = self._simple_command(name, charset, criteria)
Guido van Rossum26367a01998-09-28 15:34:46 +0000442 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000443
444
445 def select(self, mailbox='INBOX', readonly=None):
446 """Select a mailbox.
447
Guido van Rossum46586821998-05-18 14:39:42 +0000448 Flush all untagged responses.
449
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000450 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
451
452 'data' is count of messages in mailbox ('EXISTS' response).
453 """
454 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
Guido van Rossum46586821998-05-18 14:39:42 +0000455 self.untagged_responses = {} # Flush old responses.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000456 if readonly:
457 name = 'EXAMINE'
458 else:
459 name = 'SELECT'
460 typ, dat = self._simple_command(name, mailbox)
Guido van Rossum26367a01998-09-28 15:34:46 +0000461 if typ != 'OK':
462 self.state = 'AUTH' # Might have been 'SELECTED'
463 return typ, dat
464 self.state = 'SELECTED'
465 if not self.untagged_responses.has_key('READ-WRITE') \
466 and not readonly:
467 if __debug__ and self.debug >= 1: _dump_ur(self.untagged_responses)
468 raise self.readonly('%s is not writable' % mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000469 return typ, self.untagged_responses.get('EXISTS', [None])
470
471
472 def status(self, mailbox, names):
473 """Request named status conditions for mailbox.
474
475 (typ, [data]) = <instance>.status(mailbox, names)
476 """
477 name = 'STATUS'
Guido van Rossumbe14e691998-04-11 03:11:51 +0000478 if self.PROTOCOL_VERSION == 'IMAP4':
479 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000480 typ, dat = self._simple_command(name, mailbox, names)
Guido van Rossum26367a01998-09-28 15:34:46 +0000481 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000482
483
484 def store(self, message_set, command, flag_list):
485 """Alters flag dispositions for messages in mailbox.
486
487 (typ, [data]) = <instance>.store(message_set, command, flag_list)
488 """
Guido van Rossum46586821998-05-18 14:39:42 +0000489 typ, dat = self._simple_command('STORE', message_set, command, flag_list)
Guido van Rossum26367a01998-09-28 15:34:46 +0000490 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000491
492
493 def subscribe(self, mailbox):
494 """Subscribe to new mailbox.
495
496 (typ, [data]) = <instance>.subscribe(mailbox)
497 """
498 return self._simple_command('SUBSCRIBE', mailbox)
499
500
Guido van Rossum46586821998-05-18 14:39:42 +0000501 def uid(self, command, *args):
502 """Execute "command arg ..." with messages identified by UID,
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000503 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000504
Guido van Rossum46586821998-05-18 14:39:42 +0000505 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000506
507 Returns response appropriate to 'command'.
508 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000509 command = string.upper(command)
510 if not Commands.has_key(command):
511 raise self.error("Unknown IMAP4 UID command: %s" % command)
512 if self.state not in Commands[command]:
513 raise self.error('command %s illegal in state %s'
514 % (command, self.state))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000515 name = 'UID'
Guido van Rossumeda960a1998-06-18 14:24:28 +0000516 typ, dat = apply(self._simple_command, (name, command) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000517 if command == 'SEARCH':
518 name = 'SEARCH'
519 else:
520 name = 'FETCH'
Guido van Rossum26367a01998-09-28 15:34:46 +0000521 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000522
523
524 def unsubscribe(self, mailbox):
525 """Unsubscribe from old mailbox.
526
527 (typ, [data]) = <instance>.unsubscribe(mailbox)
528 """
529 return self._simple_command('UNSUBSCRIBE', mailbox)
530
531
Guido van Rossum46586821998-05-18 14:39:42 +0000532 def xatom(self, name, *args):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000533 """Allow simple extension commands
534 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000535
Guido van Rossum46586821998-05-18 14:39:42 +0000536 (typ, [data]) = <instance>.xatom(name, arg, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000537 """
538 if name[0] != 'X' or not name in self.capabilities:
539 raise self.error('unknown extension command: %s' % name)
Guido van Rossum46586821998-05-18 14:39:42 +0000540 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000541
542
543
544 # Private methods
545
546
547 def _append_untagged(self, typ, dat):
548
Guido van Rossumeda960a1998-06-18 14:24:28 +0000549 ur = self.untagged_responses
Guido van Rossum26367a01998-09-28 15:34:46 +0000550 if __debug__ and self.debug >= 5:
551 _mesg('untagged_responses[%s] %s += %s' %
552 (typ, len(ur.get(typ,'')), dat))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000553 if ur.has_key(typ):
554 ur[typ].append(dat)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000555 else:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000556 ur[typ] = [dat]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000557
558
Guido van Rossum6884af71998-05-29 13:34:03 +0000559 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000560
561 if self.state not in Commands[name]:
Guido van Rossum6884af71998-05-29 13:34:03 +0000562 self.literal = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000563 raise self.error(
564 'command %s illegal in state %s' % (name, self.state))
565
Guido van Rossum26367a01998-09-28 15:34:46 +0000566 for typ in ('OK', 'NO', 'BAD'):
567 if self.untagged_responses.has_key(typ):
568 del self.untagged_responses[typ]
569
570 if self.untagged_responses.has_key('READ-WRITE') \
571 and self.untagged_responses.has_key('READ-ONLY'):
572 del self.untagged_responses['READ-WRITE']
573 raise self.readonly('mailbox status changed to READ-ONLY')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000574
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000575 tag = self._new_tag()
576 data = '%s %s' % (tag, name)
Guido van Rossum6884af71998-05-29 13:34:03 +0000577 for d in args:
Guido van Rossum46586821998-05-18 14:39:42 +0000578 if d is None: continue
579 if type(d) is type(''):
580 l = len(string.split(d))
581 else:
582 l = 1
583 if l == 0 or l > 1 and (d[0],d[-1]) not in (('(',')'),('"','"')):
584 data = '%s "%s"' % (data, d)
585 else:
586 data = '%s %s' % (data, d)
Guido van Rossum6884af71998-05-29 13:34:03 +0000587
588 literal = self.literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000589 if literal is not None:
Guido van Rossum6884af71998-05-29 13:34:03 +0000590 self.literal = None
Guido van Rossumeda960a1998-06-18 14:24:28 +0000591 if type(literal) is type(self._command):
592 literator = literal
593 else:
594 literator = None
595 data = '%s {%s}' % (data, len(literal))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000596
597 try:
598 self.sock.send('%s%s' % (data, CRLF))
599 except socket.error, val:
600 raise self.abort('socket error: %s' % val)
601
602 if __debug__ and self.debug >= 4:
Guido van Rossum26367a01998-09-28 15:34:46 +0000603 _mesg('> %s' % data)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000604
605 if literal is None:
606 return tag
607
Guido van Rossumeda960a1998-06-18 14:24:28 +0000608 while 1:
609 # Wait for continuation response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000610
Guido van Rossumeda960a1998-06-18 14:24:28 +0000611 while self._get_response():
612 if self.tagged_commands[tag]: # BAD/NO?
613 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000614
Guido van Rossumeda960a1998-06-18 14:24:28 +0000615 # Send literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000616
Guido van Rossumeda960a1998-06-18 14:24:28 +0000617 if literator:
618 literal = literator(self.continuation_response)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000619
Guido van Rossumeda960a1998-06-18 14:24:28 +0000620 if __debug__ and self.debug >= 4:
Guido van Rossum26367a01998-09-28 15:34:46 +0000621 _mesg('write literal size %s' % len(literal))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000622
623 try:
624 self.sock.send(literal)
625 self.sock.send(CRLF)
626 except socket.error, val:
627 raise self.abort('socket error: %s' % val)
628
629 if not literator:
630 break
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000631
632 return tag
633
634
635 def _command_complete(self, name, tag):
636 try:
637 typ, data = self._get_tagged_response(tag)
638 except self.abort, val:
639 raise self.abort('command: %s => %s' % (name, val))
640 except self.error, val:
641 raise self.error('command: %s => %s' % (name, val))
642 if self.untagged_responses.has_key('BYE') and name != 'LOGOUT':
643 raise self.abort(self.untagged_responses['BYE'][-1])
644 if typ == 'BAD':
645 raise self.error('%s command error: %s %s' % (name, typ, data))
646 return typ, data
647
648
649 def _get_response(self):
650
651 # Read response and store.
652 #
653 # Returns None for continuation responses,
Guido van Rossum46586821998-05-18 14:39:42 +0000654 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000655
Guido van Rossum46586821998-05-18 14:39:42 +0000656 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000657
658 # Command completion response?
659
660 if self._match(self.tagre, resp):
661 tag = self.mo.group('tag')
662 if not self.tagged_commands.has_key(tag):
663 raise self.abort('unexpected tagged response: %s' % resp)
664
665 typ = self.mo.group('type')
666 dat = self.mo.group('data')
667 self.tagged_commands[tag] = (typ, [dat])
668 else:
669 dat2 = None
670
671 # '*' (untagged) responses?
672
673 if not self._match(Untagged_response, resp):
674 if self._match(Untagged_status, resp):
675 dat2 = self.mo.group('data2')
676
677 if self.mo is None:
678 # Only other possibility is '+' (continuation) rsponse...
679
680 if self._match(Continuation, resp):
681 self.continuation_response = self.mo.group('data')
682 return None # NB: indicates continuation
683
Guido van Rossumeda960a1998-06-18 14:24:28 +0000684 raise self.abort("unexpected response: '%s'" % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000685
686 typ = self.mo.group('type')
687 dat = self.mo.group('data')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000688 if dat is None: dat = '' # Null untagged response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000689 if dat2: dat = dat + ' ' + dat2
690
691 # Is there a literal to come?
692
693 while self._match(Literal, dat):
694
695 # Read literal direct from connection.
696
697 size = string.atoi(self.mo.group('size'))
698 if __debug__ and self.debug >= 4:
Guido van Rossum26367a01998-09-28 15:34:46 +0000699 _mesg('read literal size %s' % size)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000700 data = self.file.read(size)
701
702 # Store response with literal as tuple
703
704 self._append_untagged(typ, (dat, data))
705
706 # Read trailer - possibly containing another literal
707
Guido van Rossum46586821998-05-18 14:39:42 +0000708 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000709
710 self._append_untagged(typ, dat)
711
712 # Bracketed response information?
713
714 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
715 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
716
Guido van Rossum26367a01998-09-28 15:34:46 +0000717 if __debug__ and self.debug >= 1 and typ in ('NO', 'BAD'):
718 _mesg('%s response: %s' % (typ, dat))
719
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000720 return resp
721
722
723 def _get_tagged_response(self, tag):
724
725 while 1:
726 result = self.tagged_commands[tag]
727 if result is not None:
728 del self.tagged_commands[tag]
729 return result
730 self._get_response()
731
732
733 def _get_line(self):
734
735 line = self.file.readline()
736 if not line:
Guido van Rossum26367a01998-09-28 15:34:46 +0000737 raise self.abort('socket error: EOF')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000738
739 # Protocol mandates all lines terminated by CRLF
740
Guido van Rossum46586821998-05-18 14:39:42 +0000741 line = line[:-2]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000742 if __debug__ and self.debug >= 4:
Guido van Rossum26367a01998-09-28 15:34:46 +0000743 _mesg('< %s' % line)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000744 return line
745
746
747 def _match(self, cre, s):
748
749 # Run compiled regular expression match method on 's'.
750 # Save result, return success.
751
752 self.mo = cre.match(s)
753 if __debug__ and self.mo is not None and self.debug >= 5:
Guido van Rossum26367a01998-09-28 15:34:46 +0000754 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000755 return self.mo is not None
756
757
758 def _new_tag(self):
759
760 tag = '%s%s' % (self.tagpre, self.tagnum)
761 self.tagnum = self.tagnum + 1
762 self.tagged_commands[tag] = None
763 return tag
764
765
Guido van Rossum46586821998-05-18 14:39:42 +0000766 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000767
Guido van Rossum46586821998-05-18 14:39:42 +0000768 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000769
770
Guido van Rossum26367a01998-09-28 15:34:46 +0000771 def _untagged_response(self, typ, dat, name):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000772
Guido van Rossum26367a01998-09-28 15:34:46 +0000773 if typ == 'NO':
774 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000775 if not self.untagged_responses.has_key(name):
776 return typ, [None]
777 data = self.untagged_responses[name]
Guido van Rossum46586821998-05-18 14:39:42 +0000778 if __debug__ and self.debug >= 5:
Guido van Rossum26367a01998-09-28 15:34:46 +0000779 _mesg('untagged_responses[%s] => %s' % (name, data))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000780 del self.untagged_responses[name]
781 return typ, data
782
783
784
Guido van Rossumeda960a1998-06-18 14:24:28 +0000785class _Authenticator:
786
787 """Private class to provide en/decoding
788 for base64-based authentication conversation.
789 """
790
791 def __init__(self, mechinst):
792 self.mech = mechinst # Callable object to provide/process data
793
794 def process(self, data):
795 ret = self.mech(self.decode(data))
796 if ret is None:
797 return '*' # Abort conversation
798 return self.encode(ret)
799
800 def encode(self, inp):
801 #
802 # Invoke binascii.b2a_base64 iteratively with
803 # short even length buffers, strip the trailing
804 # line feed from the result and append. "Even"
805 # means a number that factors to both 6 and 8,
806 # so when it gets to the end of the 8-bit input
807 # there's no partial 6-bit output.
808 #
809 oup = ''
810 while inp:
811 if len(inp) > 48:
812 t = inp[:48]
813 inp = inp[48:]
814 else:
815 t = inp
816 inp = ''
817 e = binascii.b2a_base64(t)
818 if e:
819 oup = oup + e[:-1]
820 return oup
821
822 def decode(self, inp):
823 if not inp:
824 return ''
825 return binascii.a2b_base64(inp)
826
827
828
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000829Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
830 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
831
832def Internaldate2tuple(resp):
833
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000834 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000835
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000836 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000837 """
838
839 mo = InternalDate.match(resp)
840 if not mo:
841 return None
842
843 mon = Mon2num[mo.group('mon')]
844 zonen = mo.group('zonen')
845
846 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
847 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
848
849 # INTERNALDATE timezone must be subtracted to get UT
850
851 zone = (zoneh*60 + zonem)*60
852 if zonen == '-':
853 zone = -zone
854
855 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
856
857 utc = time.mktime(tt)
858
859 # Following is necessary because the time module has no 'mkgmtime'.
860 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
861
862 lt = time.localtime(utc)
863 if time.daylight and lt[-1]:
864 zone = zone + time.altzone
865 else:
866 zone = zone + time.timezone
867
868 return time.localtime(utc - zone)
869
870
871
872def Int2AP(num):
873
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000874 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000875
876 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000877 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000878 while num:
879 num, mod = divmod(num, 16)
880 val = AP[mod] + val
881 return val
882
883
884
885def ParseFlags(resp):
886
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000887 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000888
889 mo = Flags.match(resp)
890 if not mo:
891 return ()
892
893 return tuple(string.split(mo.group('flags')))
894
895
896def Time2Internaldate(date_time):
897
898 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
899
900 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
901 """
902
903 dttype = type(date_time)
904 if dttype is type(1):
905 tt = time.localtime(date_time)
906 elif dttype is type(()):
907 tt = date_time
908 elif dttype is type(""):
909 return date_time # Assume in correct format
910 else: raise ValueError
911
912 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
913 if dt[0] == '0':
914 dt = ' ' + dt[1:]
915 if time.daylight and tt[-1]:
916 zone = -time.altzone
917 else:
918 zone = -time.timezone
919 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
920
921
922
Guido van Rossumeda960a1998-06-18 14:24:28 +0000923if __debug__:
924
Guido van Rossum26367a01998-09-28 15:34:46 +0000925 def _mesg(s):
926# if len(s) > 70: s = '%.70s..' % s
927 sys.stderr.write('\t'+s+'\n')
928 sys.stderr.flush()
929
930 def _dump_ur(dict):
931 # Dump untagged responses (in `dict').
932 l = dict.items()
933 if not l: return
934 t = '\n\t\t'
935 j = string.join
936 l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l)
937 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000938
939
940
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000941if __debug__ and __name__ == '__main__':
942
Guido van Rossumb1f08121998-06-25 02:22:16 +0000943 import getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +0000944
Guido van Rossumb1f08121998-06-25 02:22:16 +0000945 host = ''
946 if sys.argv[1:]: host = sys.argv[1]
947
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000948 USER = getpass.getuser()
Guido van Rossumb1f08121998-06-25 02:22:16 +0000949 PASSWD = getpass.getpass("IMAP password for %s: " % (host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000950
951 test_seq1 = (
952 ('login', (USER, PASSWD)),
Guido van Rossum46586821998-05-18 14:39:42 +0000953 ('create', ('/tmp/xxx 1',)),
954 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
955 ('CREATE', ('/tmp/yyz 2',)),
956 ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
957 ('select', ('/tmp/yyz 2',)),
Guido van Rossumeda960a1998-06-18 14:24:28 +0000958 ('search', (None, '(TO zork)')),
959 ('partial', ('1', 'RFC822', 1, 1024)),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000960 ('store', ('1', 'FLAGS', '(\Deleted)')),
961 ('expunge', ()),
Guido van Rossum46586821998-05-18 14:39:42 +0000962 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000963 ('close', ()),
964 )
965
966 test_seq2 = (
967 ('select', ()),
968 ('response',('UIDVALIDITY',)),
969 ('uid', ('SEARCH', 'ALL')),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000970 ('response', ('EXISTS',)),
Guido van Rossum46586821998-05-18 14:39:42 +0000971 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000972 ('logout', ()),
973 )
974
975 def run(cmd, args):
976 typ, dat = apply(eval('M.%s' % cmd), args)
Guido van Rossum26367a01998-09-28 15:34:46 +0000977 _mesg(' %s %s\n => %s %s' % (cmd, args, typ, dat))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000978 return dat
979
Guido van Rossumeda960a1998-06-18 14:24:28 +0000980 Debug = 5
Guido van Rossumd6596931998-05-29 18:08:48 +0000981 M = IMAP4(host)
Guido van Rossum26367a01998-09-28 15:34:46 +0000982 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000983
984 for cmd,args in test_seq1:
985 run(cmd, args)
986
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000987 for ml in run('list', ('/tmp/', 'yy%')):
Guido van Rossum46586821998-05-18 14:39:42 +0000988 mo = re.match(r'.*"([^"]+)"$', ml)
989 if mo: path = mo.group(1)
990 else: path = string.split(ml)[-1]
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000991 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000992
993 for cmd,args in test_seq2:
994 dat = run(cmd, args)
995
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000996 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
997 continue
998
Guido van Rossumeda960a1998-06-18 14:24:28 +0000999 uid = string.split(dat[-1])[-1]
Guido van Rossum46586821998-05-18 14:39:42 +00001000 run('uid', ('FETCH', '%s' % uid,
Guido van Rossumeda960a1998-06-18 14:24:28 +00001001 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))