blob: 3c45b2e48c57897e6f9aac684015be38df046a21 [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 Rossumeeec0af1998-04-09 14:20:31 +000015import re, socket, string, time, whrandom
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
80 name (in lower-case). Each command returns a tuple: (type, [data, ...])
81 where 'type' is usually 'OK' or 'NO', and 'data' is either the
82 text from the tagged response, or untagged results from command.
83
84 Errors raise the exception class <instance>.error("<reason>").
85 IMAP4 server errors raise <instance>.abort("<reason>"),
86 which is a sub-class of 'error'.
87 """
88
89 class error(Exception): pass # Logical errors - debug required
90 class abort(error): pass # Service errors - close and retry
Guido van Rossumc2c07fa1998-04-09 13:51:46 +000091
92
93 def __init__(self, host = '', port = IMAP4_PORT):
94 self.host = host
95 self.port = port
96 self.debug = Debug
97 self.state = 'LOGOUT'
98 self.tagged_commands = {} # Tagged commands awaiting response
99 self.untagged_responses = {} # {typ: [data, ...], ...}
100 self.continuation_response = '' # Last continuation response
101 self.tagnum = 0
102
103 # Open socket to server.
104
105 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
106 self.sock.connect(self.host, self.port)
107 self.file = self.sock.makefile('r')
108
109 # Create unique tag for this session,
110 # and compile tagged response matcher.
111
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000112 self.tagpre = Int2AP(whrandom.random()*32000)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000113 self.tagre = re.compile(r'(?P<tag>'
114 + self.tagpre
115 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
116
117 # Get server welcome message,
118 # request and store CAPABILITY response.
119
120 if __debug__ and self.debug >= 1:
121 print '\tnew IMAP4 connection, tag=%s' % self.tagpre
122
123 self.welcome = self._get_response()
124 if self.untagged_responses.has_key('PREAUTH'):
125 self.state = 'AUTH'
126 elif self.untagged_responses.has_key('OK'):
127 self.state = 'NONAUTH'
128# elif self.untagged_responses.has_key('BYE'):
129 else:
130 raise self.error(self.welcome)
131
132 cap = 'CAPABILITY'
133 self._simple_command(cap)
134 if not self.untagged_responses.has_key(cap):
135 raise self.error('no CAPABILITY response from server')
136 self.capabilities = tuple(string.split(self.untagged_responses[cap][-1]))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000137
138 if __debug__ and self.debug >= 3:
139 print '\tCAPABILITIES: %s' % `self.capabilities`
140
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000141 self.PROTOCOL_VERSION = None
142 for version in AllowedVersions:
143 if not version in self.capabilities:
144 continue
145 self.PROTOCOL_VERSION = version
146 break
147 if not self.PROTOCOL_VERSION:
148 raise self.error('server not IMAP4 compliant')
149
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000150
151 def __getattr__(self, attr):
152 """Allow UPPERCASE variants of all following IMAP4 commands."""
153 if Commands.has_key(attr):
154 return eval("self.%s" % string.lower(attr))
155 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
156
157
158 # Public methods
159
160
161 def append(self, mailbox, flags, date_time, message):
162 """Append message to named mailbox.
163
164 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
165 """
166 name = 'APPEND'
167 if flags:
168 flags = '(%s)' % flags
169 else:
170 flags = None
171 if date_time:
172 date_time = Time2Internaldate(date_time)
173 else:
174 date_time = None
Guido van Rossum46586821998-05-18 14:39:42 +0000175 return self._simple_command(name, mailbox, flags, date_time, message)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000176
177
178 def authenticate(self, func):
179 """Authenticate command - requires response processing.
180
181 UNIMPLEMENTED
182 """
183 raise self.error('UNIMPLEMENTED')
184
185
186 def check(self):
187 """Checkpoint mailbox on server.
188
189 (typ, [data]) = <instance>.check()
190 """
191 return self._simple_command('CHECK')
192
193
194 def close(self):
195 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000196
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000197 Deleted messages are removed from writable mailbox.
198 This is the recommended command before 'LOGOUT'.
199
200 (typ, [data]) = <instance>.close()
201 """
202 try:
203 try: typ, dat = self._simple_command('CLOSE')
204 except EOFError: typ, dat = None, [None]
205 finally:
206 self.state = 'AUTH'
207 return typ, dat
208
209
210 def copy(self, message_set, new_mailbox):
211 """Copy 'message_set' messages onto end of 'new_mailbox'.
212
213 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
214 """
215 return self._simple_command('COPY', message_set, new_mailbox)
216
217
218 def create(self, mailbox):
219 """Create new mailbox.
220
221 (typ, [data]) = <instance>.create(mailbox)
222 """
223 return self._simple_command('CREATE', mailbox)
224
225
226 def delete(self, mailbox):
227 """Delete old mailbox.
228
229 (typ, [data]) = <instance>.delete(mailbox)
230 """
231 return self._simple_command('DELETE', mailbox)
232
233
234 def expunge(self):
235 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000236
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000237 Generates 'EXPUNGE' response for each deleted message.
238
239 (typ, [data]) = <instance>.expunge()
240
241 'data' is list of 'EXPUNGE'd message numbers in order received.
242 """
243 name = 'EXPUNGE'
244 typ, dat = self._simple_command(name)
245 return self._untagged_response(typ, name)
246
247
248 def fetch(self, message_set, message_parts):
249 """Fetch (parts of) messages.
250
251 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
252
253 'data' are tuples of message part envelope and data.
254 """
255 name = 'FETCH'
256 typ, dat = self._simple_command(name, message_set, message_parts)
257 return self._untagged_response(typ, name)
258
259
260 def list(self, directory='""', pattern='*'):
261 """List mailbox names in directory matching pattern.
262
263 (typ, [data]) = <instance>.list(directory='""', pattern='*')
264
265 'data' is list of LIST responses.
266 """
267 name = 'LIST'
268 typ, dat = self._simple_command(name, directory, pattern)
269 return self._untagged_response(typ, name)
270
271
272 def login(self, user, password):
273 """Identify client using plaintext password.
274
275 (typ, [data]) = <instance>.list(user, password)
276 """
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000277 typ, dat = self._simple_command('LOGIN', user, password)
278 if typ != 'OK':
279 raise self.error(dat)
280 self.state = 'AUTH'
281 return typ, dat
282
283
284 def logout(self):
285 """Shutdown connection to server.
286
287 (typ, [data]) = <instance>.logout()
288
289 Returns server 'BYE' response.
290 """
291 self.state = 'LOGOUT'
292 try: typ, dat = self._simple_command('LOGOUT')
293 except EOFError: typ, dat = None, [None]
294 self.file.close()
295 self.sock.close()
296 if self.untagged_responses.has_key('BYE'):
297 return 'BYE', self.untagged_responses['BYE']
298 return typ, dat
299
300
301 def lsub(self, directory='""', pattern='*'):
302 """List 'subscribed' mailbox names in directory matching pattern.
303
304 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
305
306 'data' are tuples of message part envelope and data.
307 """
308 name = 'LSUB'
309 typ, dat = self._simple_command(name, directory, pattern)
310 return self._untagged_response(typ, name)
311
312
313 def recent(self):
314 """Prompt server for an update.
315
Guido van Rossum46586821998-05-18 14:39:42 +0000316 Flush all untagged responses.
317
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000318 (typ, [data]) = <instance>.recent()
319
320 'data' is None if no new messages,
321 else value of RECENT response.
322 """
323 name = 'RECENT'
324 typ, dat = self._untagged_response('OK', name)
325 if dat[-1]:
326 return typ, dat
Guido van Rossum46586821998-05-18 14:39:42 +0000327 self.untagged_responses = {}
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000328 typ, dat = self._simple_command('NOOP')
329 return self._untagged_response(typ, name)
330
331
332 def rename(self, oldmailbox, newmailbox):
333 """Rename old mailbox name to new.
334
335 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
336 """
337 return self._simple_command('RENAME', oldmailbox, newmailbox)
338
339
340 def response(self, code):
341 """Return data for response 'code' if received, or None.
342
Guido van Rossum46586821998-05-18 14:39:42 +0000343 Old value for response 'code' is cleared.
344
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000345 (code, [data]) = <instance>.response(code)
346 """
Guido van Rossum46586821998-05-18 14:39:42 +0000347 return self._untagged_response(code, code)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000348
349
350 def search(self, charset, criteria):
351 """Search mailbox for matching messages.
352
353 (typ, [data]) = <instance>.search(charset, criteria)
354
355 'data' is space separated list of matching message numbers.
356 """
357 name = 'SEARCH'
358 if charset:
359 charset = 'CHARSET ' + charset
360 typ, dat = self._simple_command(name, charset, criteria)
361 return self._untagged_response(typ, name)
362
363
364 def select(self, mailbox='INBOX', readonly=None):
365 """Select a mailbox.
366
Guido van Rossum46586821998-05-18 14:39:42 +0000367 Flush all untagged responses.
368
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000369 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
370
371 'data' is count of messages in mailbox ('EXISTS' response).
372 """
373 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
Guido van Rossum46586821998-05-18 14:39:42 +0000374 self.untagged_responses = {} # Flush old responses.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000375 if readonly:
376 name = 'EXAMINE'
377 else:
378 name = 'SELECT'
379 typ, dat = self._simple_command(name, mailbox)
380 if typ == 'OK':
381 self.state = 'SELECTED'
382 elif typ == 'NO':
383 self.state = 'AUTH'
384 if not readonly and not self.untagged_responses.has_key('READ-WRITE'):
385 raise self.error('%s is not writable' % mailbox)
386 return typ, self.untagged_responses.get('EXISTS', [None])
387
388
389 def status(self, mailbox, names):
390 """Request named status conditions for mailbox.
391
392 (typ, [data]) = <instance>.status(mailbox, names)
393 """
394 name = 'STATUS'
Guido van Rossumbe14e691998-04-11 03:11:51 +0000395 if self.PROTOCOL_VERSION == 'IMAP4':
396 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000397 typ, dat = self._simple_command(name, mailbox, names)
398 return self._untagged_response(typ, name)
399
400
401 def store(self, message_set, command, flag_list):
402 """Alters flag dispositions for messages in mailbox.
403
404 (typ, [data]) = <instance>.store(message_set, command, flag_list)
405 """
Guido van Rossum46586821998-05-18 14:39:42 +0000406 typ, dat = self._simple_command('STORE', message_set, command, flag_list)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000407 return self._untagged_response(typ, 'FETCH')
408
409
410 def subscribe(self, mailbox):
411 """Subscribe to new mailbox.
412
413 (typ, [data]) = <instance>.subscribe(mailbox)
414 """
415 return self._simple_command('SUBSCRIBE', mailbox)
416
417
Guido van Rossum46586821998-05-18 14:39:42 +0000418 def uid(self, command, *args):
419 """Execute "command arg ..." with messages identified by UID,
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000420 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000421
Guido van Rossum46586821998-05-18 14:39:42 +0000422 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000423
424 Returns response appropriate to 'command'.
425 """
426 name = 'UID'
Guido van Rossum46586821998-05-18 14:39:42 +0000427 typ, dat = apply(self._simple_command, ('UID', command) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000428 if command == 'SEARCH':
429 name = 'SEARCH'
430 else:
431 name = 'FETCH'
432 typ, dat2 = self._untagged_response(typ, name)
433 if dat2[-1]: dat = dat2
434 return typ, dat
435
436
437 def unsubscribe(self, mailbox):
438 """Unsubscribe from old mailbox.
439
440 (typ, [data]) = <instance>.unsubscribe(mailbox)
441 """
442 return self._simple_command('UNSUBSCRIBE', mailbox)
443
444
Guido van Rossum46586821998-05-18 14:39:42 +0000445 def xatom(self, name, *args):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000446 """Allow simple extension commands
447 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000448
Guido van Rossum46586821998-05-18 14:39:42 +0000449 (typ, [data]) = <instance>.xatom(name, arg, ...)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000450 """
451 if name[0] != 'X' or not name in self.capabilities:
452 raise self.error('unknown extension command: %s' % name)
Guido van Rossum46586821998-05-18 14:39:42 +0000453 return apply(self._simple_command, (name,) + args)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000454
455
456
457 # Private methods
458
459
460 def _append_untagged(self, typ, dat):
461
462 if self.untagged_responses.has_key(typ):
463 self.untagged_responses[typ].append(dat)
464 else:
465 self.untagged_responses[typ] = [dat]
466
467 if __debug__ and self.debug >= 5:
468 print '\tuntagged_responses[%s] += %.20s..' % (typ, `dat`)
469
470
471 def _command(self, name, dat1=None, dat2=None, dat3=None, literal=None):
472
473 if self.state not in Commands[name]:
474 raise self.error(
475 'command %s illegal in state %s' % (name, self.state))
476
477 tag = self._new_tag()
478 data = '%s %s' % (tag, name)
479 for d in (dat1, dat2, dat3):
Guido van Rossum46586821998-05-18 14:39:42 +0000480 if d is None: continue
481 if type(d) is type(''):
482 l = len(string.split(d))
483 else:
484 l = 1
485 if l == 0 or l > 1 and (d[0],d[-1]) not in (('(',')'),('"','"')):
486 data = '%s "%s"' % (data, d)
487 else:
488 data = '%s %s' % (data, d)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000489 if literal is not None:
490 data = '%s {%s}' % (data, len(literal))
491
492 try:
493 self.sock.send('%s%s' % (data, CRLF))
494 except socket.error, val:
495 raise self.abort('socket error: %s' % val)
496
497 if __debug__ and self.debug >= 4:
498 print '\t> %s' % data
499
500 if literal is None:
501 return tag
502
503 # Wait for continuation response
504
505 while self._get_response():
506 if self.tagged_commands[tag]: # BAD/NO?
507 return tag
508
509 # Send literal
510
511 if __debug__ and self.debug >= 4:
512 print '\twrite literal size %s' % len(literal)
513
514 try:
515 self.sock.send(literal)
516 self.sock.send(CRLF)
517 except socket.error, val:
518 raise self.abort('socket error: %s' % val)
519
520 return tag
521
522
523 def _command_complete(self, name, tag):
524 try:
525 typ, data = self._get_tagged_response(tag)
526 except self.abort, val:
527 raise self.abort('command: %s => %s' % (name, val))
528 except self.error, val:
529 raise self.error('command: %s => %s' % (name, val))
530 if self.untagged_responses.has_key('BYE') and name != 'LOGOUT':
531 raise self.abort(self.untagged_responses['BYE'][-1])
532 if typ == 'BAD':
533 raise self.error('%s command error: %s %s' % (name, typ, data))
534 return typ, data
535
536
537 def _get_response(self):
538
539 # Read response and store.
540 #
541 # Returns None for continuation responses,
Guido van Rossum46586821998-05-18 14:39:42 +0000542 # otherwise first response line received.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000543
Guido van Rossum46586821998-05-18 14:39:42 +0000544 resp = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000545
546 # Command completion response?
547
548 if self._match(self.tagre, resp):
549 tag = self.mo.group('tag')
550 if not self.tagged_commands.has_key(tag):
551 raise self.abort('unexpected tagged response: %s' % resp)
552
553 typ = self.mo.group('type')
554 dat = self.mo.group('data')
555 self.tagged_commands[tag] = (typ, [dat])
556 else:
557 dat2 = None
558
559 # '*' (untagged) responses?
560
561 if not self._match(Untagged_response, resp):
562 if self._match(Untagged_status, resp):
563 dat2 = self.mo.group('data2')
564
565 if self.mo is None:
566 # Only other possibility is '+' (continuation) rsponse...
567
568 if self._match(Continuation, resp):
569 self.continuation_response = self.mo.group('data')
570 return None # NB: indicates continuation
571
572 raise self.abort('unexpected response: %s' % resp)
573
574 typ = self.mo.group('type')
575 dat = self.mo.group('data')
576 if dat2: dat = dat + ' ' + dat2
577
578 # Is there a literal to come?
579
580 while self._match(Literal, dat):
581
582 # Read literal direct from connection.
583
584 size = string.atoi(self.mo.group('size'))
585 if __debug__ and self.debug >= 4:
586 print '\tread literal size %s' % size
587 data = self.file.read(size)
588
589 # Store response with literal as tuple
590
591 self._append_untagged(typ, (dat, data))
592
593 # Read trailer - possibly containing another literal
594
Guido van Rossum46586821998-05-18 14:39:42 +0000595 dat = self._get_line()
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000596
597 self._append_untagged(typ, dat)
598
599 # Bracketed response information?
600
601 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
602 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
603
604 return resp
605
606
607 def _get_tagged_response(self, tag):
608
609 while 1:
610 result = self.tagged_commands[tag]
611 if result is not None:
612 del self.tagged_commands[tag]
613 return result
614 self._get_response()
615
616
617 def _get_line(self):
618
619 line = self.file.readline()
620 if not line:
621 raise EOFError
622
623 # Protocol mandates all lines terminated by CRLF
624
Guido van Rossum46586821998-05-18 14:39:42 +0000625 line = line[:-2]
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000626 if __debug__ and self.debug >= 4:
Guido van Rossum46586821998-05-18 14:39:42 +0000627 print '\t< %s' % line
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000628 return line
629
630
631 def _match(self, cre, s):
632
633 # Run compiled regular expression match method on 's'.
634 # Save result, return success.
635
636 self.mo = cre.match(s)
637 if __debug__ and self.mo is not None and self.debug >= 5:
638 print "\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)
639 return self.mo is not None
640
641
642 def _new_tag(self):
643
644 tag = '%s%s' % (self.tagpre, self.tagnum)
645 self.tagnum = self.tagnum + 1
646 self.tagged_commands[tag] = None
647 return tag
648
649
Guido van Rossum46586821998-05-18 14:39:42 +0000650 def _simple_command(self, name, *args):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000651
Guido van Rossum46586821998-05-18 14:39:42 +0000652 return self._command_complete(name, apply(self._command, (name,) + args))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000653
654
655 def _untagged_response(self, typ, name):
656
657 if not self.untagged_responses.has_key(name):
658 return typ, [None]
659 data = self.untagged_responses[name]
Guido van Rossum46586821998-05-18 14:39:42 +0000660 if __debug__ and self.debug >= 5:
661 print '\tuntagged_responses[%s] => %.20s..' % (name, `data`)
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000662 del self.untagged_responses[name]
663 return typ, data
664
665
666
667Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
668 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
669
670def Internaldate2tuple(resp):
671
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000672 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000673
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000674 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000675 """
676
677 mo = InternalDate.match(resp)
678 if not mo:
679 return None
680
681 mon = Mon2num[mo.group('mon')]
682 zonen = mo.group('zonen')
683
684 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
685 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
686
687 # INTERNALDATE timezone must be subtracted to get UT
688
689 zone = (zoneh*60 + zonem)*60
690 if zonen == '-':
691 zone = -zone
692
693 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
694
695 utc = time.mktime(tt)
696
697 # Following is necessary because the time module has no 'mkgmtime'.
698 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
699
700 lt = time.localtime(utc)
701 if time.daylight and lt[-1]:
702 zone = zone + time.altzone
703 else:
704 zone = zone + time.timezone
705
706 return time.localtime(utc - zone)
707
708
709
710def Int2AP(num):
711
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000712 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000713
714 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000715 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000716 while num:
717 num, mod = divmod(num, 16)
718 val = AP[mod] + val
719 return val
720
721
722
723def ParseFlags(resp):
724
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000725 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000726
727 mo = Flags.match(resp)
728 if not mo:
729 return ()
730
731 return tuple(string.split(mo.group('flags')))
732
733
734def Time2Internaldate(date_time):
735
736 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
737
738 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
739 """
740
741 dttype = type(date_time)
742 if dttype is type(1):
743 tt = time.localtime(date_time)
744 elif dttype is type(()):
745 tt = date_time
746 elif dttype is type(""):
747 return date_time # Assume in correct format
748 else: raise ValueError
749
750 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
751 if dt[0] == '0':
752 dt = ' ' + dt[1:]
753 if time.daylight and tt[-1]:
754 zone = -time.altzone
755 else:
756 zone = -time.timezone
757 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
758
759
760
761if __debug__ and __name__ == '__main__':
762
763 import getpass
764 USER = getpass.getuser()
765 PASSWD = getpass.getpass()
766
767 test_seq1 = (
768 ('login', (USER, PASSWD)),
Guido van Rossum46586821998-05-18 14:39:42 +0000769 ('create', ('/tmp/xxx 1',)),
770 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
771 ('CREATE', ('/tmp/yyz 2',)),
772 ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
773 ('select', ('/tmp/yyz 2',)),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000774 ('uid', ('SEARCH', 'ALL')),
775 ('fetch', ('1', '(INTERNALDATE RFC822)')),
776 ('store', ('1', 'FLAGS', '(\Deleted)')),
777 ('expunge', ()),
Guido van Rossum46586821998-05-18 14:39:42 +0000778 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000779 ('close', ()),
780 )
781
782 test_seq2 = (
783 ('select', ()),
784 ('response',('UIDVALIDITY',)),
785 ('uid', ('SEARCH', 'ALL')),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000786 ('response', ('EXISTS',)),
Guido van Rossum46586821998-05-18 14:39:42 +0000787 ('recent', ()),
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000788 ('logout', ()),
789 )
790
791 def run(cmd, args):
792 typ, dat = apply(eval('M.%s' % cmd), args)
793 print ' %s %s\n => %s %s' % (cmd, args, typ, dat)
794 return dat
795
796 Debug = 4
Guido van Rossumbe14e691998-04-11 03:11:51 +0000797 M = IMAP4()
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000798 print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000799
800 for cmd,args in test_seq1:
801 run(cmd, args)
802
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000803 for ml in run('list', ('/tmp/', 'yy%')):
Guido van Rossum46586821998-05-18 14:39:42 +0000804 mo = re.match(r'.*"([^"]+)"$', ml)
805 if mo: path = mo.group(1)
806 else: path = string.split(ml)[-1]
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000807 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000808
809 for cmd,args in test_seq2:
810 dat = run(cmd, args)
811
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000812 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
813 continue
814
815 uid = string.split(dat[0])[-1]
Guido van Rossum46586821998-05-18 14:39:42 +0000816 run('uid', ('FETCH', '%s' % uid,
817 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822)'))