blob: bea54987172611f3443abd48f4f3ad02c38829a9 [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
175 tag = self._command(name, mailbox, flags, date_time, message)
176 return self._command_complete(name, tag)
177
178
179 def authenticate(self, func):
180 """Authenticate command - requires response processing.
181
182 UNIMPLEMENTED
183 """
184 raise self.error('UNIMPLEMENTED')
185
186
187 def check(self):
188 """Checkpoint mailbox on server.
189
190 (typ, [data]) = <instance>.check()
191 """
192 return self._simple_command('CHECK')
193
194
195 def close(self):
196 """Close currently selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000197
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000198 Deleted messages are removed from writable mailbox.
199 This is the recommended command before 'LOGOUT'.
200
201 (typ, [data]) = <instance>.close()
202 """
203 try:
204 try: typ, dat = self._simple_command('CLOSE')
205 except EOFError: typ, dat = None, [None]
206 finally:
207 self.state = 'AUTH'
208 return typ, dat
209
210
211 def copy(self, message_set, new_mailbox):
212 """Copy 'message_set' messages onto end of 'new_mailbox'.
213
214 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
215 """
216 return self._simple_command('COPY', message_set, new_mailbox)
217
218
219 def create(self, mailbox):
220 """Create new mailbox.
221
222 (typ, [data]) = <instance>.create(mailbox)
223 """
224 return self._simple_command('CREATE', mailbox)
225
226
227 def delete(self, mailbox):
228 """Delete old mailbox.
229
230 (typ, [data]) = <instance>.delete(mailbox)
231 """
232 return self._simple_command('DELETE', mailbox)
233
234
235 def expunge(self):
236 """Permanently remove deleted items from selected mailbox.
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000237
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000238 Generates 'EXPUNGE' response for each deleted message.
239
240 (typ, [data]) = <instance>.expunge()
241
242 'data' is list of 'EXPUNGE'd message numbers in order received.
243 """
244 name = 'EXPUNGE'
245 typ, dat = self._simple_command(name)
246 return self._untagged_response(typ, name)
247
248
249 def fetch(self, message_set, message_parts):
250 """Fetch (parts of) messages.
251
252 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
253
254 'data' are tuples of message part envelope and data.
255 """
256 name = 'FETCH'
257 typ, dat = self._simple_command(name, message_set, message_parts)
258 return self._untagged_response(typ, name)
259
260
261 def list(self, directory='""', pattern='*'):
262 """List mailbox names in directory matching pattern.
263
264 (typ, [data]) = <instance>.list(directory='""', pattern='*')
265
266 'data' is list of LIST responses.
267 """
268 name = 'LIST'
269 typ, dat = self._simple_command(name, directory, pattern)
270 return self._untagged_response(typ, name)
271
272
273 def login(self, user, password):
274 """Identify client using plaintext password.
275
276 (typ, [data]) = <instance>.list(user, password)
277 """
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000278 if not 'AUTH-LOGIN' in self.capabilities:
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000279 raise self.error("server doesn't allow LOGIN authorisation")
280 typ, dat = self._simple_command('LOGIN', user, password)
281 if typ != 'OK':
282 raise self.error(dat)
283 self.state = 'AUTH'
284 return typ, dat
285
286
287 def logout(self):
288 """Shutdown connection to server.
289
290 (typ, [data]) = <instance>.logout()
291
292 Returns server 'BYE' response.
293 """
294 self.state = 'LOGOUT'
295 try: typ, dat = self._simple_command('LOGOUT')
296 except EOFError: typ, dat = None, [None]
297 self.file.close()
298 self.sock.close()
299 if self.untagged_responses.has_key('BYE'):
300 return 'BYE', self.untagged_responses['BYE']
301 return typ, dat
302
303
304 def lsub(self, directory='""', pattern='*'):
305 """List 'subscribed' mailbox names in directory matching pattern.
306
307 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
308
309 'data' are tuples of message part envelope and data.
310 """
311 name = 'LSUB'
312 typ, dat = self._simple_command(name, directory, pattern)
313 return self._untagged_response(typ, name)
314
315
316 def recent(self):
317 """Prompt server for an update.
318
319 (typ, [data]) = <instance>.recent()
320
321 'data' is None if no new messages,
322 else value of RECENT response.
323 """
324 name = 'RECENT'
325 typ, dat = self._untagged_response('OK', name)
326 if dat[-1]:
327 return typ, dat
328 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
343 (code, [data]) = <instance>.response(code)
344 """
345 return code, self.untagged_responses.get(code, [None])
346
347
348 def search(self, charset, criteria):
349 """Search mailbox for matching messages.
350
351 (typ, [data]) = <instance>.search(charset, criteria)
352
353 'data' is space separated list of matching message numbers.
354 """
355 name = 'SEARCH'
356 if charset:
357 charset = 'CHARSET ' + charset
358 typ, dat = self._simple_command(name, charset, criteria)
359 return self._untagged_response(typ, name)
360
361
362 def select(self, mailbox='INBOX', readonly=None):
363 """Select a mailbox.
364
365 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
366
367 'data' is count of messages in mailbox ('EXISTS' response).
368 """
369 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
370 # Remove immediately interesting responses
371 for r in ('EXISTS', 'READ-WRITE'):
372 if self.untagged_responses.has_key(r):
373 del self.untagged_responses[r]
374 if readonly:
375 name = 'EXAMINE'
376 else:
377 name = 'SELECT'
378 typ, dat = self._simple_command(name, mailbox)
379 if typ == 'OK':
380 self.state = 'SELECTED'
381 elif typ == 'NO':
382 self.state = 'AUTH'
383 if not readonly and not self.untagged_responses.has_key('READ-WRITE'):
384 raise self.error('%s is not writable' % mailbox)
385 return typ, self.untagged_responses.get('EXISTS', [None])
386
387
388 def status(self, mailbox, names):
389 """Request named status conditions for mailbox.
390
391 (typ, [data]) = <instance>.status(mailbox, names)
392 """
393 name = 'STATUS'
394 typ, dat = self._simple_command(name, mailbox, names)
395 return self._untagged_response(typ, name)
396
397
398 def store(self, message_set, command, flag_list):
399 """Alters flag dispositions for messages in mailbox.
400
401 (typ, [data]) = <instance>.store(message_set, command, flag_list)
402 """
403 command = '%s %s' % (command, flag_list)
404 typ, dat = self._simple_command('STORE', message_set, command)
405 return self._untagged_response(typ, 'FETCH')
406
407
408 def subscribe(self, mailbox):
409 """Subscribe to new mailbox.
410
411 (typ, [data]) = <instance>.subscribe(mailbox)
412 """
413 return self._simple_command('SUBSCRIBE', mailbox)
414
415
416 def uid(self, command, args):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000417 """Execute "command args" with messages identified by UID,
418 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000419
420 (typ, [data]) = <instance>.uid(command, args)
421
422 Returns response appropriate to 'command'.
423 """
424 name = 'UID'
425 typ, dat = self._simple_command('UID', command, args)
426 if command == 'SEARCH':
427 name = 'SEARCH'
428 else:
429 name = 'FETCH'
430 typ, dat2 = self._untagged_response(typ, name)
431 if dat2[-1]: dat = dat2
432 return typ, dat
433
434
435 def unsubscribe(self, mailbox):
436 """Unsubscribe from old mailbox.
437
438 (typ, [data]) = <instance>.unsubscribe(mailbox)
439 """
440 return self._simple_command('UNSUBSCRIBE', mailbox)
441
442
443 def xatom(self, name, arg1=None, arg2=None):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000444 """Allow simple extension commands
445 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000446
447 (typ, [data]) = <instance>.xatom(name, arg1=None, arg2=None)
448 """
449 if name[0] != 'X' or not name in self.capabilities:
450 raise self.error('unknown extension command: %s' % name)
451 return self._simple_command(name, arg1, arg2)
452
453
454
455 # Private methods
456
457
458 def _append_untagged(self, typ, dat):
459
460 if self.untagged_responses.has_key(typ):
461 self.untagged_responses[typ].append(dat)
462 else:
463 self.untagged_responses[typ] = [dat]
464
465 if __debug__ and self.debug >= 5:
466 print '\tuntagged_responses[%s] += %.20s..' % (typ, `dat`)
467
468
469 def _command(self, name, dat1=None, dat2=None, dat3=None, literal=None):
470
471 if self.state not in Commands[name]:
472 raise self.error(
473 'command %s illegal in state %s' % (name, self.state))
474
475 tag = self._new_tag()
476 data = '%s %s' % (tag, name)
477 for d in (dat1, dat2, dat3):
478 if d is not None: data = '%s %s' % (data, d)
479 if literal is not None:
480 data = '%s {%s}' % (data, len(literal))
481
482 try:
483 self.sock.send('%s%s' % (data, CRLF))
484 except socket.error, val:
485 raise self.abort('socket error: %s' % val)
486
487 if __debug__ and self.debug >= 4:
488 print '\t> %s' % data
489
490 if literal is None:
491 return tag
492
493 # Wait for continuation response
494
495 while self._get_response():
496 if self.tagged_commands[tag]: # BAD/NO?
497 return tag
498
499 # Send literal
500
501 if __debug__ and self.debug >= 4:
502 print '\twrite literal size %s' % len(literal)
503
504 try:
505 self.sock.send(literal)
506 self.sock.send(CRLF)
507 except socket.error, val:
508 raise self.abort('socket error: %s' % val)
509
510 return tag
511
512
513 def _command_complete(self, name, tag):
514 try:
515 typ, data = self._get_tagged_response(tag)
516 except self.abort, val:
517 raise self.abort('command: %s => %s' % (name, val))
518 except self.error, val:
519 raise self.error('command: %s => %s' % (name, val))
520 if self.untagged_responses.has_key('BYE') and name != 'LOGOUT':
521 raise self.abort(self.untagged_responses['BYE'][-1])
522 if typ == 'BAD':
523 raise self.error('%s command error: %s %s' % (name, typ, data))
524 return typ, data
525
526
527 def _get_response(self):
528
529 # Read response and store.
530 #
531 # Returns None for continuation responses,
532 # otherwise first response line received
533
534 # Protocol mandates all lines terminated by CRLF.
535
536 resp = self._get_line()[:-2]
537
538 # Command completion response?
539
540 if self._match(self.tagre, resp):
541 tag = self.mo.group('tag')
542 if not self.tagged_commands.has_key(tag):
543 raise self.abort('unexpected tagged response: %s' % resp)
544
545 typ = self.mo.group('type')
546 dat = self.mo.group('data')
547 self.tagged_commands[tag] = (typ, [dat])
548 else:
549 dat2 = None
550
551 # '*' (untagged) responses?
552
553 if not self._match(Untagged_response, resp):
554 if self._match(Untagged_status, resp):
555 dat2 = self.mo.group('data2')
556
557 if self.mo is None:
558 # Only other possibility is '+' (continuation) rsponse...
559
560 if self._match(Continuation, resp):
561 self.continuation_response = self.mo.group('data')
562 return None # NB: indicates continuation
563
564 raise self.abort('unexpected response: %s' % resp)
565
566 typ = self.mo.group('type')
567 dat = self.mo.group('data')
568 if dat2: dat = dat + ' ' + dat2
569
570 # Is there a literal to come?
571
572 while self._match(Literal, dat):
573
574 # Read literal direct from connection.
575
576 size = string.atoi(self.mo.group('size'))
577 if __debug__ and self.debug >= 4:
578 print '\tread literal size %s' % size
579 data = self.file.read(size)
580
581 # Store response with literal as tuple
582
583 self._append_untagged(typ, (dat, data))
584
585 # Read trailer - possibly containing another literal
586
587 dat = self._get_line()[:-2]
588
589 self._append_untagged(typ, dat)
590
591 # Bracketed response information?
592
593 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
594 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
595
596 return resp
597
598
599 def _get_tagged_response(self, tag):
600
601 while 1:
602 result = self.tagged_commands[tag]
603 if result is not None:
604 del self.tagged_commands[tag]
605 return result
606 self._get_response()
607
608
609 def _get_line(self):
610
611 line = self.file.readline()
612 if not line:
613 raise EOFError
614
615 # Protocol mandates all lines terminated by CRLF
616
617 if __debug__ and self.debug >= 4:
618 print '\t< %s' % line[:-2]
619 return line
620
621
622 def _match(self, cre, s):
623
624 # Run compiled regular expression match method on 's'.
625 # Save result, return success.
626
627 self.mo = cre.match(s)
628 if __debug__ and self.mo is not None and self.debug >= 5:
629 print "\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)
630 return self.mo is not None
631
632
633 def _new_tag(self):
634
635 tag = '%s%s' % (self.tagpre, self.tagnum)
636 self.tagnum = self.tagnum + 1
637 self.tagged_commands[tag] = None
638 return tag
639
640
641 def _simple_command(self, name, dat1=None, dat2=None):
642
643 return self._command_complete(name, self._command(name, dat1, dat2))
644
645
646 def _untagged_response(self, typ, name):
647
648 if not self.untagged_responses.has_key(name):
649 return typ, [None]
650 data = self.untagged_responses[name]
651 del self.untagged_responses[name]
652 return typ, data
653
654
655
656Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
657 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
658
659def Internaldate2tuple(resp):
660
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000661 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000662
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000663 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000664 """
665
666 mo = InternalDate.match(resp)
667 if not mo:
668 return None
669
670 mon = Mon2num[mo.group('mon')]
671 zonen = mo.group('zonen')
672
673 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
674 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
675
676 # INTERNALDATE timezone must be subtracted to get UT
677
678 zone = (zoneh*60 + zonem)*60
679 if zonen == '-':
680 zone = -zone
681
682 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
683
684 utc = time.mktime(tt)
685
686 # Following is necessary because the time module has no 'mkgmtime'.
687 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
688
689 lt = time.localtime(utc)
690 if time.daylight and lt[-1]:
691 zone = zone + time.altzone
692 else:
693 zone = zone + time.timezone
694
695 return time.localtime(utc - zone)
696
697
698
699def Int2AP(num):
700
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000701 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000702
703 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000704 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000705 while num:
706 num, mod = divmod(num, 16)
707 val = AP[mod] + val
708 return val
709
710
711
712def ParseFlags(resp):
713
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000714 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000715
716 mo = Flags.match(resp)
717 if not mo:
718 return ()
719
720 return tuple(string.split(mo.group('flags')))
721
722
723def Time2Internaldate(date_time):
724
725 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
726
727 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
728 """
729
730 dttype = type(date_time)
731 if dttype is type(1):
732 tt = time.localtime(date_time)
733 elif dttype is type(()):
734 tt = date_time
735 elif dttype is type(""):
736 return date_time # Assume in correct format
737 else: raise ValueError
738
739 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
740 if dt[0] == '0':
741 dt = ' ' + dt[1:]
742 if time.daylight and tt[-1]:
743 zone = -time.altzone
744 else:
745 zone = -time.timezone
746 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
747
748
749
750if __debug__ and __name__ == '__main__':
751
752 import getpass
753 USER = getpass.getuser()
754 PASSWD = getpass.getpass()
755
756 test_seq1 = (
757 ('login', (USER, PASSWD)),
758 ('create', ('/tmp/xxx',)),
759 ('rename', ('/tmp/xxx', '/tmp/yyy')),
760 ('CREATE', ('/tmp/yyz',)),
761 ('append', ('/tmp/yyz', None, None, 'From: anon@x.y.z\n\ndata...')),
762 ('select', ('/tmp/yyz',)),
763 ('recent', ()),
764 ('uid', ('SEARCH', 'ALL')),
765 ('fetch', ('1', '(INTERNALDATE RFC822)')),
766 ('store', ('1', 'FLAGS', '(\Deleted)')),
767 ('expunge', ()),
768 ('close', ()),
769 )
770
771 test_seq2 = (
772 ('select', ()),
773 ('response',('UIDVALIDITY',)),
774 ('uid', ('SEARCH', 'ALL')),
775 ('recent', ()),
776 ('response', ('EXISTS',)),
777 ('logout', ()),
778 )
779
780 def run(cmd, args):
781 typ, dat = apply(eval('M.%s' % cmd), args)
782 print ' %s %s\n => %s %s' % (cmd, args, typ, dat)
783 return dat
784
785 Debug = 4
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000786 M = IMAP4("newcnri")
787 print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000788
789 for cmd,args in test_seq1:
790 run(cmd, args)
791
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000792 for ml in run('list', ('/tmp/', 'yy%')):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000793 path = string.split(ml)[-1]
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000794 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000795
796 for cmd,args in test_seq2:
797 dat = run(cmd, args)
798
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000799 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
800 continue
801
802 uid = string.split(dat[0])[-1]
803 run('uid', ('FETCH',
804 '%s (FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822)' % uid))