blob: 921ee0cbecddf00248aa0bfc9fc2c555542f09f4 [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
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00006Public class: IMAP4
7Public variable: Debug
8Public functions: Internaldate2tuple
9 Int2AP
10 ParseFlags
11 Time2Internaldate
12"""
Guido van Rossumb1f08121998-06-25 02:22:16 +000013
Guido van Rossum98d9fd32000-02-28 15:12:25 +000014# Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
15#
16# Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
17
Guido van Rossum66d45132000-03-28 20:20:53 +000018__version__ = "2.36"
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
Guido van Rossum619c3372000-02-28 22:37:30 +0000131 self.is_readonly = None # READ-ONLY desired state
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000132 self.tagnum = 0
133
134 # Open socket to server.
135
Guido van Rossumeda960a1998-06-18 14:24:28 +0000136 self.open(host, port)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000137
138 # Create unique tag for this session,
139 # and compile tagged response matcher.
140
Guido van Rossum6884af71998-05-29 13:34:03 +0000141 self.tagpre = Int2AP(random.randint(0, 31999))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000142 self.tagre = re.compile(r'(?P<tag>'
143 + self.tagpre
144 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
145
146 # Get server welcome message,
147 # request and store CAPABILITY response.
148
Guido van Rossum8c062211999-12-13 23:27:45 +0000149 if __debug__:
150 if self.debug >= 1:
151 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000152
153 self.welcome = self._get_response()
154 if self.untagged_responses.has_key('PREAUTH'):
155 self.state = 'AUTH'
156 elif self.untagged_responses.has_key('OK'):
157 self.state = 'NONAUTH'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000158 else:
159 raise self.error(self.welcome)
160
161 cap = 'CAPABILITY'
162 self._simple_command(cap)
163 if not self.untagged_responses.has_key(cap):
164 raise self.error('no CAPABILITY response from server')
Guido van Rossum04da10c1998-10-21 22:06:56 +0000165 self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1])))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000166
Guido van Rossum8c062211999-12-13 23:27:45 +0000167 if __debug__:
168 if self.debug >= 3:
169 _mesg('CAPABILITIES: %s' % `self.capabilities`)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000170
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000171 for version in AllowedVersions:
172 if not version in self.capabilities:
173 continue
174 self.PROTOCOL_VERSION = version
Guido van Rossumb1f08121998-06-25 02:22:16 +0000175 return
176
177 raise self.error('server not IMAP4 compliant')
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000178
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000179
Guido van Rossum26367a01998-09-28 15:34:46 +0000180 def __getattr__(self, attr):
181 # Allow UPPERCASE variants of IMAP4 command methods.
182 if Commands.has_key(attr):
183 return eval("self.%s" % string.lower(attr))
184 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
185
186
187
188 # Public methods
189
190
Guido van Rossumeda960a1998-06-18 14:24:28 +0000191 def open(self, host, port):
192 """Setup 'self.sock' and 'self.file'."""
193 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Guido van Rossum93a7c0f2000-03-28 21:45:46 +0000194 self.sock.connect((self.host, self.port))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000195 self.file = self.sock.makefile('r')
196
197
Guido van Rossum26367a01998-09-28 15:34:46 +0000198 def recent(self):
199 """Return most recent 'RECENT' responses if any exist,
200 else prompt server for an update using the 'NOOP' command.
201
202 (typ, [data]) = <instance>.recent()
203
204 'data' is None if no new messages,
205 else list of RECENT responses, most recent last.
206 """
207 name = 'RECENT'
208 typ, dat = self._untagged_response('OK', [None], name)
209 if dat[-1]:
210 return typ, dat
211 typ, dat = self.noop() # Prod server for response
212 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000213
214
Guido van Rossum26367a01998-09-28 15:34:46 +0000215 def response(self, code):
216 """Return data for response 'code' if received, or None.
217
218 Old value for response 'code' is cleared.
219
220 (code, [data]) = <instance>.response(code)
221 """
222 return self._untagged_response(code, [None], string.upper(code))
223
224
225 def socket(self):
226 """Return socket instance used to connect to IMAP4 server.
227
228 socket = <instance>.socket()
229 """
230 return self.sock
231
232
233
234 # IMAP4 commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000235
236
237 def append(self, mailbox, flags, date_time, message):
238 """Append message to named mailbox.
239
240 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
Guido van Rossum8c062211999-12-13 23:27:45 +0000241
242 All args except `message' can be None.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000243 """
244 name = 'APPEND'
Guido van Rossum8c062211999-12-13 23:27:45 +0000245 if not mailbox:
246 mailbox = 'INBOX'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000247 if flags:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000248 if (flags[0],flags[-1]) != ('(',')'):
249 flags = '(%s)' % flags
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000250 else:
251 flags = None
252 if date_time:
253 date_time = Time2Internaldate(date_time)
254 else:
255 date_time = None
Guido van Rossum6884af71998-05-29 13:34:03 +0000256 self.literal = message
257 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000258
259
Guido van Rossumeda960a1998-06-18 14:24:28 +0000260 def authenticate(self, mechanism, authobject):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000261 """Authenticate command - requires response processing.
262
Guido van Rossumeda960a1998-06-18 14:24:28 +0000263 'mechanism' specifies which authentication mechanism is to
264 be used - it must appear in <instance>.capabilities in the
265 form AUTH=<mechanism>.
266
267 'authobject' must be a callable object:
268
269 data = authobject(response)
270
271 It will be called to process server continuation responses.
272 It should return data that will be encoded and sent to server.
273 It should return None if the client abort response '*' should
274 be sent instead.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000275 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000276 mech = string.upper(mechanism)
277 cap = 'AUTH=%s' % mech
278 if not cap in self.capabilities:
279 raise self.error("Server doesn't allow %s authentication." % mech)
280 self.literal = _Authenticator(authobject).process
281 typ, dat = self._simple_command('AUTHENTICATE', mech)
282 if typ != 'OK':
Guido van Rossum26367a01998-09-28 15:34:46 +0000283 raise self.error(dat[-1])
Guido van Rossumeda960a1998-06-18 14:24:28 +0000284 self.state = 'AUTH'
285 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000286
287
288 def check(self):
289 """Checkpoint mailbox on server.
290
291 (typ, [data]) = <instance>.check()
292 """
293 return self._simple_command('CHECK')
294
295
296 def close(self):
297 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000298
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000299 Deleted messages are removed from writable mailbox.
300 This is the recommended command before 'LOGOUT'.
301
302 (typ, [data]) = <instance>.close()
303 """
304 try:
Guido van Rossum26367a01998-09-28 15:34:46 +0000305 typ, dat = self._simple_command('CLOSE')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000306 finally:
307 self.state = 'AUTH'
308 return typ, dat
309
310
311 def copy(self, message_set, new_mailbox):
312 """Copy 'message_set' messages onto end of 'new_mailbox'.
313
314 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
315 """
316 return self._simple_command('COPY', message_set, new_mailbox)
317
318
319 def create(self, mailbox):
320 """Create new mailbox.
321
322 (typ, [data]) = <instance>.create(mailbox)
323 """
324 return self._simple_command('CREATE', mailbox)
325
326
327 def delete(self, mailbox):
328 """Delete old mailbox.
329
330 (typ, [data]) = <instance>.delete(mailbox)
331 """
332 return self._simple_command('DELETE', mailbox)
333
334
335 def expunge(self):
336 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000337
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000338 Generates 'EXPUNGE' response for each deleted message.
339
340 (typ, [data]) = <instance>.expunge()
341
342 'data' is list of 'EXPUNGE'd message numbers in order received.
343 """
344 name = 'EXPUNGE'
345 typ, dat = self._simple_command(name)
Guido van Rossum26367a01998-09-28 15:34:46 +0000346 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000347
348
349 def fetch(self, message_set, message_parts):
350 """Fetch (parts of) messages.
351
352 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
353
354 'data' are tuples of message part envelope and data.
355 """
356 name = 'FETCH'
357 typ, dat = self._simple_command(name, message_set, message_parts)
Guido van Rossum26367a01998-09-28 15:34:46 +0000358 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000359
360
361 def list(self, directory='""', pattern='*'):
362 """List mailbox names in directory matching pattern.
363
364 (typ, [data]) = <instance>.list(directory='""', pattern='*')
365
366 'data' is list of LIST responses.
367 """
368 name = 'LIST'
369 typ, dat = self._simple_command(name, directory, pattern)
Guido van Rossum26367a01998-09-28 15:34:46 +0000370 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000371
372
373 def login(self, user, password):
374 """Identify client using plaintext password.
375
Guido van Rossum8c062211999-12-13 23:27:45 +0000376 (typ, [data]) = <instance>.login(user, password)
377
378 NB: 'password' will be quoted.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000379 """
Guido van Rossum8c062211999-12-13 23:27:45 +0000380 #if not 'AUTH=LOGIN' in self.capabilities:
381 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
382 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000383 if typ != 'OK':
Guido van Rossum26367a01998-09-28 15:34:46 +0000384 raise self.error(dat[-1])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000385 self.state = 'AUTH'
386 return typ, dat
387
388
389 def logout(self):
390 """Shutdown connection to server.
391
392 (typ, [data]) = <instance>.logout()
393
394 Returns server 'BYE' response.
395 """
396 self.state = 'LOGOUT'
397 try: typ, dat = self._simple_command('LOGOUT')
Guido van Rossum26367a01998-09-28 15:34:46 +0000398 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000399 self.file.close()
400 self.sock.close()
401 if self.untagged_responses.has_key('BYE'):
402 return 'BYE', self.untagged_responses['BYE']
403 return typ, dat
404
405
406 def lsub(self, directory='""', pattern='*'):
407 """List 'subscribed' mailbox names in directory matching pattern.
408
409 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
410
411 'data' are tuples of message part envelope and data.
412 """
413 name = 'LSUB'
414 typ, dat = self._simple_command(name, directory, pattern)
Guido van Rossum26367a01998-09-28 15:34:46 +0000415 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000416
417
Guido van Rossum6884af71998-05-29 13:34:03 +0000418 def noop(self):
419 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000420
Guido van Rossum6884af71998-05-29 13:34:03 +0000421 (typ, data) = <instance>.noop()
422 """
Guido van Rossum8c062211999-12-13 23:27:45 +0000423 if __debug__:
424 if self.debug >= 3:
425 _dump_ur(self.untagged_responses)
Guido van Rossum6884af71998-05-29 13:34:03 +0000426 return self._simple_command('NOOP')
427
428
Guido van Rossumeda960a1998-06-18 14:24:28 +0000429 def partial(self, message_num, message_part, start, length):
430 """Fetch truncated part of a message.
431
432 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
433
434 'data' is tuple of message part envelope and data.
435 """
436 name = 'PARTIAL'
437 typ, dat = self._simple_command(name, message_num, message_part, start, length)
Guido van Rossum26367a01998-09-28 15:34:46 +0000438 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000439
440
441 def rename(self, oldmailbox, newmailbox):
442 """Rename old mailbox name to new.
443
444 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
445 """
446 return self._simple_command('RENAME', oldmailbox, newmailbox)
447
448
Guido van Rossum66d45132000-03-28 20:20:53 +0000449 def search(self, charset, *criteria):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000450 """Search mailbox for matching messages.
451
Guido van Rossum66d45132000-03-28 20:20:53 +0000452 (typ, [data]) = <instance>.search(charset, criterium, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000453
454 'data' is space separated list of matching message numbers.
455 """
456 name = 'SEARCH'
457 if charset:
458 charset = 'CHARSET ' + charset
Guido van Rossum66d45132000-03-28 20:20:53 +0000459 typ, dat = apply(self._simple_command, (name, charset) + criteria)
Guido van Rossum26367a01998-09-28 15:34:46 +0000460 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000461
462
463 def select(self, mailbox='INBOX', readonly=None):
464 """Select a mailbox.
465
Guido van Rossum46586821998-05-18 14:39:42 +0000466 Flush all untagged responses.
467
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000468 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
469
470 'data' is count of messages in mailbox ('EXISTS' response).
471 """
472 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
Guido van Rossum46586821998-05-18 14:39:42 +0000473 self.untagged_responses = {} # Flush old responses.
Guido van Rossum619c3372000-02-28 22:37:30 +0000474 self.is_readonly = readonly
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000475 if readonly:
476 name = 'EXAMINE'
477 else:
478 name = 'SELECT'
479 typ, dat = self._simple_command(name, mailbox)
Guido van Rossum26367a01998-09-28 15:34:46 +0000480 if typ != 'OK':
481 self.state = 'AUTH' # Might have been 'SELECTED'
482 return typ, dat
483 self.state = 'SELECTED'
Guido van Rossum619c3372000-02-28 22:37:30 +0000484 if self.untagged_responses.has_key('READ-ONLY') \
Guido van Rossum26367a01998-09-28 15:34:46 +0000485 and not readonly:
Guido van Rossum8c062211999-12-13 23:27:45 +0000486 if __debug__:
487 if self.debug >= 1:
488 _dump_ur(self.untagged_responses)
Guido van Rossum26367a01998-09-28 15:34:46 +0000489 raise self.readonly('%s is not writable' % mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000490 return typ, self.untagged_responses.get('EXISTS', [None])
491
492
493 def status(self, mailbox, names):
494 """Request named status conditions for mailbox.
495
496 (typ, [data]) = <instance>.status(mailbox, names)
497 """
498 name = 'STATUS'
Guido van Rossumbe14e691998-04-11 03:11:51 +0000499 if self.PROTOCOL_VERSION == 'IMAP4':
500 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000501 typ, dat = self._simple_command(name, mailbox, names)
Guido van Rossum26367a01998-09-28 15:34:46 +0000502 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000503
504
505 def store(self, message_set, command, flag_list):
506 """Alters flag dispositions for messages in mailbox.
507
508 (typ, [data]) = <instance>.store(message_set, command, flag_list)
509 """
Guido van Rossum46586821998-05-18 14:39:42 +0000510 typ, dat = self._simple_command('STORE', message_set, command, flag_list)
Guido van Rossum26367a01998-09-28 15:34:46 +0000511 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000512
513
514 def subscribe(self, mailbox):
515 """Subscribe to new mailbox.
516
517 (typ, [data]) = <instance>.subscribe(mailbox)
518 """
519 return self._simple_command('SUBSCRIBE', mailbox)
520
521
Guido van Rossum46586821998-05-18 14:39:42 +0000522 def uid(self, command, *args):
523 """Execute "command arg ..." with messages identified by UID,
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000524 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000525
Guido van Rossum46586821998-05-18 14:39:42 +0000526 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000527
528 Returns response appropriate to 'command'.
529 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000530 command = string.upper(command)
531 if not Commands.has_key(command):
532 raise self.error("Unknown IMAP4 UID command: %s" % command)
533 if self.state not in Commands[command]:
534 raise self.error('command %s illegal in state %s'
535 % (command, self.state))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000536 name = 'UID'
Guido van Rossumeda960a1998-06-18 14:24:28 +0000537 typ, dat = apply(self._simple_command, (name, command) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000538 if command == 'SEARCH':
539 name = 'SEARCH'
540 else:
541 name = 'FETCH'
Guido van Rossum26367a01998-09-28 15:34:46 +0000542 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000543
544
545 def unsubscribe(self, mailbox):
546 """Unsubscribe from old mailbox.
547
548 (typ, [data]) = <instance>.unsubscribe(mailbox)
549 """
550 return self._simple_command('UNSUBSCRIBE', mailbox)
551
552
Guido van Rossum46586821998-05-18 14:39:42 +0000553 def xatom(self, name, *args):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000554 """Allow simple extension commands
555 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000556
Guido van Rossum46586821998-05-18 14:39:42 +0000557 (typ, [data]) = <instance>.xatom(name, arg, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000558 """
559 if name[0] != 'X' or not name in self.capabilities:
560 raise self.error('unknown extension command: %s' % name)
Guido van Rossum46586821998-05-18 14:39:42 +0000561 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000562
563
564
565 # Private methods
566
567
568 def _append_untagged(self, typ, dat):
569
Guido van Rossum8c062211999-12-13 23:27:45 +0000570 if dat is None: dat = ''
Guido van Rossumeda960a1998-06-18 14:24:28 +0000571 ur = self.untagged_responses
Guido van Rossum8c062211999-12-13 23:27:45 +0000572 if __debug__:
573 if self.debug >= 5:
574 _mesg('untagged_responses[%s] %s += ["%s"]' %
575 (typ, len(ur.get(typ,'')), dat))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000576 if ur.has_key(typ):
577 ur[typ].append(dat)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000578 else:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000579 ur[typ] = [dat]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000580
581
Guido van Rossum8c062211999-12-13 23:27:45 +0000582 def _check_bye(self):
583 bye = self.untagged_responses.get('BYE')
584 if bye:
585 raise self.abort(bye[-1])
586
587
Guido van Rossum6884af71998-05-29 13:34:03 +0000588 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000589
590 if self.state not in Commands[name]:
Guido van Rossum6884af71998-05-29 13:34:03 +0000591 self.literal = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000592 raise self.error(
593 'command %s illegal in state %s' % (name, self.state))
594
Guido van Rossum26367a01998-09-28 15:34:46 +0000595 for typ in ('OK', 'NO', 'BAD'):
596 if self.untagged_responses.has_key(typ):
597 del self.untagged_responses[typ]
598
Guido van Rossum619c3372000-02-28 22:37:30 +0000599 if self.untagged_responses.has_key('READ-ONLY') \
600 and not self.is_readonly:
Guido van Rossum26367a01998-09-28 15:34:46 +0000601 raise self.readonly('mailbox status changed to READ-ONLY')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000602
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000603 tag = self._new_tag()
604 data = '%s %s' % (tag, name)
Guido van Rossum8c062211999-12-13 23:27:45 +0000605 for arg in args:
606 if arg is None: continue
607 data = '%s %s' % (data, self._checkquote(arg))
Guido van Rossum6884af71998-05-29 13:34:03 +0000608
609 literal = self.literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000610 if literal is not None:
Guido van Rossum6884af71998-05-29 13:34:03 +0000611 self.literal = None
Guido van Rossumeda960a1998-06-18 14:24:28 +0000612 if type(literal) is type(self._command):
613 literator = literal
614 else:
615 literator = None
616 data = '%s {%s}' % (data, len(literal))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000617
Guido van Rossum8c062211999-12-13 23:27:45 +0000618 if __debug__:
619 if self.debug >= 4:
620 _mesg('> %s' % data)
621 else:
622 _log('> %s' % data)
623
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000624 try:
625 self.sock.send('%s%s' % (data, CRLF))
626 except socket.error, val:
627 raise self.abort('socket error: %s' % val)
628
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000629 if literal is None:
630 return tag
631
Guido van Rossumeda960a1998-06-18 14:24:28 +0000632 while 1:
633 # Wait for continuation response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000634
Guido van Rossumeda960a1998-06-18 14:24:28 +0000635 while self._get_response():
636 if self.tagged_commands[tag]: # BAD/NO?
637 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000638
Guido van Rossumeda960a1998-06-18 14:24:28 +0000639 # Send literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000640
Guido van Rossumeda960a1998-06-18 14:24:28 +0000641 if literator:
642 literal = literator(self.continuation_response)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000643
Guido van Rossum8c062211999-12-13 23:27:45 +0000644 if __debug__:
645 if self.debug >= 4:
646 _mesg('write literal size %s' % len(literal))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000647
648 try:
649 self.sock.send(literal)
650 self.sock.send(CRLF)
651 except socket.error, val:
652 raise self.abort('socket error: %s' % val)
653
654 if not literator:
655 break
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000656
657 return tag
658
659
660 def _command_complete(self, name, tag):
Guido van Rossum8c062211999-12-13 23:27:45 +0000661 self._check_bye()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000662 try:
663 typ, data = self._get_tagged_response(tag)
664 except self.abort, val:
665 raise self.abort('command: %s => %s' % (name, val))
666 except self.error, val:
667 raise self.error('command: %s => %s' % (name, val))
Guido van Rossum8c062211999-12-13 23:27:45 +0000668 self._check_bye()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000669 if typ == 'BAD':
670 raise self.error('%s command error: %s %s' % (name, typ, data))
671 return typ, data
672
673
674 def _get_response(self):
675
676 # Read response and store.
677 #
678 # Returns None for continuation responses,
Guido van Rossum46586821998-05-18 14:39:42 +0000679 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000680
Guido van Rossum46586821998-05-18 14:39:42 +0000681 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000682
683 # Command completion response?
684
685 if self._match(self.tagre, resp):
686 tag = self.mo.group('tag')
687 if not self.tagged_commands.has_key(tag):
688 raise self.abort('unexpected tagged response: %s' % resp)
689
690 typ = self.mo.group('type')
691 dat = self.mo.group('data')
692 self.tagged_commands[tag] = (typ, [dat])
693 else:
694 dat2 = None
695
696 # '*' (untagged) responses?
697
698 if not self._match(Untagged_response, resp):
699 if self._match(Untagged_status, resp):
700 dat2 = self.mo.group('data2')
701
702 if self.mo is None:
Guido van Rossumf36b1822000-02-17 17:12:39 +0000703 # Only other possibility is '+' (continuation) response...
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000704
705 if self._match(Continuation, resp):
706 self.continuation_response = self.mo.group('data')
707 return None # NB: indicates continuation
708
Guido van Rossumeda960a1998-06-18 14:24:28 +0000709 raise self.abort("unexpected response: '%s'" % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000710
711 typ = self.mo.group('type')
712 dat = self.mo.group('data')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000713 if dat is None: dat = '' # Null untagged response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000714 if dat2: dat = dat + ' ' + dat2
715
716 # Is there a literal to come?
717
718 while self._match(Literal, dat):
719
720 # Read literal direct from connection.
721
722 size = string.atoi(self.mo.group('size'))
Guido van Rossum8c062211999-12-13 23:27:45 +0000723 if __debug__:
724 if self.debug >= 4:
725 _mesg('read literal size %s' % size)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000726 data = self.file.read(size)
727
728 # Store response with literal as tuple
729
730 self._append_untagged(typ, (dat, data))
731
732 # Read trailer - possibly containing another literal
733
Guido van Rossum46586821998-05-18 14:39:42 +0000734 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000735
736 self._append_untagged(typ, dat)
737
738 # Bracketed response information?
739
740 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
741 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
742
Guido van Rossum8c062211999-12-13 23:27:45 +0000743 if __debug__:
744 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
745 _mesg('%s response: %s' % (typ, dat))
Guido van Rossum26367a01998-09-28 15:34:46 +0000746
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000747 return resp
748
749
750 def _get_tagged_response(self, tag):
751
752 while 1:
753 result = self.tagged_commands[tag]
754 if result is not None:
755 del self.tagged_commands[tag]
756 return result
Guido van Rossumf36b1822000-02-17 17:12:39 +0000757
758 # Some have reported "unexpected response" exceptions.
Guido van Rossum19ce91b2000-02-24 02:24:50 +0000759 # Note that ignoring them here causes loops.
760 # Instead, send me details of the unexpected response and
761 # I'll update the code in `_get_response()'.
Guido van Rossumf36b1822000-02-17 17:12:39 +0000762
763 try:
764 self._get_response()
765 except self.abort, val:
766 if __debug__:
767 if self.debug >= 1:
Guido van Rossumf36b1822000-02-17 17:12:39 +0000768 print_log()
Guido van Rossum19ce91b2000-02-24 02:24:50 +0000769 raise
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000770
771
772 def _get_line(self):
773
774 line = self.file.readline()
775 if not line:
Guido van Rossum26367a01998-09-28 15:34:46 +0000776 raise self.abort('socket error: EOF')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000777
778 # Protocol mandates all lines terminated by CRLF
779
Guido van Rossum46586821998-05-18 14:39:42 +0000780 line = line[:-2]
Guido van Rossum8c062211999-12-13 23:27:45 +0000781 if __debug__:
782 if self.debug >= 4:
783 _mesg('< %s' % line)
784 else:
785 _log('< %s' % line)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000786 return line
787
788
789 def _match(self, cre, s):
790
791 # Run compiled regular expression match method on 's'.
792 # Save result, return success.
793
794 self.mo = cre.match(s)
Guido van Rossum8c062211999-12-13 23:27:45 +0000795 if __debug__:
796 if self.mo is not None and self.debug >= 5:
797 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000798 return self.mo is not None
799
800
801 def _new_tag(self):
802
803 tag = '%s%s' % (self.tagpre, self.tagnum)
804 self.tagnum = self.tagnum + 1
805 self.tagged_commands[tag] = None
806 return tag
807
808
Guido van Rossum8c062211999-12-13 23:27:45 +0000809 def _checkquote(self, arg):
810
811 # Must quote command args if non-alphanumeric chars present,
812 # and not already quoted.
813
814 if type(arg) is not type(''):
815 return arg
816 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
817 return arg
818 if self.mustquote.search(arg) is None:
819 return arg
820 return self._quote(arg)
821
822
823 def _quote(self, arg):
824
825 arg = string.replace(arg, '\\', '\\\\')
826 arg = string.replace(arg, '"', '\\"')
827
828 return '"%s"' % arg
829
830
Guido van Rossum46586821998-05-18 14:39:42 +0000831 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000832
Guido van Rossum46586821998-05-18 14:39:42 +0000833 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000834
835
Guido van Rossum26367a01998-09-28 15:34:46 +0000836 def _untagged_response(self, typ, dat, name):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000837
Guido van Rossum26367a01998-09-28 15:34:46 +0000838 if typ == 'NO':
839 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000840 if not self.untagged_responses.has_key(name):
841 return typ, [None]
842 data = self.untagged_responses[name]
Guido van Rossum8c062211999-12-13 23:27:45 +0000843 if __debug__:
844 if self.debug >= 5:
845 _mesg('untagged_responses[%s] => %s' % (name, data))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000846 del self.untagged_responses[name]
847 return typ, data
848
849
850
Guido van Rossumeda960a1998-06-18 14:24:28 +0000851class _Authenticator:
852
853 """Private class to provide en/decoding
854 for base64-based authentication conversation.
855 """
856
857 def __init__(self, mechinst):
858 self.mech = mechinst # Callable object to provide/process data
859
860 def process(self, data):
861 ret = self.mech(self.decode(data))
862 if ret is None:
863 return '*' # Abort conversation
864 return self.encode(ret)
865
866 def encode(self, inp):
867 #
868 # Invoke binascii.b2a_base64 iteratively with
869 # short even length buffers, strip the trailing
870 # line feed from the result and append. "Even"
871 # means a number that factors to both 6 and 8,
872 # so when it gets to the end of the 8-bit input
873 # there's no partial 6-bit output.
874 #
875 oup = ''
876 while inp:
877 if len(inp) > 48:
878 t = inp[:48]
879 inp = inp[48:]
880 else:
881 t = inp
882 inp = ''
883 e = binascii.b2a_base64(t)
884 if e:
885 oup = oup + e[:-1]
886 return oup
887
888 def decode(self, inp):
889 if not inp:
890 return ''
891 return binascii.a2b_base64(inp)
892
893
894
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000895Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
896 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
897
898def Internaldate2tuple(resp):
899
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000900 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000901
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000902 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000903 """
904
905 mo = InternalDate.match(resp)
906 if not mo:
907 return None
908
909 mon = Mon2num[mo.group('mon')]
910 zonen = mo.group('zonen')
911
912 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
913 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
914
915 # INTERNALDATE timezone must be subtracted to get UT
916
917 zone = (zoneh*60 + zonem)*60
918 if zonen == '-':
919 zone = -zone
920
921 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
922
923 utc = time.mktime(tt)
924
925 # Following is necessary because the time module has no 'mkgmtime'.
926 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
927
928 lt = time.localtime(utc)
929 if time.daylight and lt[-1]:
930 zone = zone + time.altzone
931 else:
932 zone = zone + time.timezone
933
934 return time.localtime(utc - zone)
935
936
937
938def Int2AP(num):
939
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000940 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000941
942 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000943 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000944 while num:
945 num, mod = divmod(num, 16)
946 val = AP[mod] + val
947 return val
948
949
950
951def ParseFlags(resp):
952
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000953 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000954
955 mo = Flags.match(resp)
956 if not mo:
957 return ()
958
959 return tuple(string.split(mo.group('flags')))
960
961
962def Time2Internaldate(date_time):
963
964 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
965
966 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
967 """
968
969 dttype = type(date_time)
Guido van Rossum8c062211999-12-13 23:27:45 +0000970 if dttype is type(1) or dttype is type(1.1):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000971 tt = time.localtime(date_time)
972 elif dttype is type(()):
973 tt = date_time
974 elif dttype is type(""):
975 return date_time # Assume in correct format
976 else: raise ValueError
977
978 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
979 if dt[0] == '0':
980 dt = ' ' + dt[1:]
981 if time.daylight and tt[-1]:
982 zone = -time.altzone
983 else:
984 zone = -time.timezone
985 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
986
987
988
Guido van Rossumeda960a1998-06-18 14:24:28 +0000989if __debug__:
990
Guido van Rossum8c062211999-12-13 23:27:45 +0000991 def _mesg(s, secs=None):
992 if secs is None:
993 secs = time.time()
994 tm = time.strftime('%M:%S', time.localtime(secs))
995 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
Guido van Rossum26367a01998-09-28 15:34:46 +0000996 sys.stderr.flush()
997
998 def _dump_ur(dict):
999 # Dump untagged responses (in `dict').
1000 l = dict.items()
1001 if not l: return
1002 t = '\n\t\t'
1003 j = string.join
1004 l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l)
1005 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
Guido van Rossumeda960a1998-06-18 14:24:28 +00001006
Guido van Rossum8c062211999-12-13 23:27:45 +00001007 _cmd_log = [] # Last `_cmd_log_len' interactions
1008 _cmd_log_len = 10
1009
1010 def _log(line):
1011 # Keep log of last `_cmd_log_len' interactions for debugging.
1012 if len(_cmd_log) == _cmd_log_len:
1013 del _cmd_log[0]
1014 _cmd_log.append((time.time(), line))
1015
1016 def print_log():
1017 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1018 for secs,line in _cmd_log:
1019 _mesg(line, secs)
Guido van Rossumeda960a1998-06-18 14:24:28 +00001020
1021
Guido van Rossum8c062211999-12-13 23:27:45 +00001022
1023if __name__ == '__main__':
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001024
Guido van Rossum66d45132000-03-28 20:20:53 +00001025 import getopt, getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +00001026
Guido van Rossum66d45132000-03-28 20:20:53 +00001027 try:
1028 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1029 except getopt.error, val:
1030 pass
1031
1032 for opt,val in optlist:
1033 if opt == '-d':
1034 Debug = int(val)
1035
1036 if not args: args = ('',)
1037
1038 host = args[0]
Guido van Rossumb1f08121998-06-25 02:22:16 +00001039
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001040 USER = getpass.getuser()
Guido van Rossumf36b1822000-02-17 17:12:39 +00001041 PASSWD = getpass.getpass("IMAP password for %s on %s" % (USER, host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001042
Guido van Rossumf36b1822000-02-17 17:12:39 +00001043 test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001044 test_seq1 = (
1045 ('login', (USER, PASSWD)),
Guido van Rossum46586821998-05-18 14:39:42 +00001046 ('create', ('/tmp/xxx 1',)),
1047 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1048 ('CREATE', ('/tmp/yyz 2',)),
Guido van Rossumf36b1822000-02-17 17:12:39 +00001049 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
Guido van Rossum8c062211999-12-13 23:27:45 +00001050 ('list', ('/tmp', 'yy*')),
Guido van Rossum46586821998-05-18 14:39:42 +00001051 ('select', ('/tmp/yyz 2',)),
Guido van Rossum66d45132000-03-28 20:20:53 +00001052 ('search', (None, 'SUBJECT', 'test')),
Guido van Rossumeda960a1998-06-18 14:24:28 +00001053 ('partial', ('1', 'RFC822', 1, 1024)),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001054 ('store', ('1', 'FLAGS', '(\Deleted)')),
1055 ('expunge', ()),
Guido van Rossum46586821998-05-18 14:39:42 +00001056 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001057 ('close', ()),
1058 )
1059
1060 test_seq2 = (
1061 ('select', ()),
1062 ('response',('UIDVALIDITY',)),
1063 ('uid', ('SEARCH', 'ALL')),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001064 ('response', ('EXISTS',)),
Guido van Rossumf36b1822000-02-17 17:12:39 +00001065 ('append', (None, None, None, test_mesg)),
Guido van Rossum46586821998-05-18 14:39:42 +00001066 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001067 ('logout', ()),
1068 )
1069
1070 def run(cmd, args):
Guido van Rossum8c062211999-12-13 23:27:45 +00001071 _mesg('%s %s' % (cmd, args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001072 typ, dat = apply(eval('M.%s' % cmd), args)
Guido van Rossum8c062211999-12-13 23:27:45 +00001073 _mesg('%s => %s %s' % (cmd, typ, dat))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001074 return dat
1075
Guido van Rossum66d45132000-03-28 20:20:53 +00001076 try:
1077 M = IMAP4(host)
1078 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001079
Guido van Rossum66d45132000-03-28 20:20:53 +00001080 for cmd,args in test_seq1:
1081 run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001082
Guido van Rossum66d45132000-03-28 20:20:53 +00001083 for ml in run('list', ('/tmp/', 'yy%')):
1084 mo = re.match(r'.*"([^"]+)"$', ml)
1085 if mo: path = mo.group(1)
1086 else: path = string.split(ml)[-1]
1087 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001088
Guido van Rossum66d45132000-03-28 20:20:53 +00001089 for cmd,args in test_seq2:
1090 dat = run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001091
Guido van Rossum66d45132000-03-28 20:20:53 +00001092 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1093 continue
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001094
Guido van Rossum66d45132000-03-28 20:20:53 +00001095 uid = string.split(dat[-1])
1096 if not uid: continue
1097 run('uid', ('FETCH', '%s' % uid[-1],
1098 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1099
1100 print '\nAll tests OK.'
1101
1102 except:
1103 print '\nTests failed.'
1104
1105 if not Debug:
1106 print '''
1107If you would like to see debugging output,
1108try: %s -d5
1109''' % sys.argv[0]
1110
1111 raise