blob: c65cc903322be75850aab7dbe4eaff99c0fedc99 [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 Rossumf36b1822000-02-17 17:12:39 +000018__version__ = "2.30"
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'"')
Guido van Rossumf36b1822000-02-17 17:12:39 +000069Literal = re.compile(r'.*{(?P<size>\d+)}$')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000070Response_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 Rossumf36b1822000-02-17 17:12:39 +0000120 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
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:
Guido van Rossumf36b1822000-02-17 17:12:39 +0000702 # Only other possibility is '+' (continuation) response...
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000703
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
Guido van Rossumf36b1822000-02-17 17:12:39 +0000756
757 # Some have reported "unexpected response" exceptions.
758 # (Isn't this non-IMAP4-compliant behaviour?
759 # Please mail me details printed below!)
760 # Anyway, ignore them here.
761
762 try:
763 self._get_response()
764 except self.abort, val:
765 if __debug__:
766 if self.debug >= 1:
767 _mesg('abort exception ignored: %s' % val)
768 print_log()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000769
770
771 def _get_line(self):
772
773 line = self.file.readline()
774 if not line:
Guido van Rossum26367a01998-09-28 15:34:46 +0000775 raise self.abort('socket error: EOF')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000776
777 # Protocol mandates all lines terminated by CRLF
778
Guido van Rossum46586821998-05-18 14:39:42 +0000779 line = line[:-2]
Guido van Rossum8c062211999-12-13 23:27:45 +0000780 if __debug__:
781 if self.debug >= 4:
782 _mesg('< %s' % line)
783 else:
784 _log('< %s' % line)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000785 return line
786
787
788 def _match(self, cre, s):
789
790 # Run compiled regular expression match method on 's'.
791 # Save result, return success.
792
793 self.mo = cre.match(s)
Guido van Rossum8c062211999-12-13 23:27:45 +0000794 if __debug__:
795 if self.mo is not None and self.debug >= 5:
796 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000797 return self.mo is not None
798
799
800 def _new_tag(self):
801
802 tag = '%s%s' % (self.tagpre, self.tagnum)
803 self.tagnum = self.tagnum + 1
804 self.tagged_commands[tag] = None
805 return tag
806
807
Guido van Rossum8c062211999-12-13 23:27:45 +0000808 def _checkquote(self, arg):
809
810 # Must quote command args if non-alphanumeric chars present,
811 # and not already quoted.
812
813 if type(arg) is not type(''):
814 return arg
815 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
816 return arg
817 if self.mustquote.search(arg) is None:
818 return arg
819 return self._quote(arg)
820
821
822 def _quote(self, arg):
823
824 arg = string.replace(arg, '\\', '\\\\')
825 arg = string.replace(arg, '"', '\\"')
826
827 return '"%s"' % arg
828
829
Guido van Rossum46586821998-05-18 14:39:42 +0000830 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000831
Guido van Rossum46586821998-05-18 14:39:42 +0000832 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000833
834
Guido van Rossum26367a01998-09-28 15:34:46 +0000835 def _untagged_response(self, typ, dat, name):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000836
Guido van Rossum26367a01998-09-28 15:34:46 +0000837 if typ == 'NO':
838 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000839 if not self.untagged_responses.has_key(name):
840 return typ, [None]
841 data = self.untagged_responses[name]
Guido van Rossum8c062211999-12-13 23:27:45 +0000842 if __debug__:
843 if self.debug >= 5:
844 _mesg('untagged_responses[%s] => %s' % (name, data))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000845 del self.untagged_responses[name]
846 return typ, data
847
848
849
Guido van Rossumeda960a1998-06-18 14:24:28 +0000850class _Authenticator:
851
852 """Private class to provide en/decoding
853 for base64-based authentication conversation.
854 """
855
856 def __init__(self, mechinst):
857 self.mech = mechinst # Callable object to provide/process data
858
859 def process(self, data):
860 ret = self.mech(self.decode(data))
861 if ret is None:
862 return '*' # Abort conversation
863 return self.encode(ret)
864
865 def encode(self, inp):
866 #
867 # Invoke binascii.b2a_base64 iteratively with
868 # short even length buffers, strip the trailing
869 # line feed from the result and append. "Even"
870 # means a number that factors to both 6 and 8,
871 # so when it gets to the end of the 8-bit input
872 # there's no partial 6-bit output.
873 #
874 oup = ''
875 while inp:
876 if len(inp) > 48:
877 t = inp[:48]
878 inp = inp[48:]
879 else:
880 t = inp
881 inp = ''
882 e = binascii.b2a_base64(t)
883 if e:
884 oup = oup + e[:-1]
885 return oup
886
887 def decode(self, inp):
888 if not inp:
889 return ''
890 return binascii.a2b_base64(inp)
891
892
893
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000894Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
895 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
896
897def Internaldate2tuple(resp):
898
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000899 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000900
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000901 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000902 """
903
904 mo = InternalDate.match(resp)
905 if not mo:
906 return None
907
908 mon = Mon2num[mo.group('mon')]
909 zonen = mo.group('zonen')
910
911 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
912 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
913
914 # INTERNALDATE timezone must be subtracted to get UT
915
916 zone = (zoneh*60 + zonem)*60
917 if zonen == '-':
918 zone = -zone
919
920 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
921
922 utc = time.mktime(tt)
923
924 # Following is necessary because the time module has no 'mkgmtime'.
925 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
926
927 lt = time.localtime(utc)
928 if time.daylight and lt[-1]:
929 zone = zone + time.altzone
930 else:
931 zone = zone + time.timezone
932
933 return time.localtime(utc - zone)
934
935
936
937def Int2AP(num):
938
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000939 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000940
941 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000942 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000943 while num:
944 num, mod = divmod(num, 16)
945 val = AP[mod] + val
946 return val
947
948
949
950def ParseFlags(resp):
951
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000952 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000953
954 mo = Flags.match(resp)
955 if not mo:
956 return ()
957
958 return tuple(string.split(mo.group('flags')))
959
960
961def Time2Internaldate(date_time):
962
963 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
964
965 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
966 """
967
968 dttype = type(date_time)
Guido van Rossum8c062211999-12-13 23:27:45 +0000969 if dttype is type(1) or dttype is type(1.1):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000970 tt = time.localtime(date_time)
971 elif dttype is type(()):
972 tt = date_time
973 elif dttype is type(""):
974 return date_time # Assume in correct format
975 else: raise ValueError
976
977 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
978 if dt[0] == '0':
979 dt = ' ' + dt[1:]
980 if time.daylight and tt[-1]:
981 zone = -time.altzone
982 else:
983 zone = -time.timezone
984 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
985
986
987
Guido van Rossumeda960a1998-06-18 14:24:28 +0000988if __debug__:
989
Guido van Rossum8c062211999-12-13 23:27:45 +0000990 def _mesg(s, secs=None):
991 if secs is None:
992 secs = time.time()
993 tm = time.strftime('%M:%S', time.localtime(secs))
994 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
Guido van Rossum26367a01998-09-28 15:34:46 +0000995 sys.stderr.flush()
996
997 def _dump_ur(dict):
998 # Dump untagged responses (in `dict').
999 l = dict.items()
1000 if not l: return
1001 t = '\n\t\t'
1002 j = string.join
1003 l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l)
1004 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
Guido van Rossumeda960a1998-06-18 14:24:28 +00001005
Guido van Rossum8c062211999-12-13 23:27:45 +00001006 _cmd_log = [] # Last `_cmd_log_len' interactions
1007 _cmd_log_len = 10
1008
1009 def _log(line):
1010 # Keep log of last `_cmd_log_len' interactions for debugging.
1011 if len(_cmd_log) == _cmd_log_len:
1012 del _cmd_log[0]
1013 _cmd_log.append((time.time(), line))
1014
1015 def print_log():
1016 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1017 for secs,line in _cmd_log:
1018 _mesg(line, secs)
Guido van Rossumeda960a1998-06-18 14:24:28 +00001019
1020
Guido van Rossum8c062211999-12-13 23:27:45 +00001021
1022if __name__ == '__main__':
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001023
Guido van Rossumb1f08121998-06-25 02:22:16 +00001024 import getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +00001025
Guido van Rossumb1f08121998-06-25 02:22:16 +00001026 host = ''
1027 if sys.argv[1:]: host = sys.argv[1]
1028
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001029 USER = getpass.getuser()
Guido van Rossumf36b1822000-02-17 17:12:39 +00001030 PASSWD = getpass.getpass("IMAP password for %s on %s" % (USER, host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001031
Guido van Rossumf36b1822000-02-17 17:12:39 +00001032 test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001033 test_seq1 = (
1034 ('login', (USER, PASSWD)),
Guido van Rossum46586821998-05-18 14:39:42 +00001035 ('create', ('/tmp/xxx 1',)),
1036 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1037 ('CREATE', ('/tmp/yyz 2',)),
Guido van Rossumf36b1822000-02-17 17:12:39 +00001038 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
Guido van Rossum8c062211999-12-13 23:27:45 +00001039 ('list', ('/tmp', 'yy*')),
Guido van Rossum46586821998-05-18 14:39:42 +00001040 ('select', ('/tmp/yyz 2',)),
Guido van Rossumeda960a1998-06-18 14:24:28 +00001041 ('search', (None, '(TO zork)')),
1042 ('partial', ('1', 'RFC822', 1, 1024)),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001043 ('store', ('1', 'FLAGS', '(\Deleted)')),
1044 ('expunge', ()),
Guido van Rossum46586821998-05-18 14:39:42 +00001045 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001046 ('close', ()),
1047 )
1048
1049 test_seq2 = (
1050 ('select', ()),
1051 ('response',('UIDVALIDITY',)),
1052 ('uid', ('SEARCH', 'ALL')),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001053 ('response', ('EXISTS',)),
Guido van Rossumf36b1822000-02-17 17:12:39 +00001054 ('append', (None, None, None, test_mesg)),
Guido van Rossum46586821998-05-18 14:39:42 +00001055 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001056 ('logout', ()),
1057 )
1058
1059 def run(cmd, args):
Guido van Rossum8c062211999-12-13 23:27:45 +00001060 _mesg('%s %s' % (cmd, args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001061 typ, dat = apply(eval('M.%s' % cmd), args)
Guido van Rossum8c062211999-12-13 23:27:45 +00001062 _mesg('%s => %s %s' % (cmd, typ, dat))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001063 return dat
1064
Guido van Rossumeda960a1998-06-18 14:24:28 +00001065 Debug = 5
Guido van Rossumd6596931998-05-29 18:08:48 +00001066 M = IMAP4(host)
Guido van Rossum26367a01998-09-28 15:34:46 +00001067 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001068
1069 for cmd,args in test_seq1:
1070 run(cmd, args)
1071
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001072 for ml in run('list', ('/tmp/', 'yy%')):
Guido van Rossum46586821998-05-18 14:39:42 +00001073 mo = re.match(r'.*"([^"]+)"$', ml)
1074 if mo: path = mo.group(1)
1075 else: path = string.split(ml)[-1]
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001076 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001077
1078 for cmd,args in test_seq2:
1079 dat = run(cmd, args)
1080
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001081 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1082 continue
1083
Guido van Rossum8c062211999-12-13 23:27:45 +00001084 uid = string.split(dat[-1])
1085 if not uid: continue
1086 run('uid', ('FETCH', '%s' % uid[-1],
Guido van Rossumeda960a1998-06-18 14:24:28 +00001087 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))