blob: 4e3465442bee8a1f450f8c150cdb3ece947fe125 [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
Fred Drakefd267d92000-05-25 03:25:26 +000018__version__ = "2.39"
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
Fred Drakefd267d92000-05-25 03:25:26 +000090 an IMAP4 literal. If necessary (the string contains any
91 non-printing characters or white-space and isn't enclosed with
92 either parentheses or double quotes) each string is quoted.
93 However, the 'password' argument to the LOGIN command is always
94 quoted. If you want to avoid having an argument string quoted
95 (eg: the 'flags' argument to STORE) then enclose the string in
96 parentheses (eg: "(\Deleted)").
Guido van Rossum6884af71998-05-29 13:34:03 +000097
98 Each command returns a tuple: (type, [data, ...]) where 'type'
99 is usually 'OK' or 'NO', and 'data' is either the text from the
100 tagged response, or untagged results from command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000101
102 Errors raise the exception class <instance>.error("<reason>").
103 IMAP4 server errors raise <instance>.abort("<reason>"),
Guido van Rossum26367a01998-09-28 15:34:46 +0000104 which is a sub-class of 'error'. Mailbox status changes
105 from READ-WRITE to READ-ONLY raise the exception class
106 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
Guido van Rossumeda960a1998-06-18 14:24:28 +0000107
Guido van Rossum8c062211999-12-13 23:27:45 +0000108 "error" exceptions imply a program error.
109 "abort" exceptions imply the connection should be reset, and
110 the command re-tried.
111 "readonly" exceptions imply the command should be re-tried.
112
Guido van Rossumeda960a1998-06-18 14:24:28 +0000113 Note: to use this module, you must read the RFCs pertaining
114 to the IMAP4 protocol, as the semantics of the arguments to
115 each IMAP4 command are left to the invoker, not to mention
116 the results.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000117 """
118
119 class error(Exception): pass # Logical errors - debug required
120 class abort(error): pass # Service errors - close and retry
Guido van Rossum26367a01998-09-28 15:34:46 +0000121 class readonly(abort): pass # Mailbox status changed to READ-ONLY
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000122
Guido van Rossumf36b1822000-02-17 17:12:39 +0000123 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000124
125 def __init__(self, host = '', port = IMAP4_PORT):
126 self.host = host
127 self.port = port
128 self.debug = Debug
129 self.state = 'LOGOUT'
Guido van Rossum6884af71998-05-29 13:34:03 +0000130 self.literal = None # A literal argument to a command
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000131 self.tagged_commands = {} # Tagged commands awaiting response
132 self.untagged_responses = {} # {typ: [data, ...], ...}
133 self.continuation_response = '' # Last continuation response
Guido van Rossum619c3372000-02-28 22:37:30 +0000134 self.is_readonly = None # READ-ONLY desired state
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000135 self.tagnum = 0
136
137 # Open socket to server.
138
Guido van Rossumeda960a1998-06-18 14:24:28 +0000139 self.open(host, port)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000140
141 # Create unique tag for this session,
142 # and compile tagged response matcher.
143
Guido van Rossum6884af71998-05-29 13:34:03 +0000144 self.tagpre = Int2AP(random.randint(0, 31999))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000145 self.tagre = re.compile(r'(?P<tag>'
146 + self.tagpre
147 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
148
149 # Get server welcome message,
150 # request and store CAPABILITY response.
151
Guido van Rossum8c062211999-12-13 23:27:45 +0000152 if __debug__:
153 if self.debug >= 1:
154 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000155
156 self.welcome = self._get_response()
157 if self.untagged_responses.has_key('PREAUTH'):
158 self.state = 'AUTH'
159 elif self.untagged_responses.has_key('OK'):
160 self.state = 'NONAUTH'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000161 else:
162 raise self.error(self.welcome)
163
164 cap = 'CAPABILITY'
165 self._simple_command(cap)
166 if not self.untagged_responses.has_key(cap):
167 raise self.error('no CAPABILITY response from server')
Guido van Rossum04da10c1998-10-21 22:06:56 +0000168 self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1])))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000169
Guido van Rossum8c062211999-12-13 23:27:45 +0000170 if __debug__:
171 if self.debug >= 3:
172 _mesg('CAPABILITIES: %s' % `self.capabilities`)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000173
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000174 for version in AllowedVersions:
175 if not version in self.capabilities:
176 continue
177 self.PROTOCOL_VERSION = version
Guido van Rossumb1f08121998-06-25 02:22:16 +0000178 return
179
180 raise self.error('server not IMAP4 compliant')
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000181
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000182
Guido van Rossum26367a01998-09-28 15:34:46 +0000183 def __getattr__(self, attr):
184 # Allow UPPERCASE variants of IMAP4 command methods.
185 if Commands.has_key(attr):
186 return eval("self.%s" % string.lower(attr))
187 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
188
189
190
191 # Public methods
192
193
Guido van Rossumeda960a1998-06-18 14:24:28 +0000194 def open(self, host, port):
195 """Setup 'self.sock' and 'self.file'."""
196 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Guido van Rossum93a7c0f2000-03-28 21:45:46 +0000197 self.sock.connect((self.host, self.port))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000198 self.file = self.sock.makefile('r')
199
200
Guido van Rossum26367a01998-09-28 15:34:46 +0000201 def recent(self):
202 """Return most recent 'RECENT' responses if any exist,
203 else prompt server for an update using the 'NOOP' command.
204
205 (typ, [data]) = <instance>.recent()
206
207 'data' is None if no new messages,
208 else list of RECENT responses, most recent last.
209 """
210 name = 'RECENT'
211 typ, dat = self._untagged_response('OK', [None], name)
212 if dat[-1]:
213 return typ, dat
214 typ, dat = self.noop() # Prod server for response
215 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000216
217
Guido van Rossum26367a01998-09-28 15:34:46 +0000218 def response(self, code):
219 """Return data for response 'code' if received, or None.
220
221 Old value for response 'code' is cleared.
222
223 (code, [data]) = <instance>.response(code)
224 """
225 return self._untagged_response(code, [None], string.upper(code))
226
227
228 def socket(self):
229 """Return socket instance used to connect to IMAP4 server.
230
231 socket = <instance>.socket()
232 """
233 return self.sock
234
235
236
237 # IMAP4 commands
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000238
239
240 def append(self, mailbox, flags, date_time, message):
241 """Append message to named mailbox.
242
243 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
Guido van Rossum8c062211999-12-13 23:27:45 +0000244
245 All args except `message' can be None.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000246 """
247 name = 'APPEND'
Guido van Rossum8c062211999-12-13 23:27:45 +0000248 if not mailbox:
249 mailbox = 'INBOX'
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000250 if flags:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000251 if (flags[0],flags[-1]) != ('(',')'):
252 flags = '(%s)' % flags
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000253 else:
254 flags = None
255 if date_time:
256 date_time = Time2Internaldate(date_time)
257 else:
258 date_time = None
Guido van Rossum6884af71998-05-29 13:34:03 +0000259 self.literal = message
260 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000261
262
Guido van Rossumeda960a1998-06-18 14:24:28 +0000263 def authenticate(self, mechanism, authobject):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000264 """Authenticate command - requires response processing.
265
Guido van Rossumeda960a1998-06-18 14:24:28 +0000266 'mechanism' specifies which authentication mechanism is to
267 be used - it must appear in <instance>.capabilities in the
268 form AUTH=<mechanism>.
269
270 'authobject' must be a callable object:
271
272 data = authobject(response)
273
274 It will be called to process server continuation responses.
275 It should return data that will be encoded and sent to server.
276 It should return None if the client abort response '*' should
277 be sent instead.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000278 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000279 mech = string.upper(mechanism)
280 cap = 'AUTH=%s' % mech
281 if not cap in self.capabilities:
282 raise self.error("Server doesn't allow %s authentication." % mech)
283 self.literal = _Authenticator(authobject).process
284 typ, dat = self._simple_command('AUTHENTICATE', mech)
285 if typ != 'OK':
Guido van Rossum26367a01998-09-28 15:34:46 +0000286 raise self.error(dat[-1])
Guido van Rossumeda960a1998-06-18 14:24:28 +0000287 self.state = 'AUTH'
288 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000289
290
291 def check(self):
292 """Checkpoint mailbox on server.
293
294 (typ, [data]) = <instance>.check()
295 """
296 return self._simple_command('CHECK')
297
298
299 def close(self):
300 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000301
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000302 Deleted messages are removed from writable mailbox.
303 This is the recommended command before 'LOGOUT'.
304
305 (typ, [data]) = <instance>.close()
306 """
307 try:
Guido van Rossum26367a01998-09-28 15:34:46 +0000308 typ, dat = self._simple_command('CLOSE')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000309 finally:
310 self.state = 'AUTH'
311 return typ, dat
312
313
314 def copy(self, message_set, new_mailbox):
315 """Copy 'message_set' messages onto end of 'new_mailbox'.
316
317 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
318 """
319 return self._simple_command('COPY', message_set, new_mailbox)
320
321
322 def create(self, mailbox):
323 """Create new mailbox.
324
325 (typ, [data]) = <instance>.create(mailbox)
326 """
327 return self._simple_command('CREATE', mailbox)
328
329
330 def delete(self, mailbox):
331 """Delete old mailbox.
332
333 (typ, [data]) = <instance>.delete(mailbox)
334 """
335 return self._simple_command('DELETE', mailbox)
336
337
338 def expunge(self):
339 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000340
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000341 Generates 'EXPUNGE' response for each deleted message.
342
343 (typ, [data]) = <instance>.expunge()
344
345 'data' is list of 'EXPUNGE'd message numbers in order received.
346 """
347 name = 'EXPUNGE'
348 typ, dat = self._simple_command(name)
Guido van Rossum26367a01998-09-28 15:34:46 +0000349 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000350
351
352 def fetch(self, message_set, message_parts):
353 """Fetch (parts of) messages.
354
355 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
356
Fred Drakefd267d92000-05-25 03:25:26 +0000357 'message_parts' should be a string of selected parts
358 enclosed in parentheses, eg: "(UID BODY[TEXT])".
359
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000360 'data' are tuples of message part envelope and data.
361 """
362 name = 'FETCH'
363 typ, dat = self._simple_command(name, message_set, message_parts)
Guido van Rossum26367a01998-09-28 15:34:46 +0000364 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000365
366
367 def list(self, directory='""', pattern='*'):
368 """List mailbox names in directory matching pattern.
369
370 (typ, [data]) = <instance>.list(directory='""', pattern='*')
371
372 'data' is list of LIST responses.
373 """
374 name = 'LIST'
375 typ, dat = self._simple_command(name, directory, pattern)
Guido van Rossum26367a01998-09-28 15:34:46 +0000376 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000377
378
379 def login(self, user, password):
380 """Identify client using plaintext password.
381
Guido van Rossum8c062211999-12-13 23:27:45 +0000382 (typ, [data]) = <instance>.login(user, password)
383
384 NB: 'password' will be quoted.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000385 """
Guido van Rossum8c062211999-12-13 23:27:45 +0000386 #if not 'AUTH=LOGIN' in self.capabilities:
387 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
388 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000389 if typ != 'OK':
Guido van Rossum26367a01998-09-28 15:34:46 +0000390 raise self.error(dat[-1])
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000391 self.state = 'AUTH'
392 return typ, dat
393
394
395 def logout(self):
396 """Shutdown connection to server.
397
398 (typ, [data]) = <instance>.logout()
399
400 Returns server 'BYE' response.
401 """
402 self.state = 'LOGOUT'
403 try: typ, dat = self._simple_command('LOGOUT')
Guido van Rossum26367a01998-09-28 15:34:46 +0000404 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000405 self.file.close()
406 self.sock.close()
407 if self.untagged_responses.has_key('BYE'):
408 return 'BYE', self.untagged_responses['BYE']
409 return typ, dat
410
411
412 def lsub(self, directory='""', pattern='*'):
413 """List 'subscribed' mailbox names in directory matching pattern.
414
415 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
416
417 'data' are tuples of message part envelope and data.
418 """
419 name = 'LSUB'
420 typ, dat = self._simple_command(name, directory, pattern)
Guido van Rossum26367a01998-09-28 15:34:46 +0000421 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000422
423
Guido van Rossum6884af71998-05-29 13:34:03 +0000424 def noop(self):
425 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000426
Guido van Rossum6884af71998-05-29 13:34:03 +0000427 (typ, data) = <instance>.noop()
428 """
Guido van Rossum8c062211999-12-13 23:27:45 +0000429 if __debug__:
430 if self.debug >= 3:
431 _dump_ur(self.untagged_responses)
Guido van Rossum6884af71998-05-29 13:34:03 +0000432 return self._simple_command('NOOP')
433
434
Guido van Rossumeda960a1998-06-18 14:24:28 +0000435 def partial(self, message_num, message_part, start, length):
436 """Fetch truncated part of a message.
437
438 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
439
440 'data' is tuple of message part envelope and data.
441 """
442 name = 'PARTIAL'
443 typ, dat = self._simple_command(name, message_num, message_part, start, length)
Guido van Rossum26367a01998-09-28 15:34:46 +0000444 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000445
446
447 def rename(self, oldmailbox, newmailbox):
448 """Rename old mailbox name to new.
449
450 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
451 """
452 return self._simple_command('RENAME', oldmailbox, newmailbox)
453
454
Guido van Rossum66d45132000-03-28 20:20:53 +0000455 def search(self, charset, *criteria):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000456 """Search mailbox for matching messages.
457
Guido van Rossum66d45132000-03-28 20:20:53 +0000458 (typ, [data]) = <instance>.search(charset, criterium, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000459
460 'data' is space separated list of matching message numbers.
461 """
462 name = 'SEARCH'
463 if charset:
464 charset = 'CHARSET ' + charset
Guido van Rossum66d45132000-03-28 20:20:53 +0000465 typ, dat = apply(self._simple_command, (name, charset) + criteria)
Guido van Rossum26367a01998-09-28 15:34:46 +0000466 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000467
468
469 def select(self, mailbox='INBOX', readonly=None):
470 """Select a mailbox.
471
Guido van Rossum46586821998-05-18 14:39:42 +0000472 Flush all untagged responses.
473
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000474 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
475
476 'data' is count of messages in mailbox ('EXISTS' response).
477 """
478 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
Guido van Rossum46586821998-05-18 14:39:42 +0000479 self.untagged_responses = {} # Flush old responses.
Guido van Rossum619c3372000-02-28 22:37:30 +0000480 self.is_readonly = readonly
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000481 if readonly:
482 name = 'EXAMINE'
483 else:
484 name = 'SELECT'
485 typ, dat = self._simple_command(name, mailbox)
Guido van Rossum26367a01998-09-28 15:34:46 +0000486 if typ != 'OK':
487 self.state = 'AUTH' # Might have been 'SELECTED'
488 return typ, dat
489 self.state = 'SELECTED'
Guido van Rossum619c3372000-02-28 22:37:30 +0000490 if self.untagged_responses.has_key('READ-ONLY') \
Guido van Rossum26367a01998-09-28 15:34:46 +0000491 and not readonly:
Guido van Rossum8c062211999-12-13 23:27:45 +0000492 if __debug__:
493 if self.debug >= 1:
494 _dump_ur(self.untagged_responses)
Guido van Rossum26367a01998-09-28 15:34:46 +0000495 raise self.readonly('%s is not writable' % mailbox)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000496 return typ, self.untagged_responses.get('EXISTS', [None])
497
498
499 def status(self, mailbox, names):
500 """Request named status conditions for mailbox.
501
502 (typ, [data]) = <instance>.status(mailbox, names)
503 """
504 name = 'STATUS'
Guido van Rossumbe14e691998-04-11 03:11:51 +0000505 if self.PROTOCOL_VERSION == 'IMAP4':
506 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000507 typ, dat = self._simple_command(name, mailbox, names)
Guido van Rossum26367a01998-09-28 15:34:46 +0000508 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000509
510
Fred Drakefd267d92000-05-25 03:25:26 +0000511 def store(self, message_set, command, flags):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000512 """Alters flag dispositions for messages in mailbox.
513
Fred Drakefd267d92000-05-25 03:25:26 +0000514 (typ, [data]) = <instance>.store(message_set, command, flags)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000515 """
Fred Drakefd267d92000-05-25 03:25:26 +0000516 if (flags[0],flags[-1]) != ('(',')'):
517 flags = '(%s)' % flags # Avoid quoting the flags
518 typ, dat = self._simple_command('STORE', message_set, command, flags)
Guido van Rossum26367a01998-09-28 15:34:46 +0000519 return self._untagged_response(typ, dat, 'FETCH')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000520
521
522 def subscribe(self, mailbox):
523 """Subscribe to new mailbox.
524
525 (typ, [data]) = <instance>.subscribe(mailbox)
526 """
527 return self._simple_command('SUBSCRIBE', mailbox)
528
529
Guido van Rossum46586821998-05-18 14:39:42 +0000530 def uid(self, command, *args):
531 """Execute "command arg ..." with messages identified by UID,
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000532 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000533
Guido van Rossum46586821998-05-18 14:39:42 +0000534 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000535
536 Returns response appropriate to 'command'.
537 """
Guido van Rossumeda960a1998-06-18 14:24:28 +0000538 command = string.upper(command)
539 if not Commands.has_key(command):
540 raise self.error("Unknown IMAP4 UID command: %s" % command)
541 if self.state not in Commands[command]:
542 raise self.error('command %s illegal in state %s'
543 % (command, self.state))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000544 name = 'UID'
Guido van Rossumeda960a1998-06-18 14:24:28 +0000545 typ, dat = apply(self._simple_command, (name, command) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000546 if command == 'SEARCH':
547 name = 'SEARCH'
548 else:
549 name = 'FETCH'
Guido van Rossum26367a01998-09-28 15:34:46 +0000550 return self._untagged_response(typ, dat, name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000551
552
553 def unsubscribe(self, mailbox):
554 """Unsubscribe from old mailbox.
555
556 (typ, [data]) = <instance>.unsubscribe(mailbox)
557 """
558 return self._simple_command('UNSUBSCRIBE', mailbox)
559
560
Guido van Rossum46586821998-05-18 14:39:42 +0000561 def xatom(self, name, *args):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000562 """Allow simple extension commands
563 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000564
Guido van Rossum46586821998-05-18 14:39:42 +0000565 (typ, [data]) = <instance>.xatom(name, arg, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000566 """
567 if name[0] != 'X' or not name in self.capabilities:
568 raise self.error('unknown extension command: %s' % name)
Guido van Rossum46586821998-05-18 14:39:42 +0000569 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000570
571
572
573 # Private methods
574
575
576 def _append_untagged(self, typ, dat):
577
Guido van Rossum8c062211999-12-13 23:27:45 +0000578 if dat is None: dat = ''
Guido van Rossumeda960a1998-06-18 14:24:28 +0000579 ur = self.untagged_responses
Guido van Rossum8c062211999-12-13 23:27:45 +0000580 if __debug__:
581 if self.debug >= 5:
582 _mesg('untagged_responses[%s] %s += ["%s"]' %
583 (typ, len(ur.get(typ,'')), dat))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000584 if ur.has_key(typ):
585 ur[typ].append(dat)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000586 else:
Guido van Rossumeda960a1998-06-18 14:24:28 +0000587 ur[typ] = [dat]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000588
589
Guido van Rossum8c062211999-12-13 23:27:45 +0000590 def _check_bye(self):
591 bye = self.untagged_responses.get('BYE')
592 if bye:
593 raise self.abort(bye[-1])
594
595
Guido van Rossum6884af71998-05-29 13:34:03 +0000596 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000597
598 if self.state not in Commands[name]:
Guido van Rossum6884af71998-05-29 13:34:03 +0000599 self.literal = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000600 raise self.error(
601 'command %s illegal in state %s' % (name, self.state))
602
Guido van Rossum26367a01998-09-28 15:34:46 +0000603 for typ in ('OK', 'NO', 'BAD'):
604 if self.untagged_responses.has_key(typ):
605 del self.untagged_responses[typ]
606
Guido van Rossum619c3372000-02-28 22:37:30 +0000607 if self.untagged_responses.has_key('READ-ONLY') \
608 and not self.is_readonly:
Guido van Rossum26367a01998-09-28 15:34:46 +0000609 raise self.readonly('mailbox status changed to READ-ONLY')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000610
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000611 tag = self._new_tag()
612 data = '%s %s' % (tag, name)
Guido van Rossum8c062211999-12-13 23:27:45 +0000613 for arg in args:
614 if arg is None: continue
615 data = '%s %s' % (data, self._checkquote(arg))
Guido van Rossum6884af71998-05-29 13:34:03 +0000616
617 literal = self.literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000618 if literal is not None:
Guido van Rossum6884af71998-05-29 13:34:03 +0000619 self.literal = None
Guido van Rossumeda960a1998-06-18 14:24:28 +0000620 if type(literal) is type(self._command):
621 literator = literal
622 else:
623 literator = None
624 data = '%s {%s}' % (data, len(literal))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000625
Guido van Rossum8c062211999-12-13 23:27:45 +0000626 if __debug__:
627 if self.debug >= 4:
628 _mesg('> %s' % data)
629 else:
630 _log('> %s' % data)
631
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000632 try:
633 self.sock.send('%s%s' % (data, CRLF))
634 except socket.error, val:
635 raise self.abort('socket error: %s' % val)
636
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000637 if literal is None:
638 return tag
639
Guido van Rossumeda960a1998-06-18 14:24:28 +0000640 while 1:
641 # Wait for continuation response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000642
Guido van Rossumeda960a1998-06-18 14:24:28 +0000643 while self._get_response():
644 if self.tagged_commands[tag]: # BAD/NO?
645 return tag
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000646
Guido van Rossumeda960a1998-06-18 14:24:28 +0000647 # Send literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000648
Guido van Rossumeda960a1998-06-18 14:24:28 +0000649 if literator:
650 literal = literator(self.continuation_response)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000651
Guido van Rossum8c062211999-12-13 23:27:45 +0000652 if __debug__:
653 if self.debug >= 4:
654 _mesg('write literal size %s' % len(literal))
Guido van Rossumeda960a1998-06-18 14:24:28 +0000655
656 try:
657 self.sock.send(literal)
658 self.sock.send(CRLF)
659 except socket.error, val:
660 raise self.abort('socket error: %s' % val)
661
662 if not literator:
663 break
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000664
665 return tag
666
667
668 def _command_complete(self, name, tag):
Guido van Rossum8c062211999-12-13 23:27:45 +0000669 self._check_bye()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000670 try:
671 typ, data = self._get_tagged_response(tag)
672 except self.abort, val:
673 raise self.abort('command: %s => %s' % (name, val))
674 except self.error, val:
675 raise self.error('command: %s => %s' % (name, val))
Guido van Rossum8c062211999-12-13 23:27:45 +0000676 self._check_bye()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000677 if typ == 'BAD':
678 raise self.error('%s command error: %s %s' % (name, typ, data))
679 return typ, data
680
681
682 def _get_response(self):
683
684 # Read response and store.
685 #
686 # Returns None for continuation responses,
Guido van Rossum46586821998-05-18 14:39:42 +0000687 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000688
Guido van Rossum46586821998-05-18 14:39:42 +0000689 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000690
691 # Command completion response?
692
693 if self._match(self.tagre, resp):
694 tag = self.mo.group('tag')
695 if not self.tagged_commands.has_key(tag):
696 raise self.abort('unexpected tagged response: %s' % resp)
697
698 typ = self.mo.group('type')
699 dat = self.mo.group('data')
700 self.tagged_commands[tag] = (typ, [dat])
701 else:
702 dat2 = None
703
704 # '*' (untagged) responses?
705
706 if not self._match(Untagged_response, resp):
707 if self._match(Untagged_status, resp):
708 dat2 = self.mo.group('data2')
709
710 if self.mo is None:
Guido van Rossumf36b1822000-02-17 17:12:39 +0000711 # Only other possibility is '+' (continuation) response...
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000712
713 if self._match(Continuation, resp):
714 self.continuation_response = self.mo.group('data')
715 return None # NB: indicates continuation
716
Guido van Rossumeda960a1998-06-18 14:24:28 +0000717 raise self.abort("unexpected response: '%s'" % resp)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000718
719 typ = self.mo.group('type')
720 dat = self.mo.group('data')
Guido van Rossumeda960a1998-06-18 14:24:28 +0000721 if dat is None: dat = '' # Null untagged response
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000722 if dat2: dat = dat + ' ' + dat2
723
724 # Is there a literal to come?
725
726 while self._match(Literal, dat):
727
728 # Read literal direct from connection.
729
730 size = string.atoi(self.mo.group('size'))
Guido van Rossum8c062211999-12-13 23:27:45 +0000731 if __debug__:
732 if self.debug >= 4:
733 _mesg('read literal size %s' % size)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000734 data = self.file.read(size)
735
736 # Store response with literal as tuple
737
738 self._append_untagged(typ, (dat, data))
739
740 # Read trailer - possibly containing another literal
741
Guido van Rossum46586821998-05-18 14:39:42 +0000742 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000743
744 self._append_untagged(typ, dat)
745
746 # Bracketed response information?
747
748 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
749 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
750
Guido van Rossum8c062211999-12-13 23:27:45 +0000751 if __debug__:
752 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
753 _mesg('%s response: %s' % (typ, dat))
Guido van Rossum26367a01998-09-28 15:34:46 +0000754
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000755 return resp
756
757
758 def _get_tagged_response(self, tag):
759
760 while 1:
761 result = self.tagged_commands[tag]
762 if result is not None:
763 del self.tagged_commands[tag]
764 return result
Guido van Rossumf36b1822000-02-17 17:12:39 +0000765
766 # Some have reported "unexpected response" exceptions.
Guido van Rossum19ce91b2000-02-24 02:24:50 +0000767 # Note that ignoring them here causes loops.
768 # Instead, send me details of the unexpected response and
769 # I'll update the code in `_get_response()'.
Guido van Rossumf36b1822000-02-17 17:12:39 +0000770
771 try:
772 self._get_response()
773 except self.abort, val:
774 if __debug__:
775 if self.debug >= 1:
Guido van Rossumf36b1822000-02-17 17:12:39 +0000776 print_log()
Guido van Rossum19ce91b2000-02-24 02:24:50 +0000777 raise
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000778
779
780 def _get_line(self):
781
782 line = self.file.readline()
783 if not line:
Guido van Rossum26367a01998-09-28 15:34:46 +0000784 raise self.abort('socket error: EOF')
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000785
786 # Protocol mandates all lines terminated by CRLF
787
Guido van Rossum46586821998-05-18 14:39:42 +0000788 line = line[:-2]
Guido van Rossum8c062211999-12-13 23:27:45 +0000789 if __debug__:
790 if self.debug >= 4:
791 _mesg('< %s' % line)
792 else:
793 _log('< %s' % line)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000794 return line
795
796
797 def _match(self, cre, s):
798
799 # Run compiled regular expression match method on 's'.
800 # Save result, return success.
801
802 self.mo = cre.match(s)
Guido van Rossum8c062211999-12-13 23:27:45 +0000803 if __debug__:
804 if self.mo is not None and self.debug >= 5:
805 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000806 return self.mo is not None
807
808
809 def _new_tag(self):
810
811 tag = '%s%s' % (self.tagpre, self.tagnum)
812 self.tagnum = self.tagnum + 1
813 self.tagged_commands[tag] = None
814 return tag
815
816
Guido van Rossum8c062211999-12-13 23:27:45 +0000817 def _checkquote(self, arg):
818
819 # Must quote command args if non-alphanumeric chars present,
820 # and not already quoted.
821
822 if type(arg) is not type(''):
823 return arg
824 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
825 return arg
826 if self.mustquote.search(arg) is None:
827 return arg
828 return self._quote(arg)
829
830
831 def _quote(self, arg):
832
833 arg = string.replace(arg, '\\', '\\\\')
834 arg = string.replace(arg, '"', '\\"')
835
836 return '"%s"' % arg
837
838
Guido van Rossum46586821998-05-18 14:39:42 +0000839 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000840
Guido van Rossum46586821998-05-18 14:39:42 +0000841 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000842
843
Guido van Rossum26367a01998-09-28 15:34:46 +0000844 def _untagged_response(self, typ, dat, name):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000845
Guido van Rossum26367a01998-09-28 15:34:46 +0000846 if typ == 'NO':
847 return typ, dat
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000848 if not self.untagged_responses.has_key(name):
849 return typ, [None]
850 data = self.untagged_responses[name]
Guido van Rossum8c062211999-12-13 23:27:45 +0000851 if __debug__:
852 if self.debug >= 5:
853 _mesg('untagged_responses[%s] => %s' % (name, data))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000854 del self.untagged_responses[name]
855 return typ, data
856
857
858
Guido van Rossumeda960a1998-06-18 14:24:28 +0000859class _Authenticator:
860
861 """Private class to provide en/decoding
862 for base64-based authentication conversation.
863 """
864
865 def __init__(self, mechinst):
866 self.mech = mechinst # Callable object to provide/process data
867
868 def process(self, data):
869 ret = self.mech(self.decode(data))
870 if ret is None:
871 return '*' # Abort conversation
872 return self.encode(ret)
873
874 def encode(self, inp):
875 #
876 # Invoke binascii.b2a_base64 iteratively with
877 # short even length buffers, strip the trailing
878 # line feed from the result and append. "Even"
879 # means a number that factors to both 6 and 8,
880 # so when it gets to the end of the 8-bit input
881 # there's no partial 6-bit output.
882 #
883 oup = ''
884 while inp:
885 if len(inp) > 48:
886 t = inp[:48]
887 inp = inp[48:]
888 else:
889 t = inp
890 inp = ''
891 e = binascii.b2a_base64(t)
892 if e:
893 oup = oup + e[:-1]
894 return oup
895
896 def decode(self, inp):
897 if not inp:
898 return ''
899 return binascii.a2b_base64(inp)
900
901
902
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000903Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
904 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
905
906def Internaldate2tuple(resp):
907
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000908 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000909
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000910 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000911 """
912
913 mo = InternalDate.match(resp)
914 if not mo:
915 return None
916
917 mon = Mon2num[mo.group('mon')]
918 zonen = mo.group('zonen')
919
920 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
921 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
922
923 # INTERNALDATE timezone must be subtracted to get UT
924
925 zone = (zoneh*60 + zonem)*60
926 if zonen == '-':
927 zone = -zone
928
929 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
930
931 utc = time.mktime(tt)
932
933 # Following is necessary because the time module has no 'mkgmtime'.
934 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
935
936 lt = time.localtime(utc)
937 if time.daylight and lt[-1]:
938 zone = zone + time.altzone
939 else:
940 zone = zone + time.timezone
941
942 return time.localtime(utc - zone)
943
944
945
946def Int2AP(num):
947
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000948 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000949
950 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000951 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000952 while num:
953 num, mod = divmod(num, 16)
954 val = AP[mod] + val
955 return val
956
957
958
959def ParseFlags(resp):
960
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000961 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000962
963 mo = Flags.match(resp)
964 if not mo:
965 return ()
966
967 return tuple(string.split(mo.group('flags')))
968
969
970def Time2Internaldate(date_time):
971
972 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
973
974 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
975 """
976
977 dttype = type(date_time)
Guido van Rossum8c062211999-12-13 23:27:45 +0000978 if dttype is type(1) or dttype is type(1.1):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000979 tt = time.localtime(date_time)
980 elif dttype is type(()):
981 tt = date_time
982 elif dttype is type(""):
983 return date_time # Assume in correct format
984 else: raise ValueError
985
986 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
987 if dt[0] == '0':
988 dt = ' ' + dt[1:]
989 if time.daylight and tt[-1]:
990 zone = -time.altzone
991 else:
992 zone = -time.timezone
993 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
994
995
996
Guido van Rossumeda960a1998-06-18 14:24:28 +0000997if __debug__:
998
Guido van Rossum8c062211999-12-13 23:27:45 +0000999 def _mesg(s, secs=None):
1000 if secs is None:
1001 secs = time.time()
1002 tm = time.strftime('%M:%S', time.localtime(secs))
1003 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
Guido van Rossum26367a01998-09-28 15:34:46 +00001004 sys.stderr.flush()
1005
1006 def _dump_ur(dict):
1007 # Dump untagged responses (in `dict').
1008 l = dict.items()
1009 if not l: return
1010 t = '\n\t\t'
1011 j = string.join
1012 l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l)
1013 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
Guido van Rossumeda960a1998-06-18 14:24:28 +00001014
Guido van Rossum8c062211999-12-13 23:27:45 +00001015 _cmd_log = [] # Last `_cmd_log_len' interactions
1016 _cmd_log_len = 10
1017
1018 def _log(line):
1019 # Keep log of last `_cmd_log_len' interactions for debugging.
1020 if len(_cmd_log) == _cmd_log_len:
1021 del _cmd_log[0]
1022 _cmd_log.append((time.time(), line))
1023
1024 def print_log():
1025 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1026 for secs,line in _cmd_log:
1027 _mesg(line, secs)
Guido van Rossumeda960a1998-06-18 14:24:28 +00001028
1029
Guido van Rossum8c062211999-12-13 23:27:45 +00001030
1031if __name__ == '__main__':
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001032
Guido van Rossum66d45132000-03-28 20:20:53 +00001033 import getopt, getpass, sys
Guido van Rossumd6596931998-05-29 18:08:48 +00001034
Guido van Rossum66d45132000-03-28 20:20:53 +00001035 try:
1036 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1037 except getopt.error, val:
1038 pass
1039
1040 for opt,val in optlist:
1041 if opt == '-d':
1042 Debug = int(val)
1043
1044 if not args: args = ('',)
1045
1046 host = args[0]
Guido van Rossumb1f08121998-06-25 02:22:16 +00001047
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001048 USER = getpass.getuser()
Guido van Rossumf36b1822000-02-17 17:12:39 +00001049 PASSWD = getpass.getpass("IMAP password for %s on %s" % (USER, host or "localhost"))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001050
Guido van Rossumf36b1822000-02-17 17:12:39 +00001051 test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001052 test_seq1 = (
1053 ('login', (USER, PASSWD)),
Guido van Rossum46586821998-05-18 14:39:42 +00001054 ('create', ('/tmp/xxx 1',)),
1055 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1056 ('CREATE', ('/tmp/yyz 2',)),
Guido van Rossumf36b1822000-02-17 17:12:39 +00001057 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
Guido van Rossum8c062211999-12-13 23:27:45 +00001058 ('list', ('/tmp', 'yy*')),
Guido van Rossum46586821998-05-18 14:39:42 +00001059 ('select', ('/tmp/yyz 2',)),
Guido van Rossum66d45132000-03-28 20:20:53 +00001060 ('search', (None, 'SUBJECT', 'test')),
Guido van Rossumeda960a1998-06-18 14:24:28 +00001061 ('partial', ('1', 'RFC822', 1, 1024)),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001062 ('store', ('1', 'FLAGS', '(\Deleted)')),
1063 ('expunge', ()),
Guido van Rossum46586821998-05-18 14:39:42 +00001064 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001065 ('close', ()),
1066 )
1067
1068 test_seq2 = (
1069 ('select', ()),
1070 ('response',('UIDVALIDITY',)),
1071 ('uid', ('SEARCH', 'ALL')),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001072 ('response', ('EXISTS',)),
Guido van Rossumf36b1822000-02-17 17:12:39 +00001073 ('append', (None, None, None, test_mesg)),
Guido van Rossum46586821998-05-18 14:39:42 +00001074 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001075 ('logout', ()),
1076 )
1077
1078 def run(cmd, args):
Guido van Rossum8c062211999-12-13 23:27:45 +00001079 _mesg('%s %s' % (cmd, args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001080 typ, dat = apply(eval('M.%s' % cmd), args)
Guido van Rossum8c062211999-12-13 23:27:45 +00001081 _mesg('%s => %s %s' % (cmd, typ, dat))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001082 return dat
1083
Guido van Rossum66d45132000-03-28 20:20:53 +00001084 try:
1085 M = IMAP4(host)
1086 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001087
Guido van Rossum66d45132000-03-28 20:20:53 +00001088 for cmd,args in test_seq1:
1089 run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001090
Guido van Rossum66d45132000-03-28 20:20:53 +00001091 for ml in run('list', ('/tmp/', 'yy%')):
1092 mo = re.match(r'.*"([^"]+)"$', ml)
1093 if mo: path = mo.group(1)
1094 else: path = string.split(ml)[-1]
1095 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001096
Guido van Rossum66d45132000-03-28 20:20:53 +00001097 for cmd,args in test_seq2:
1098 dat = run(cmd, args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001099
Guido van Rossum66d45132000-03-28 20:20:53 +00001100 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1101 continue
Guido van Rossum38d8f4e1998-04-11 01:22:34 +00001102
Guido van Rossum66d45132000-03-28 20:20:53 +00001103 uid = string.split(dat[-1])
1104 if not uid: continue
1105 run('uid', ('FETCH', '%s' % uid[-1],
1106 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1107
1108 print '\nAll tests OK.'
1109
1110 except:
1111 print '\nTests failed.'
1112
1113 if not Debug:
1114 print '''
1115If you would like to see debugging output,
1116try: %s -d5
1117''' % sys.argv[0]
1118
1119 raise