blob: 027557132331c5b9f34d084235ce4665eeea8bd0 [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 Rossum8c062211999-12-13 23:27:45 +000018__version__ = "2.16"
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
Guido van Rossum8c062211999-12-13 23:27:45 +000092 double quotes) each string is quoted. However, the 'password'
93 argument to the LOGIN command is always 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>"),
Guido van Rossum26367a01998-09-28 15:34:46 +0000101 which is a sub-class of 'error'. Mailbox status changes
102 from READ-WRITE to READ-ONLY raise the exception class
103 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000104
Guido van Rossum8c062211999-12-13 23:27:45 +0000105 "error" exceptions imply a program error.
106 "abort" exceptions imply the connection should be reset, and
107 the command re-tried.
108 "readonly" exceptions imply the command should be re-tried.
109
Guido van Rossumeda960a1998-06-18 14:24:28 +0000110 Note: to use this module, you must read the RFCs pertaining
111 to the IMAP4 protocol, as the semantics of the arguments to
112 each IMAP4 command are left to the invoker, not to mention
113 the results.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000114 """
115
116 class error(Exception): pass # Logical errors - debug required
117 class abort(error): pass # Service errors - close and retry
Guido van Rossum26367a01998-09-28 15:34:46 +0000118 class readonly(abort): pass # Mailbox status changed to READ-ONLY
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000119
Guido van Rossum8c062211999-12-13 23:27:45 +0000120 mustquote = re.compile(r'\W') # Match any non-alphanumeric character
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000121
122 def __init__(self, host = '', port = IMAP4_PORT):
123 self.host = host
124 self.port = port
125 self.debug = Debug
126 self.state = 'LOGOUT'
Guido van Rossum6884af71998-05-29 13:34:03 +0000127 self.literal = None # A literal argument to a command
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000128 self.tagged_commands = {} # Tagged commands awaiting response
129 self.untagged_responses = {} # {typ: [data, ...], ...}
130 self.continuation_response = '' # Last continuation response
131 self.tagnum = 0
132
133 # Open socket to server.
134
Guido van Rossumeda960a1998-06-18 14:24:28 +0000135 self.open(host, port)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000136
137 # Create unique tag for this session,
138 # and compile tagged response matcher.
139
Guido van Rossum6884af71998-05-29 13:34:03 +0000140 self.tagpre = Int2AP(random.randint(0, 31999))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000141 self.tagre = re.compile(r'(?P<tag>'
142 + self.tagpre
143 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
144
145 # Get server welcome message,
146 # request and store CAPABILITY response.
147
Guido van Rossum8c062211999-12-13 23:27:45 +0000148 if __debug__:
149 if self.debug >= 1:
150 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000151
152 self.welcome = self._get_response()
153 if self.untagged_responses.has_key('PREAUTH'):
154 self.state = 'AUTH'
155 elif self.untagged_responses.has_key('OK'):
156 self.state = 'NONAUTH'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000157 else:
158 raise self.error(self.welcome)
159
160 cap = 'CAPABILITY'
161 self._simple_command(cap)
162 if not self.untagged_responses.has_key(cap):
163 raise self.error('no CAPABILITY response from server')
Guido van Rossum04da10c1998-10-21 22:06:56 +0000164 self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1])))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000165
Guido van Rossum8c062211999-12-13 23:27:45 +0000166 if __debug__:
167 if self.debug >= 3:
168 _mesg('CAPABILITIES: %s' % `self.capabilities`)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000169
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000170 for version in AllowedVersions:
171 if not version in self.capabilities:
172 continue
173 self.PROTOCOL_VERSION = version
Guido van Rossumb1f08121998-06-25 02:22:16 +0000174 return
175
176 raise self.error('server not IMAP4 compliant')
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000177
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000178
Guido van Rossum26367a01998-09-28 15:34:46 +0000179 def __getattr__(self, attr):
180 # Allow UPPERCASE variants of IMAP4 command methods.
181 if Commands.has_key(attr):
182 return eval("self.%s" % string.lower(attr))
183 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
184
185
186
187 # Public methods
188
189
Guido van Rossumeda960a1998-06-18 14:24:28 +0000190 def open(self, host, port):
191 """Setup 'self.sock' and 'self.file'."""
192 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
193 self.sock.connect(self.host, self.port)
194 self.file = self.sock.makefile('r')
195
196
Guido van Rossum26367a01998-09-28 15:34:46 +0000197 def recent(self):
198 """Return most recent 'RECENT' responses if any exist,
199 else prompt server for an update using the 'NOOP' command.
200
201 (typ, [data]) = <instance>.recent()
202
203 'data' is None if no new messages,
204 else list of RECENT responses, most recent last.
205 """
206 name = 'RECENT'
207 typ, dat = self._untagged_response('OK', [None], name)
208 if dat[-1]:
209 return typ, dat
210 typ, dat = self.noop() # Prod server for response
211 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000212
213
Guido van Rossum26367a01998-09-28 15:34:46 +0000214 def response(self, code):
215 """Return data for response 'code' if received, or None.
216
217 Old value for response 'code' is cleared.
218
219 (code, [data]) = <instance>.response(code)
220 """
221 return self._untagged_response(code, [None], string.upper(code))
222
223
224 def socket(self):
225 """Return socket instance used to connect to IMAP4 server.
226
227 socket = <instance>.socket()
228 """
229 return self.sock
230
231
232
233 # IMAP4 commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000234
235
236 def append(self, mailbox, flags, date_time, message):
237 """Append message to named mailbox.
238
239 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
Guido van Rossum8c062211999-12-13 23:27:45 +0000240
241 All args except `message' can be None.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000242 """
243 name = 'APPEND'
Guido van Rossum8c062211999-12-13 23:27:45 +0000244 if not mailbox:
245 mailbox = 'INBOX'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000246 if flags:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000247 if (flags[0],flags[-1]) != ('(',')'):
248 flags = '(%s)' % flags
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000249 else:
250 flags = None
251 if date_time:
252 date_time = Time2Internaldate(date_time)
253 else:
254 date_time = None
Guido van Rossum6884af71998-05-29 13:34:03 +0000255 self.literal = message
256 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000257
258
Guido van Rossumeda960a1998-06-18 14:24:28 +0000259 def authenticate(self, mechanism, authobject):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000260 """Authenticate command - requires response processing.
261
Guido van Rossumeda960a1998-06-18 14:24:28 +0000262 'mechanism' specifies which authentication mechanism is to
263 be used - it must appear in <instance>.capabilities in the
264 form AUTH=<mechanism>.
265
266 'authobject' must be a callable object:
267
268 data = authobject(response)
269
270 It will be called to process server continuation responses.
271 It should return data that will be encoded and sent to server.
272 It should return None if the client abort response '*' should
273 be sent instead.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000274 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000275 mech = string.upper(mechanism)
276 cap = 'AUTH=%s' % mech
277 if not cap in self.capabilities:
278 raise self.error("Server doesn't allow %s authentication." % mech)
279 self.literal = _Authenticator(authobject).process
280 typ, dat = self._simple_command('AUTHENTICATE', mech)
281 if typ != 'OK':
Guido van Rossum26367a01998-09-28 15:34:46 +0000282 raise self.error(dat[-1])
Guido van Rossumeda960a1998-06-18 14:24:28 +0000283 self.state = 'AUTH'
284 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000285
286
287 def check(self):
288 """Checkpoint mailbox on server.
289
290 (typ, [data]) = <instance>.check()
291 """
292 return self._simple_command('CHECK')
293
294
295 def close(self):
296 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000297
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000298 Deleted messages are removed from writable mailbox.
299 This is the recommended command before 'LOGOUT'.
300
301 (typ, [data]) = <instance>.close()
302 """
303 try:
Guido van Rossum26367a01998-09-28 15:34:46 +0000304 typ, dat = self._simple_command('CLOSE')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000305 finally:
306 self.state = 'AUTH'
307 return typ, dat
308
309
310 def copy(self, message_set, new_mailbox):
311 """Copy 'message_set' messages onto end of 'new_mailbox'.
312
313 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
314 """
315 return self._simple_command('COPY', message_set, new_mailbox)
316
317
318 def create(self, mailbox):
319 """Create new mailbox.
320
321 (typ, [data]) = <instance>.create(mailbox)
322 """
323 return self._simple_command('CREATE', mailbox)
324
325
326 def delete(self, mailbox):
327 """Delete old mailbox.
328
329 (typ, [data]) = <instance>.delete(mailbox)
330 """
331 return self._simple_command('DELETE', mailbox)
332
333
334 def expunge(self):
335 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000336
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000337 Generates 'EXPUNGE' response for each deleted message.
338
339 (typ, [data]) = <instance>.expunge()
340
341 'data' is list of 'EXPUNGE'd message numbers in order received.
342 """
343 name = 'EXPUNGE'
344 typ, dat = self._simple_command(name)
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 fetch(self, message_set, message_parts):
349 """Fetch (parts of) messages.
350
351 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
352
353 'data' are tuples of message part envelope and data.
354 """
355 name = 'FETCH'
356 typ, dat = self._simple_command(name, message_set, message_parts)
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 list(self, directory='""', pattern='*'):
361 """List mailbox names in directory matching pattern.
362
363 (typ, [data]) = <instance>.list(directory='""', pattern='*')
364
365 'data' is list of LIST responses.
366 """
367 name = 'LIST'
368 typ, dat = self._simple_command(name, directory, pattern)
Guido van Rossum26367a01998-09-28 15:34:46 +0000369 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000370
371
372 def login(self, user, password):
373 """Identify client using plaintext password.
374
Guido van Rossum8c062211999-12-13 23:27:45 +0000375 (typ, [data]) = <instance>.login(user, password)
376
377 NB: 'password' will be quoted.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000378 """
Guido van Rossum8c062211999-12-13 23:27:45 +0000379 #if not 'AUTH=LOGIN' in self.capabilities:
380 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
381 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000382 if typ != 'OK':
Guido van Rossum26367a01998-09-28 15:34:46 +0000383 raise self.error(dat[-1])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000384 self.state = 'AUTH'
385 return typ, dat
386
387
388 def logout(self):
389 """Shutdown connection to server.
390
391 (typ, [data]) = <instance>.logout()
392
393 Returns server 'BYE' response.
394 """
395 self.state = 'LOGOUT'
396 try: typ, dat = self._simple_command('LOGOUT')
Guido van Rossum26367a01998-09-28 15:34:46 +0000397 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000398 self.file.close()
399 self.sock.close()
400 if self.untagged_responses.has_key('BYE'):
401 return 'BYE', self.untagged_responses['BYE']
402 return typ, dat
403
404
405 def lsub(self, directory='""', pattern='*'):
406 """List 'subscribed' mailbox names in directory matching pattern.
407
408 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
409
410 'data' are tuples of message part envelope and data.
411 """
412 name = 'LSUB'
413 typ, dat = self._simple_command(name, directory, pattern)
Guido van Rossum26367a01998-09-28 15:34:46 +0000414 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000415
416
Guido van Rossum6884af71998-05-29 13:34:03 +0000417 def noop(self):
418 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000419
Guido van Rossum6884af71998-05-29 13:34:03 +0000420 (typ, data) = <instance>.noop()
421 """
Guido van Rossum8c062211999-12-13 23:27:45 +0000422 if __debug__:
423 if self.debug >= 3:
424 _dump_ur(self.untagged_responses)
Guido van Rossum6884af71998-05-29 13:34:03 +0000425 return self._simple_command('NOOP')
426
427
Guido van Rossumeda960a1998-06-18 14:24:28 +0000428 def partial(self, message_num, message_part, start, length):
429 """Fetch truncated part of a message.
430
431 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
432
433 'data' is tuple of message part envelope and data.
434 """
435 name = 'PARTIAL'
436 typ, dat = self._simple_command(name, message_num, message_part, start, length)
Guido van Rossum26367a01998-09-28 15:34:46 +0000437 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000438
439
440 def rename(self, oldmailbox, newmailbox):
441 """Rename old mailbox name to new.
442
443 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
444 """
445 return self._simple_command('RENAME', oldmailbox, newmailbox)
446
447
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000448 def search(self, charset, criteria):
449 """Search mailbox for matching messages.
450
451 (typ, [data]) = <instance>.search(charset, criteria)
452
453 'data' is space separated list of matching message numbers.
454 """
455 name = 'SEARCH'
456 if charset:
457 charset = 'CHARSET ' + charset
458 typ, dat = self._simple_command(name, charset, criteria)
Guido van Rossum26367a01998-09-28 15:34:46 +0000459 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000460
461
462 def select(self, mailbox='INBOX', readonly=None):
463 """Select a mailbox.
464
Guido van Rossum46586821998-05-18 14:39:42 +0000465 Flush all untagged responses.
466
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000467 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
468
469 'data' is count of messages in mailbox ('EXISTS' response).
470 """
471 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
Guido van Rossum46586821998-05-18 14:39:42 +0000472 self.untagged_responses = {} # Flush old responses.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000473 if readonly:
474 name = 'EXAMINE'
475 else:
476 name = 'SELECT'
477 typ, dat = self._simple_command(name, mailbox)
Guido van Rossum26367a01998-09-28 15:34:46 +0000478 if typ != 'OK':
479 self.state = 'AUTH' # Might have been 'SELECTED'
480 return typ, dat
481 self.state = 'SELECTED'
482 if not self.untagged_responses.has_key('READ-WRITE') \
483 and not readonly:
Guido van Rossum8c062211999-12-13 23:27:45 +0000484 if __debug__:
485 if self.debug >= 1:
486 _dump_ur(self.untagged_responses)
Guido van Rossum26367a01998-09-28 15:34:46 +0000487 raise self.readonly('%s is not writable' % mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000488 return typ, self.untagged_responses.get('EXISTS', [None])
489
490
491 def status(self, mailbox, names):
492 """Request named status conditions for mailbox.
493
494 (typ, [data]) = <instance>.status(mailbox, names)
495 """
496 name = 'STATUS'
Guido van Rossumbe14e691998-04-11 03:11:51 +0000497 if self.PROTOCOL_VERSION == 'IMAP4':
498 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000499 typ, dat = self._simple_command(name, mailbox, names)
Guido van Rossum26367a01998-09-28 15:34:46 +0000500 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000501
502
503 def store(self, message_set, command, flag_list):
504 """Alters flag dispositions for messages in mailbox.
505
506 (typ, [data]) = <instance>.store(message_set, command, flag_list)
507 """
Guido van Rossum46586821998-05-18 14:39:42 +0000508 typ, dat = self._simple_command('STORE', message_set, command, flag_list)
Guido van Rossum26367a01998-09-28 15:34:46 +0000509 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000510
511
512 def subscribe(self, mailbox):
513 """Subscribe to new mailbox.
514
515 (typ, [data]) = <instance>.subscribe(mailbox)
516 """
517 return self._simple_command('SUBSCRIBE', mailbox)
518
519
Guido van Rossum46586821998-05-18 14:39:42 +0000520 def uid(self, command, *args):
521 """Execute "command arg ..." with messages identified by UID,
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000522 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000523
Guido van Rossum46586821998-05-18 14:39:42 +0000524 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000525
526 Returns response appropriate to 'command'.
527 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000528 command = string.upper(command)
529 if not Commands.has_key(command):
530 raise self.error("Unknown IMAP4 UID command: %s" % command)
531 if self.state not in Commands[command]:
532 raise self.error('command %s illegal in state %s'
533 % (command, self.state))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000534 name = 'UID'
Guido van Rossumeda960a1998-06-18 14:24:28 +0000535 typ, dat = apply(self._simple_command, (name, command) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000536 if command == 'SEARCH':
537 name = 'SEARCH'
538 else:
539 name = 'FETCH'
Guido van Rossum26367a01998-09-28 15:34:46 +0000540 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000541
542
543 def unsubscribe(self, mailbox):
544 """Unsubscribe from old mailbox.
545
546 (typ, [data]) = <instance>.unsubscribe(mailbox)
547 """
548 return self._simple_command('UNSUBSCRIBE', mailbox)
549
550
Guido van Rossum46586821998-05-18 14:39:42 +0000551 def xatom(self, name, *args):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000552 """Allow simple extension commands
553 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000554
Guido van Rossum46586821998-05-18 14:39:42 +0000555 (typ, [data]) = <instance>.xatom(name, arg, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000556 """
557 if name[0] != 'X' or not name in self.capabilities:
558 raise self.error('unknown extension command: %s' % name)
Guido van Rossum46586821998-05-18 14:39:42 +0000559 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000560
561
562
563 # Private methods
564
565
566 def _append_untagged(self, typ, dat):
567
Guido van Rossum8c062211999-12-13 23:27:45 +0000568 if dat is None: dat = ''
Guido van Rossumeda960a1998-06-18 14:24:28 +0000569 ur = self.untagged_responses
Guido van Rossum8c062211999-12-13 23:27:45 +0000570 if __debug__:
571 if self.debug >= 5:
572 _mesg('untagged_responses[%s] %s += ["%s"]' %
573 (typ, len(ur.get(typ,'')), dat))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000574 if ur.has_key(typ):
575 ur[typ].append(dat)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000576 else:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000577 ur[typ] = [dat]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000578
579
Guido van Rossum8c062211999-12-13 23:27:45 +0000580 def _check_bye(self):
581 bye = self.untagged_responses.get('BYE')
582 if bye:
583 raise self.abort(bye[-1])
584
585
Guido van Rossum6884af71998-05-29 13:34:03 +0000586 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000587
588 if self.state not in Commands[name]:
Guido van Rossum6884af71998-05-29 13:34:03 +0000589 self.literal = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000590 raise self.error(
591 'command %s illegal in state %s' % (name, self.state))
592
Guido van Rossum26367a01998-09-28 15:34:46 +0000593 for typ in ('OK', 'NO', 'BAD'):
594 if self.untagged_responses.has_key(typ):
595 del self.untagged_responses[typ]
596
597 if self.untagged_responses.has_key('READ-WRITE') \
598 and self.untagged_responses.has_key('READ-ONLY'):
599 del self.untagged_responses['READ-WRITE']
600 raise self.readonly('mailbox status changed to READ-ONLY')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000601
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000602 tag = self._new_tag()
603 data = '%s %s' % (tag, name)
Guido van Rossum8c062211999-12-13 23:27:45 +0000604 for arg in args:
605 if arg is None: continue
606 data = '%s %s' % (data, self._checkquote(arg))
Guido van Rossum6884af71998-05-29 13:34:03 +0000607
608 literal = self.literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000609 if literal is not None:
Guido van Rossum6884af71998-05-29 13:34:03 +0000610 self.literal = None
Guido van Rossumeda960a1998-06-18 14:24:28 +0000611 if type(literal) is type(self._command):
612 literator = literal
613 else:
614 literator = None
615 data = '%s {%s}' % (data, len(literal))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000616
Guido van Rossum8c062211999-12-13 23:27:45 +0000617 if __debug__:
618 if self.debug >= 4:
619 _mesg('> %s' % data)
620 else:
621 _log('> %s' % data)
622
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000623 try:
624 self.sock.send('%s%s' % (data, CRLF))
625 except socket.error, val:
626 raise self.abort('socket error: %s' % val)
627
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000628 if literal is None:
629 return tag
630
Guido van Rossumeda960a1998-06-18 14:24:28 +0000631 while 1:
632 # Wait for continuation response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000633
Guido van Rossumeda960a1998-06-18 14:24:28 +0000634 while self._get_response():
635 if self.tagged_commands[tag]: # BAD/NO?
636 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000637
Guido van Rossumeda960a1998-06-18 14:24:28 +0000638 # Send literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000639
Guido van Rossumeda960a1998-06-18 14:24:28 +0000640 if literator:
641 literal = literator(self.continuation_response)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000642
Guido van Rossum8c062211999-12-13 23:27:45 +0000643 if __debug__:
644 if self.debug >= 4:
645 _mesg('write literal size %s' % len(literal))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000646
647 try:
648 self.sock.send(literal)
649 self.sock.send(CRLF)
650 except socket.error, val:
651 raise self.abort('socket error: %s' % val)
652
653 if not literator:
654 break
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000655
656 return tag
657
658
659 def _command_complete(self, name, tag):
Guido van Rossum8c062211999-12-13 23:27:45 +0000660 self._check_bye()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000661 try:
662 typ, data = self._get_tagged_response(tag)
663 except self.abort, val:
664 raise self.abort('command: %s => %s' % (name, val))
665 except self.error, val:
666 raise self.error('command: %s => %s' % (name, val))
Guido van Rossum8c062211999-12-13 23:27:45 +0000667 self._check_bye()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000668 if typ == 'BAD':
669 raise self.error('%s command error: %s %s' % (name, typ, data))
670 return typ, data
671
672
673 def _get_response(self):
674
675 # Read response and store.
676 #
677 # Returns None for continuation responses,
Guido van Rossum46586821998-05-18 14:39:42 +0000678 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000679
Guido van Rossum46586821998-05-18 14:39:42 +0000680 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000681
682 # Command completion response?
683
684 if self._match(self.tagre, resp):
685 tag = self.mo.group('tag')
686 if not self.tagged_commands.has_key(tag):
687 raise self.abort('unexpected tagged response: %s' % resp)
688
689 typ = self.mo.group('type')
690 dat = self.mo.group('data')
691 self.tagged_commands[tag] = (typ, [dat])
692 else:
693 dat2 = None
694
695 # '*' (untagged) responses?
696
697 if not self._match(Untagged_response, resp):
698 if self._match(Untagged_status, resp):
699 dat2 = self.mo.group('data2')
700
701 if self.mo is None:
702 # Only other possibility is '+' (continuation) rsponse...
703
704 if self._match(Continuation, resp):
705 self.continuation_response = self.mo.group('data')
706 return None # NB: indicates continuation
707
Guido van Rossumeda960a1998-06-18 14:24:28 +0000708 raise self.abort("unexpected response: '%s'" % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000709
710 typ = self.mo.group('type')
711 dat = self.mo.group('data')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000712 if dat is None: dat = '' # Null untagged response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000713 if dat2: dat = dat + ' ' + dat2
714
715 # Is there a literal to come?
716
717 while self._match(Literal, dat):
718
719 # Read literal direct from connection.
720
721 size = string.atoi(self.mo.group('size'))
Guido van Rossum8c062211999-12-13 23:27:45 +0000722 if __debug__:
723 if self.debug >= 4:
724 _mesg('read literal size %s' % size)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000725 data = self.file.read(size)
726
727 # Store response with literal as tuple
728
729 self._append_untagged(typ, (dat, data))
730
731 # Read trailer - possibly containing another literal
732
Guido van Rossum46586821998-05-18 14:39:42 +0000733 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000734
735 self._append_untagged(typ, dat)
736
737 # Bracketed response information?
738
739 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
740 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
741
Guido van Rossum8c062211999-12-13 23:27:45 +0000742 if __debug__:
743 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
744 _mesg('%s response: %s' % (typ, dat))
Guido van Rossum26367a01998-09-28 15:34:46 +0000745
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000746 return resp
747
748
749 def _get_tagged_response(self, tag):
750
751 while 1:
752 result = self.tagged_commands[tag]
753 if result is not None:
754 del self.tagged_commands[tag]
755 return result
756 self._get_response()
757
758
759 def _get_line(self):
760
761 line = self.file.readline()
762 if not line:
Guido van Rossum26367a01998-09-28 15:34:46 +0000763 raise self.abort('socket error: EOF')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000764
765 # Protocol mandates all lines terminated by CRLF
766
Guido van Rossum46586821998-05-18 14:39:42 +0000767 line = line[:-2]
Guido van Rossum8c062211999-12-13 23:27:45 +0000768 if __debug__:
769 if self.debug >= 4:
770 _mesg('< %s' % line)
771 else:
772 _log('< %s' % line)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000773 return line
774
775
776 def _match(self, cre, s):
777
778 # Run compiled regular expression match method on 's'.
779 # Save result, return success.
780
781 self.mo = cre.match(s)
Guido van Rossum8c062211999-12-13 23:27:45 +0000782 if __debug__:
783 if self.mo is not None and self.debug >= 5:
784 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000785 return self.mo is not None
786
787
788 def _new_tag(self):
789
790 tag = '%s%s' % (self.tagpre, self.tagnum)
791 self.tagnum = self.tagnum + 1
792 self.tagged_commands[tag] = None
793 return tag
794
795
Guido van Rossum8c062211999-12-13 23:27:45 +0000796 def _checkquote(self, arg):
797
798 # Must quote command args if non-alphanumeric chars present,
799 # and not already quoted.
800
801 if type(arg) is not type(''):
802 return arg
803 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
804 return arg
805 if self.mustquote.search(arg) is None:
806 return arg
807 return self._quote(arg)
808
809
810 def _quote(self, arg):
811
812 arg = string.replace(arg, '\\', '\\\\')
813 arg = string.replace(arg, '"', '\\"')
814
815 return '"%s"' % arg
816
817
Guido van Rossum46586821998-05-18 14:39:42 +0000818 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000819
Guido van Rossum46586821998-05-18 14:39:42 +0000820 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000821
822
Guido van Rossum26367a01998-09-28 15:34:46 +0000823 def _untagged_response(self, typ, dat, name):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000824
Guido van Rossum26367a01998-09-28 15:34:46 +0000825 if typ == 'NO':
826 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000827 if not self.untagged_responses.has_key(name):
828 return typ, [None]
829 data = self.untagged_responses[name]
Guido van Rossum8c062211999-12-13 23:27:45 +0000830 if __debug__:
831 if self.debug >= 5:
832 _mesg('untagged_responses[%s] => %s' % (name, data))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000833 del self.untagged_responses[name]
834 return typ, data
835
836
837
Guido van Rossumeda960a1998-06-18 14:24:28 +0000838class _Authenticator:
839
840 """Private class to provide en/decoding
841 for base64-based authentication conversation.
842 """
843
844 def __init__(self, mechinst):
845 self.mech = mechinst # Callable object to provide/process data
846
847 def process(self, data):
848 ret = self.mech(self.decode(data))
849 if ret is None:
850 return '*' # Abort conversation
851 return self.encode(ret)
852
853 def encode(self, inp):
854 #
855 # Invoke binascii.b2a_base64 iteratively with
856 # short even length buffers, strip the trailing
857 # line feed from the result and append. "Even"
858 # means a number that factors to both 6 and 8,
859 # so when it gets to the end of the 8-bit input
860 # there's no partial 6-bit output.
861 #
862 oup = ''
863 while inp:
864 if len(inp) > 48:
865 t = inp[:48]
866 inp = inp[48:]
867 else:
868 t = inp
869 inp = ''
870 e = binascii.b2a_base64(t)
871 if e:
872 oup = oup + e[:-1]
873 return oup
874
875 def decode(self, inp):
876 if not inp:
877 return ''
878 return binascii.a2b_base64(inp)
879
880
881
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000882Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
883 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
884
885def Internaldate2tuple(resp):
886
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000887 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000888
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000889 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000890 """
891
892 mo = InternalDate.match(resp)
893 if not mo:
894 return None
895
896 mon = Mon2num[mo.group('mon')]
897 zonen = mo.group('zonen')
898
899 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
900 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
901
902 # INTERNALDATE timezone must be subtracted to get UT
903
904 zone = (zoneh*60 + zonem)*60
905 if zonen == '-':
906 zone = -zone
907
908 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
909
910 utc = time.mktime(tt)
911
912 # Following is necessary because the time module has no 'mkgmtime'.
913 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
914
915 lt = time.localtime(utc)
916 if time.daylight and lt[-1]:
917 zone = zone + time.altzone
918 else:
919 zone = zone + time.timezone
920
921 return time.localtime(utc - zone)
922
923
924
925def Int2AP(num):
926
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000927 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000928
929 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000930 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000931 while num:
932 num, mod = divmod(num, 16)
933 val = AP[mod] + val
934 return val
935
936
937
938def ParseFlags(resp):
939
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000940 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000941
942 mo = Flags.match(resp)
943 if not mo:
944 return ()
945
946 return tuple(string.split(mo.group('flags')))
947
948
949def Time2Internaldate(date_time):
950
951 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
952
953 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
954 """
955
956 dttype = type(date_time)
Guido van Rossum8c062211999-12-13 23:27:45 +0000957 if dttype is type(1) or dttype is type(1.1):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000958 tt = time.localtime(date_time)
959 elif dttype is type(()):
960 tt = date_time
961 elif dttype is type(""):
962 return date_time # Assume in correct format
963 else: raise ValueError
964
965 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
966 if dt[0] == '0':
967 dt = ' ' + dt[1:]
968 if time.daylight and tt[-1]:
969 zone = -time.altzone
970 else:
971 zone = -time.timezone
972 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
973
974
975
Guido van Rossumeda960a1998-06-18 14:24:28 +0000976if __debug__:
977
Guido van Rossum8c062211999-12-13 23:27:45 +0000978 def _mesg(s, secs=None):
979 if secs is None:
980 secs = time.time()
981 tm = time.strftime('%M:%S', time.localtime(secs))
982 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
Guido van Rossum26367a01998-09-28 15:34:46 +0000983 sys.stderr.flush()
984
985 def _dump_ur(dict):
986 # Dump untagged responses (in `dict').
987 l = dict.items()
988 if not l: return
989 t = '\n\t\t'
990 j = string.join
991 l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l)
992 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000993
Guido van Rossum8c062211999-12-13 23:27:45 +0000994 _cmd_log = [] # Last `_cmd_log_len' interactions
995 _cmd_log_len = 10
996
997 def _log(line):
998 # Keep log of last `_cmd_log_len' interactions for debugging.
999 if len(_cmd_log) == _cmd_log_len:
1000 del _cmd_log[0]
1001 _cmd_log.append((time.time(), line))
1002
1003 def print_log():
1004 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1005 for secs,line in _cmd_log:
1006 _mesg(line, secs)
Guido van Rossumeda960a1998-06-18 14:24:28 +00001007
1008
Guido van Rossum8c062211999-12-13 23:27:45 +00001009
1010if __name__ == '__main__':
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001011
Guido van Rossumb1f08121998-06-25 02:22:16 +00001012 import getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +00001013
Guido van Rossumb1f08121998-06-25 02:22:16 +00001014 host = ''
1015 if sys.argv[1:]: host = sys.argv[1]
1016
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001017 USER = getpass.getuser()
Guido van Rossumb1f08121998-06-25 02:22:16 +00001018 PASSWD = getpass.getpass("IMAP password for %s: " % (host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001019
1020 test_seq1 = (
1021 ('login', (USER, PASSWD)),
Guido van Rossum46586821998-05-18 14:39:42 +00001022 ('create', ('/tmp/xxx 1',)),
1023 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1024 ('CREATE', ('/tmp/yyz 2',)),
1025 ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
Guido van Rossum8c062211999-12-13 23:27:45 +00001026 ('list', ('/tmp', 'yy*')),
Guido van Rossum46586821998-05-18 14:39:42 +00001027 ('select', ('/tmp/yyz 2',)),
Guido van Rossumeda960a1998-06-18 14:24:28 +00001028 ('search', (None, '(TO zork)')),
1029 ('partial', ('1', 'RFC822', 1, 1024)),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001030 ('store', ('1', 'FLAGS', '(\Deleted)')),
1031 ('expunge', ()),
Guido van Rossum46586821998-05-18 14:39:42 +00001032 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001033 ('close', ()),
1034 )
1035
1036 test_seq2 = (
1037 ('select', ()),
1038 ('response',('UIDVALIDITY',)),
1039 ('uid', ('SEARCH', 'ALL')),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001040 ('response', ('EXISTS',)),
Guido van Rossum8c062211999-12-13 23:27:45 +00001041 ('append', (None, None, None, 'From: anon@x.y.z\n\ndata...')),
Guido van Rossum46586821998-05-18 14:39:42 +00001042 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001043 ('logout', ()),
1044 )
1045
1046 def run(cmd, args):
Guido van Rossum8c062211999-12-13 23:27:45 +00001047 _mesg('%s %s' % (cmd, args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001048 typ, dat = apply(eval('M.%s' % cmd), args)
Guido van Rossum8c062211999-12-13 23:27:45 +00001049 _mesg('%s => %s %s' % (cmd, typ, dat))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001050 return dat
1051
Guido van Rossumeda960a1998-06-18 14:24:28 +00001052 Debug = 5
Guido van Rossumd6596931998-05-29 18:08:48 +00001053 M = IMAP4(host)
Guido van Rossum26367a01998-09-28 15:34:46 +00001054 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001055
1056 for cmd,args in test_seq1:
1057 run(cmd, args)
1058
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001059 for ml in run('list', ('/tmp/', 'yy%')):
Guido van Rossum46586821998-05-18 14:39:42 +00001060 mo = re.match(r'.*"([^"]+)"$', ml)
1061 if mo: path = mo.group(1)
1062 else: path = string.split(ml)[-1]
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001063 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001064
1065 for cmd,args in test_seq2:
1066 dat = run(cmd, args)
1067
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001068 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1069 continue
1070
Guido van Rossum8c062211999-12-13 23:27:45 +00001071 uid = string.split(dat[-1])
1072 if not uid: continue
1073 run('uid', ('FETCH', '%s' % uid[-1],
Guido van Rossumeda960a1998-06-18 14:24:28 +00001074 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))