blob: e62764452ed8f631392d46bb68a1128b52334995 [file] [log] [blame]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +00001"""IMAP4 client.
2
3Based on RFC 2060.
4
5Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
6
7Public class: IMAP4
8Public variable: Debug
9Public functions: Internaldate2tuple
10 Int2AP
11 ParseFlags
12 Time2Internaldate
13"""
14
Guido van Rossumb26a1b41998-05-20 17:05:52 +000015import re, socket, string, time, random
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000016
17# Globals
18
19CRLF = '\r\n'
20Debug = 0
21IMAP4_PORT = 143
Guido van Rossum38d8f4e1998-04-11 01:22:34 +000022AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000023
24# Commands
25
26Commands = {
27 # name valid states
28 'APPEND': ('AUTH', 'SELECTED'),
29 'AUTHENTICATE': ('NONAUTH',),
30 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
31 'CHECK': ('SELECTED',),
32 'CLOSE': ('SELECTED',),
33 'COPY': ('SELECTED',),
34 'CREATE': ('AUTH', 'SELECTED'),
35 'DELETE': ('AUTH', 'SELECTED'),
36 'EXAMINE': ('AUTH', 'SELECTED'),
37 'EXPUNGE': ('SELECTED',),
38 'FETCH': ('SELECTED',),
39 'LIST': ('AUTH', 'SELECTED'),
40 'LOGIN': ('NONAUTH',),
41 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
42 'LSUB': ('AUTH', 'SELECTED'),
43 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
44 'RENAME': ('AUTH', 'SELECTED'),
45 'SEARCH': ('SELECTED',),
46 'SELECT': ('AUTH', 'SELECTED'),
47 'STATUS': ('AUTH', 'SELECTED'),
48 'STORE': ('SELECTED',),
49 'SUBSCRIBE': ('AUTH', 'SELECTED'),
50 'UID': ('SELECTED',),
51 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
52 }
53
54# Patterns to match server responses
55
56Continuation = re.compile(r'\+ (?P<data>.*)')
57Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
58InternalDate = re.compile(r'.*INTERNALDATE "'
59 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
60 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
61 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
62 r'"')
63Literal = re.compile(r'(?P<data>.*) {(?P<size>\d+)}$')
64Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
65Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+) (?P<data>.*)')
66Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
67
68
69
70class IMAP4:
71
72 """IMAP4 client class.
73
74 Instantiate with: IMAP4([host[, port]])
75
76 host - host's name (default: localhost);
77 port - port number (default: standard IMAP4 port).
78
79 All IMAP4rev1 commands are supported by methods of the same
Guido van Rossum6884af71998-05-29 13:34:03 +000080 name (in lower-case).
81
82 All arguments to commands are converted to strings, except for
83 the last argument to APPEND which is passed as an IMAP4
84 literal. If necessary (the string isn't enclosed with either
85 parentheses or double quotes) each converted string is quoted.
86
87 Each command returns a tuple: (type, [data, ...]) where 'type'
88 is usually 'OK' or 'NO', and 'data' is either the text from the
89 tagged response, or untagged results from command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000090
91 Errors raise the exception class <instance>.error("<reason>").
92 IMAP4 server errors raise <instance>.abort("<reason>"),
93 which is a sub-class of 'error'.
94 """
95
96 class error(Exception): pass # Logical errors - debug required
97 class abort(error): pass # Service errors - close and retry
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000098
99
100 def __init__(self, host = '', port = IMAP4_PORT):
101 self.host = host
102 self.port = port
103 self.debug = Debug
104 self.state = 'LOGOUT'
Guido van Rossum6884af71998-05-29 13:34:03 +0000105 self.literal = None # A literal argument to a command
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000106 self.tagged_commands = {} # Tagged commands awaiting response
107 self.untagged_responses = {} # {typ: [data, ...], ...}
108 self.continuation_response = '' # Last continuation response
109 self.tagnum = 0
110
111 # Open socket to server.
112
113 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
114 self.sock.connect(self.host, self.port)
115 self.file = self.sock.makefile('r')
116
117 # Create unique tag for this session,
118 # and compile tagged response matcher.
119
Guido van Rossum6884af71998-05-29 13:34:03 +0000120 self.tagpre = Int2AP(random.randint(0, 31999))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000121 self.tagre = re.compile(r'(?P<tag>'
122 + self.tagpre
123 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
124
125 # Get server welcome message,
126 # request and store CAPABILITY response.
127
128 if __debug__ and self.debug >= 1:
129 print '\tnew IMAP4 connection, tag=%s' % self.tagpre
130
131 self.welcome = self._get_response()
132 if self.untagged_responses.has_key('PREAUTH'):
133 self.state = 'AUTH'
134 elif self.untagged_responses.has_key('OK'):
135 self.state = 'NONAUTH'
136# elif self.untagged_responses.has_key('BYE'):
137 else:
138 raise self.error(self.welcome)
139
140 cap = 'CAPABILITY'
141 self._simple_command(cap)
142 if not self.untagged_responses.has_key(cap):
143 raise self.error('no CAPABILITY response from server')
144 self.capabilities = tuple(string.split(self.untagged_responses[cap][-1]))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000145
146 if __debug__ and self.debug >= 3:
147 print '\tCAPABILITIES: %s' % `self.capabilities`
148
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000149 self.PROTOCOL_VERSION = None
150 for version in AllowedVersions:
151 if not version in self.capabilities:
152 continue
153 self.PROTOCOL_VERSION = version
154 break
155 if not self.PROTOCOL_VERSION:
156 raise self.error('server not IMAP4 compliant')
157
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000158
159 def __getattr__(self, attr):
160 """Allow UPPERCASE variants of all following IMAP4 commands."""
161 if Commands.has_key(attr):
162 return eval("self.%s" % string.lower(attr))
163 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
164
165
166 # Public methods
167
168
169 def append(self, mailbox, flags, date_time, message):
170 """Append message to named mailbox.
171
172 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
173 """
174 name = 'APPEND'
175 if flags:
176 flags = '(%s)' % flags
177 else:
178 flags = None
179 if date_time:
180 date_time = Time2Internaldate(date_time)
181 else:
182 date_time = None
Guido van Rossum6884af71998-05-29 13:34:03 +0000183 self.literal = message
184 return self._simple_command(name, mailbox, flags, date_time)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000185
186
187 def authenticate(self, func):
188 """Authenticate command - requires response processing.
189
190 UNIMPLEMENTED
191 """
192 raise self.error('UNIMPLEMENTED')
193
194
195 def check(self):
196 """Checkpoint mailbox on server.
197
198 (typ, [data]) = <instance>.check()
199 """
200 return self._simple_command('CHECK')
201
202
203 def close(self):
204 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000205
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000206 Deleted messages are removed from writable mailbox.
207 This is the recommended command before 'LOGOUT'.
208
209 (typ, [data]) = <instance>.close()
210 """
211 try:
212 try: typ, dat = self._simple_command('CLOSE')
213 except EOFError: typ, dat = None, [None]
214 finally:
215 self.state = 'AUTH'
216 return typ, dat
217
218
219 def copy(self, message_set, new_mailbox):
220 """Copy 'message_set' messages onto end of 'new_mailbox'.
221
222 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
223 """
224 return self._simple_command('COPY', message_set, new_mailbox)
225
226
227 def create(self, mailbox):
228 """Create new mailbox.
229
230 (typ, [data]) = <instance>.create(mailbox)
231 """
232 return self._simple_command('CREATE', mailbox)
233
234
235 def delete(self, mailbox):
236 """Delete old mailbox.
237
238 (typ, [data]) = <instance>.delete(mailbox)
239 """
240 return self._simple_command('DELETE', mailbox)
241
242
243 def expunge(self):
244 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000245
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000246 Generates 'EXPUNGE' response for each deleted message.
247
248 (typ, [data]) = <instance>.expunge()
249
250 'data' is list of 'EXPUNGE'd message numbers in order received.
251 """
252 name = 'EXPUNGE'
253 typ, dat = self._simple_command(name)
254 return self._untagged_response(typ, name)
255
256
257 def fetch(self, message_set, message_parts):
258 """Fetch (parts of) messages.
259
260 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
261
262 'data' are tuples of message part envelope and data.
263 """
264 name = 'FETCH'
265 typ, dat = self._simple_command(name, message_set, message_parts)
266 return self._untagged_response(typ, name)
267
268
269 def list(self, directory='""', pattern='*'):
270 """List mailbox names in directory matching pattern.
271
272 (typ, [data]) = <instance>.list(directory='""', pattern='*')
273
274 'data' is list of LIST responses.
275 """
276 name = 'LIST'
277 typ, dat = self._simple_command(name, directory, pattern)
278 return self._untagged_response(typ, name)
279
280
281 def login(self, user, password):
282 """Identify client using plaintext password.
283
284 (typ, [data]) = <instance>.list(user, password)
285 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000286 typ, dat = self._simple_command('LOGIN', user, password)
287 if typ != 'OK':
288 raise self.error(dat)
289 self.state = 'AUTH'
290 return typ, dat
291
292
293 def logout(self):
294 """Shutdown connection to server.
295
296 (typ, [data]) = <instance>.logout()
297
298 Returns server 'BYE' response.
299 """
300 self.state = 'LOGOUT'
301 try: typ, dat = self._simple_command('LOGOUT')
302 except EOFError: typ, dat = None, [None]
303 self.file.close()
304 self.sock.close()
305 if self.untagged_responses.has_key('BYE'):
306 return 'BYE', self.untagged_responses['BYE']
307 return typ, dat
308
309
310 def lsub(self, directory='""', pattern='*'):
311 """List 'subscribed' mailbox names in directory matching pattern.
312
313 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
314
315 'data' are tuples of message part envelope and data.
316 """
317 name = 'LSUB'
318 typ, dat = self._simple_command(name, directory, pattern)
319 return self._untagged_response(typ, name)
320
321
Guido van Rossum6884af71998-05-29 13:34:03 +0000322 def noop(self):
323 """Send NOOP command.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000324
Guido van Rossum6884af71998-05-29 13:34:03 +0000325 (typ, data) = <instance>.noop()
326 """
327 return self._simple_command('NOOP')
328
329
330 def recent(self):
331 """Return most recent 'RECENT' response if it exists,
332 else prompt server for an update using the 'NOOP' command,
333 and flush all untagged responses.
Guido van Rossum46586821998-05-18 14:39:42 +0000334
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000335 (typ, [data]) = <instance>.recent()
336
337 'data' is None if no new messages,
338 else value of RECENT response.
339 """
340 name = 'RECENT'
341 typ, dat = self._untagged_response('OK', name)
342 if dat[-1]:
343 return typ, dat
Guido van Rossum46586821998-05-18 14:39:42 +0000344 self.untagged_responses = {}
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000345 typ, dat = self._simple_command('NOOP')
346 return self._untagged_response(typ, name)
347
348
349 def rename(self, oldmailbox, newmailbox):
350 """Rename old mailbox name to new.
351
352 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
353 """
354 return self._simple_command('RENAME', oldmailbox, newmailbox)
355
356
357 def response(self, code):
358 """Return data for response 'code' if received, or None.
359
Guido van Rossum46586821998-05-18 14:39:42 +0000360 Old value for response 'code' is cleared.
361
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000362 (code, [data]) = <instance>.response(code)
363 """
Guido van Rossum46586821998-05-18 14:39:42 +0000364 return self._untagged_response(code, code)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000365
366
367 def search(self, charset, criteria):
368 """Search mailbox for matching messages.
369
370 (typ, [data]) = <instance>.search(charset, criteria)
371
372 'data' is space separated list of matching message numbers.
373 """
374 name = 'SEARCH'
375 if charset:
376 charset = 'CHARSET ' + charset
377 typ, dat = self._simple_command(name, charset, criteria)
378 return self._untagged_response(typ, name)
379
380
381 def select(self, mailbox='INBOX', readonly=None):
382 """Select a mailbox.
383
Guido van Rossum46586821998-05-18 14:39:42 +0000384 Flush all untagged responses.
385
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000386 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
387
388 'data' is count of messages in mailbox ('EXISTS' response).
389 """
390 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
Guido van Rossum46586821998-05-18 14:39:42 +0000391 self.untagged_responses = {} # Flush old responses.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000392 if readonly:
393 name = 'EXAMINE'
394 else:
395 name = 'SELECT'
396 typ, dat = self._simple_command(name, mailbox)
397 if typ == 'OK':
398 self.state = 'SELECTED'
399 elif typ == 'NO':
400 self.state = 'AUTH'
401 if not readonly and not self.untagged_responses.has_key('READ-WRITE'):
402 raise self.error('%s is not writable' % mailbox)
403 return typ, self.untagged_responses.get('EXISTS', [None])
404
405
406 def status(self, mailbox, names):
407 """Request named status conditions for mailbox.
408
409 (typ, [data]) = <instance>.status(mailbox, names)
410 """
411 name = 'STATUS'
Guido van Rossumbe14e691998-04-11 03:11:51 +0000412 if self.PROTOCOL_VERSION == 'IMAP4':
413 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000414 typ, dat = self._simple_command(name, mailbox, names)
415 return self._untagged_response(typ, name)
416
417
418 def store(self, message_set, command, flag_list):
419 """Alters flag dispositions for messages in mailbox.
420
421 (typ, [data]) = <instance>.store(message_set, command, flag_list)
422 """
Guido van Rossum46586821998-05-18 14:39:42 +0000423 typ, dat = self._simple_command('STORE', message_set, command, flag_list)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000424 return self._untagged_response(typ, 'FETCH')
425
426
427 def subscribe(self, mailbox):
428 """Subscribe to new mailbox.
429
430 (typ, [data]) = <instance>.subscribe(mailbox)
431 """
432 return self._simple_command('SUBSCRIBE', mailbox)
433
434
Guido van Rossum46586821998-05-18 14:39:42 +0000435 def uid(self, command, *args):
436 """Execute "command arg ..." with messages identified by UID,
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000437 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000438
Guido van Rossum46586821998-05-18 14:39:42 +0000439 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000440
441 Returns response appropriate to 'command'.
442 """
443 name = 'UID'
Guido van Rossum46586821998-05-18 14:39:42 +0000444 typ, dat = apply(self._simple_command, ('UID', command) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000445 if command == 'SEARCH':
446 name = 'SEARCH'
447 else:
448 name = 'FETCH'
449 typ, dat2 = self._untagged_response(typ, name)
450 if dat2[-1]: dat = dat2
451 return typ, dat
452
453
454 def unsubscribe(self, mailbox):
455 """Unsubscribe from old mailbox.
456
457 (typ, [data]) = <instance>.unsubscribe(mailbox)
458 """
459 return self._simple_command('UNSUBSCRIBE', mailbox)
460
461
Guido van Rossum46586821998-05-18 14:39:42 +0000462 def xatom(self, name, *args):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000463 """Allow simple extension commands
464 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000465
Guido van Rossum46586821998-05-18 14:39:42 +0000466 (typ, [data]) = <instance>.xatom(name, arg, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000467 """
468 if name[0] != 'X' or not name in self.capabilities:
469 raise self.error('unknown extension command: %s' % name)
Guido van Rossum46586821998-05-18 14:39:42 +0000470 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000471
472
473
474 # Private methods
475
476
477 def _append_untagged(self, typ, dat):
478
479 if self.untagged_responses.has_key(typ):
480 self.untagged_responses[typ].append(dat)
481 else:
482 self.untagged_responses[typ] = [dat]
483
484 if __debug__ and self.debug >= 5:
485 print '\tuntagged_responses[%s] += %.20s..' % (typ, `dat`)
486
487
Guido van Rossum6884af71998-05-29 13:34:03 +0000488 def _command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000489
490 if self.state not in Commands[name]:
Guido van Rossum6884af71998-05-29 13:34:03 +0000491 self.literal = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000492 raise self.error(
493 'command %s illegal in state %s' % (name, self.state))
494
495 tag = self._new_tag()
496 data = '%s %s' % (tag, name)
Guido van Rossum6884af71998-05-29 13:34:03 +0000497 for d in args:
Guido van Rossum46586821998-05-18 14:39:42 +0000498 if d is None: continue
499 if type(d) is type(''):
500 l = len(string.split(d))
501 else:
502 l = 1
503 if l == 0 or l > 1 and (d[0],d[-1]) not in (('(',')'),('"','"')):
504 data = '%s "%s"' % (data, d)
505 else:
506 data = '%s %s' % (data, d)
Guido van Rossum6884af71998-05-29 13:34:03 +0000507
508 literal = self.literal
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000509 if literal is not None:
Guido van Rossum6884af71998-05-29 13:34:03 +0000510 self.literal = None
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000511 data = '%s {%s}' % (data, len(literal))
512
513 try:
514 self.sock.send('%s%s' % (data, CRLF))
515 except socket.error, val:
516 raise self.abort('socket error: %s' % val)
517
518 if __debug__ and self.debug >= 4:
519 print '\t> %s' % data
520
521 if literal is None:
522 return tag
523
524 # Wait for continuation response
525
526 while self._get_response():
527 if self.tagged_commands[tag]: # BAD/NO?
528 return tag
529
530 # Send literal
531
532 if __debug__ and self.debug >= 4:
533 print '\twrite literal size %s' % len(literal)
534
535 try:
536 self.sock.send(literal)
537 self.sock.send(CRLF)
538 except socket.error, val:
539 raise self.abort('socket error: %s' % val)
540
541 return tag
542
543
544 def _command_complete(self, name, tag):
545 try:
546 typ, data = self._get_tagged_response(tag)
547 except self.abort, val:
548 raise self.abort('command: %s => %s' % (name, val))
549 except self.error, val:
550 raise self.error('command: %s => %s' % (name, val))
551 if self.untagged_responses.has_key('BYE') and name != 'LOGOUT':
552 raise self.abort(self.untagged_responses['BYE'][-1])
553 if typ == 'BAD':
554 raise self.error('%s command error: %s %s' % (name, typ, data))
555 return typ, data
556
557
558 def _get_response(self):
559
560 # Read response and store.
561 #
562 # Returns None for continuation responses,
Guido van Rossum46586821998-05-18 14:39:42 +0000563 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000564
Guido van Rossum46586821998-05-18 14:39:42 +0000565 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000566
567 # Command completion response?
568
569 if self._match(self.tagre, resp):
570 tag = self.mo.group('tag')
571 if not self.tagged_commands.has_key(tag):
572 raise self.abort('unexpected tagged response: %s' % resp)
573
574 typ = self.mo.group('type')
575 dat = self.mo.group('data')
576 self.tagged_commands[tag] = (typ, [dat])
577 else:
578 dat2 = None
579
580 # '*' (untagged) responses?
581
582 if not self._match(Untagged_response, resp):
583 if self._match(Untagged_status, resp):
584 dat2 = self.mo.group('data2')
585
586 if self.mo is None:
587 # Only other possibility is '+' (continuation) rsponse...
588
589 if self._match(Continuation, resp):
590 self.continuation_response = self.mo.group('data')
591 return None # NB: indicates continuation
592
593 raise self.abort('unexpected response: %s' % resp)
594
595 typ = self.mo.group('type')
596 dat = self.mo.group('data')
597 if dat2: dat = dat + ' ' + dat2
598
599 # Is there a literal to come?
600
601 while self._match(Literal, dat):
602
603 # Read literal direct from connection.
604
605 size = string.atoi(self.mo.group('size'))
606 if __debug__ and self.debug >= 4:
607 print '\tread literal size %s' % size
608 data = self.file.read(size)
609
610 # Store response with literal as tuple
611
612 self._append_untagged(typ, (dat, data))
613
614 # Read trailer - possibly containing another literal
615
Guido van Rossum46586821998-05-18 14:39:42 +0000616 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000617
618 self._append_untagged(typ, dat)
619
620 # Bracketed response information?
621
622 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
623 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
624
625 return resp
626
627
628 def _get_tagged_response(self, tag):
629
630 while 1:
631 result = self.tagged_commands[tag]
632 if result is not None:
633 del self.tagged_commands[tag]
634 return result
635 self._get_response()
636
637
638 def _get_line(self):
639
640 line = self.file.readline()
641 if not line:
642 raise EOFError
643
644 # Protocol mandates all lines terminated by CRLF
645
Guido van Rossum46586821998-05-18 14:39:42 +0000646 line = line[:-2]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000647 if __debug__ and self.debug >= 4:
Guido van Rossum46586821998-05-18 14:39:42 +0000648 print '\t< %s' % line
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000649 return line
650
651
652 def _match(self, cre, s):
653
654 # Run compiled regular expression match method on 's'.
655 # Save result, return success.
656
657 self.mo = cre.match(s)
658 if __debug__ and self.mo is not None and self.debug >= 5:
659 print "\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)
660 return self.mo is not None
661
662
663 def _new_tag(self):
664
665 tag = '%s%s' % (self.tagpre, self.tagnum)
666 self.tagnum = self.tagnum + 1
667 self.tagged_commands[tag] = None
668 return tag
669
670
Guido van Rossum46586821998-05-18 14:39:42 +0000671 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000672
Guido van Rossum46586821998-05-18 14:39:42 +0000673 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000674
675
676 def _untagged_response(self, typ, name):
677
678 if not self.untagged_responses.has_key(name):
679 return typ, [None]
680 data = self.untagged_responses[name]
Guido van Rossum46586821998-05-18 14:39:42 +0000681 if __debug__ and self.debug >= 5:
682 print '\tuntagged_responses[%s] => %.20s..' % (name, `data`)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000683 del self.untagged_responses[name]
684 return typ, data
685
686
687
688Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
689 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
690
691def Internaldate2tuple(resp):
692
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000693 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000694
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000695 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000696 """
697
698 mo = InternalDate.match(resp)
699 if not mo:
700 return None
701
702 mon = Mon2num[mo.group('mon')]
703 zonen = mo.group('zonen')
704
705 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
706 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
707
708 # INTERNALDATE timezone must be subtracted to get UT
709
710 zone = (zoneh*60 + zonem)*60
711 if zonen == '-':
712 zone = -zone
713
714 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
715
716 utc = time.mktime(tt)
717
718 # Following is necessary because the time module has no 'mkgmtime'.
719 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
720
721 lt = time.localtime(utc)
722 if time.daylight and lt[-1]:
723 zone = zone + time.altzone
724 else:
725 zone = zone + time.timezone
726
727 return time.localtime(utc - zone)
728
729
730
731def Int2AP(num):
732
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000733 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000734
735 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000736 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000737 while num:
738 num, mod = divmod(num, 16)
739 val = AP[mod] + val
740 return val
741
742
743
744def ParseFlags(resp):
745
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000746 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000747
748 mo = Flags.match(resp)
749 if not mo:
750 return ()
751
752 return tuple(string.split(mo.group('flags')))
753
754
755def Time2Internaldate(date_time):
756
757 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
758
759 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
760 """
761
762 dttype = type(date_time)
763 if dttype is type(1):
764 tt = time.localtime(date_time)
765 elif dttype is type(()):
766 tt = date_time
767 elif dttype is type(""):
768 return date_time # Assume in correct format
769 else: raise ValueError
770
771 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
772 if dt[0] == '0':
773 dt = ' ' + dt[1:]
774 if time.daylight and tt[-1]:
775 zone = -time.altzone
776 else:
777 zone = -time.timezone
778 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
779
780
781
782if __debug__ and __name__ == '__main__':
783
784 import getpass
785 USER = getpass.getuser()
786 PASSWD = getpass.getpass()
787
788 test_seq1 = (
789 ('login', (USER, PASSWD)),
Guido van Rossum46586821998-05-18 14:39:42 +0000790 ('create', ('/tmp/xxx 1',)),
791 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
792 ('CREATE', ('/tmp/yyz 2',)),
793 ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
794 ('select', ('/tmp/yyz 2',)),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000795 ('uid', ('SEARCH', 'ALL')),
796 ('fetch', ('1', '(INTERNALDATE RFC822)')),
797 ('store', ('1', 'FLAGS', '(\Deleted)')),
798 ('expunge', ()),
Guido van Rossum46586821998-05-18 14:39:42 +0000799 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000800 ('close', ()),
801 )
802
803 test_seq2 = (
804 ('select', ()),
805 ('response',('UIDVALIDITY',)),
806 ('uid', ('SEARCH', 'ALL')),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000807 ('response', ('EXISTS',)),
Guido van Rossum46586821998-05-18 14:39:42 +0000808 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000809 ('logout', ()),
810 )
811
812 def run(cmd, args):
813 typ, dat = apply(eval('M.%s' % cmd), args)
814 print ' %s %s\n => %s %s' % (cmd, args, typ, dat)
815 return dat
816
817 Debug = 4
Guido van Rossumbe14e691998-04-11 03:11:51 +0000818 M = IMAP4()
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000819 print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000820
821 for cmd,args in test_seq1:
822 run(cmd, args)
823
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000824 for ml in run('list', ('/tmp/', 'yy%')):
Guido van Rossum46586821998-05-18 14:39:42 +0000825 mo = re.match(r'.*"([^"]+)"$', ml)
826 if mo: path = mo.group(1)
827 else: path = string.split(ml)[-1]
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000828 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000829
830 for cmd,args in test_seq2:
831 dat = run(cmd, args)
832
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000833 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
834 continue
835
836 uid = string.split(dat[0])[-1]
Guido van Rossum46586821998-05-18 14:39:42 +0000837 run('uid', ('FETCH', '%s' % uid,
838 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822)'))