blob: bbca38e5c11a94924c5f81ff03c91ac0ecd2a71a [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 Rossumbe14e691998-04-11 03:11:51 +0000278 if not 'AUTH=LOGIN' in self.capabilities \
279 and not 'AUTH-LOGIN' in self.capabilities:
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000280 raise self.error("server doesn't allow LOGIN authorisation")
281 typ, dat = self._simple_command('LOGIN', user, password)
282 if typ != 'OK':
283 raise self.error(dat)
284 self.state = 'AUTH'
285 return typ, dat
286
287
288 def logout(self):
289 """Shutdown connection to server.
290
291 (typ, [data]) = <instance>.logout()
292
293 Returns server 'BYE' response.
294 """
295 self.state = 'LOGOUT'
296 try: typ, dat = self._simple_command('LOGOUT')
297 except EOFError: typ, dat = None, [None]
298 self.file.close()
299 self.sock.close()
300 if self.untagged_responses.has_key('BYE'):
301 return 'BYE', self.untagged_responses['BYE']
302 return typ, dat
303
304
305 def lsub(self, directory='""', pattern='*'):
306 """List 'subscribed' mailbox names in directory matching pattern.
307
308 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
309
310 'data' are tuples of message part envelope and data.
311 """
312 name = 'LSUB'
313 typ, dat = self._simple_command(name, directory, pattern)
314 return self._untagged_response(typ, name)
315
316
317 def recent(self):
318 """Prompt server for an update.
319
320 (typ, [data]) = <instance>.recent()
321
322 'data' is None if no new messages,
323 else value of RECENT response.
324 """
325 name = 'RECENT'
326 typ, dat = self._untagged_response('OK', name)
327 if dat[-1]:
328 return typ, dat
329 typ, dat = self._simple_command('NOOP')
330 return self._untagged_response(typ, name)
331
332
333 def rename(self, oldmailbox, newmailbox):
334 """Rename old mailbox name to new.
335
336 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
337 """
338 return self._simple_command('RENAME', oldmailbox, newmailbox)
339
340
341 def response(self, code):
342 """Return data for response 'code' if received, or None.
343
344 (code, [data]) = <instance>.response(code)
345 """
346 return code, self.untagged_responses.get(code, [None])
347
348
349 def search(self, charset, criteria):
350 """Search mailbox for matching messages.
351
352 (typ, [data]) = <instance>.search(charset, criteria)
353
354 'data' is space separated list of matching message numbers.
355 """
356 name = 'SEARCH'
357 if charset:
358 charset = 'CHARSET ' + charset
359 typ, dat = self._simple_command(name, charset, criteria)
360 return self._untagged_response(typ, name)
361
362
363 def select(self, mailbox='INBOX', readonly=None):
364 """Select a mailbox.
365
366 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
367
368 'data' is count of messages in mailbox ('EXISTS' response).
369 """
370 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
371 # Remove immediately interesting responses
372 for r in ('EXISTS', 'READ-WRITE'):
373 if self.untagged_responses.has_key(r):
374 del self.untagged_responses[r]
375 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 """
406 command = '%s %s' % (command, flag_list)
407 typ, dat = self._simple_command('STORE', message_set, command)
408 return self._untagged_response(typ, 'FETCH')
409
410
411 def subscribe(self, mailbox):
412 """Subscribe to new mailbox.
413
414 (typ, [data]) = <instance>.subscribe(mailbox)
415 """
416 return self._simple_command('SUBSCRIBE', mailbox)
417
418
419 def uid(self, command, args):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000420 """Execute "command args" with messages identified by UID,
421 rather than message number.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000422
423 (typ, [data]) = <instance>.uid(command, args)
424
425 Returns response appropriate to 'command'.
426 """
427 name = 'UID'
428 typ, dat = self._simple_command('UID', command, args)
429 if command == 'SEARCH':
430 name = 'SEARCH'
431 else:
432 name = 'FETCH'
433 typ, dat2 = self._untagged_response(typ, name)
434 if dat2[-1]: dat = dat2
435 return typ, dat
436
437
438 def unsubscribe(self, mailbox):
439 """Unsubscribe from old mailbox.
440
441 (typ, [data]) = <instance>.unsubscribe(mailbox)
442 """
443 return self._simple_command('UNSUBSCRIBE', mailbox)
444
445
446 def xatom(self, name, arg1=None, arg2=None):
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000447 """Allow simple extension commands
448 notified by server in CAPABILITY response.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000449
450 (typ, [data]) = <instance>.xatom(name, arg1=None, arg2=None)
451 """
452 if name[0] != 'X' or not name in self.capabilities:
453 raise self.error('unknown extension command: %s' % name)
454 return self._simple_command(name, arg1, arg2)
455
456
457
458 # Private methods
459
460
461 def _append_untagged(self, typ, dat):
462
463 if self.untagged_responses.has_key(typ):
464 self.untagged_responses[typ].append(dat)
465 else:
466 self.untagged_responses[typ] = [dat]
467
468 if __debug__ and self.debug >= 5:
469 print '\tuntagged_responses[%s] += %.20s..' % (typ, `dat`)
470
471
472 def _command(self, name, dat1=None, dat2=None, dat3=None, literal=None):
473
474 if self.state not in Commands[name]:
475 raise self.error(
476 'command %s illegal in state %s' % (name, self.state))
477
478 tag = self._new_tag()
479 data = '%s %s' % (tag, name)
480 for d in (dat1, dat2, dat3):
481 if d is not None: data = '%s %s' % (data, d)
482 if literal is not None:
483 data = '%s {%s}' % (data, len(literal))
484
485 try:
486 self.sock.send('%s%s' % (data, CRLF))
487 except socket.error, val:
488 raise self.abort('socket error: %s' % val)
489
490 if __debug__ and self.debug >= 4:
491 print '\t> %s' % data
492
493 if literal is None:
494 return tag
495
496 # Wait for continuation response
497
498 while self._get_response():
499 if self.tagged_commands[tag]: # BAD/NO?
500 return tag
501
502 # Send literal
503
504 if __debug__ and self.debug >= 4:
505 print '\twrite literal size %s' % len(literal)
506
507 try:
508 self.sock.send(literal)
509 self.sock.send(CRLF)
510 except socket.error, val:
511 raise self.abort('socket error: %s' % val)
512
513 return tag
514
515
516 def _command_complete(self, name, tag):
517 try:
518 typ, data = self._get_tagged_response(tag)
519 except self.abort, val:
520 raise self.abort('command: %s => %s' % (name, val))
521 except self.error, val:
522 raise self.error('command: %s => %s' % (name, val))
523 if self.untagged_responses.has_key('BYE') and name != 'LOGOUT':
524 raise self.abort(self.untagged_responses['BYE'][-1])
525 if typ == 'BAD':
526 raise self.error('%s command error: %s %s' % (name, typ, data))
527 return typ, data
528
529
530 def _get_response(self):
531
532 # Read response and store.
533 #
534 # Returns None for continuation responses,
535 # otherwise first response line received
536
537 # Protocol mandates all lines terminated by CRLF.
538
539 resp = self._get_line()[:-2]
540
541 # Command completion response?
542
543 if self._match(self.tagre, resp):
544 tag = self.mo.group('tag')
545 if not self.tagged_commands.has_key(tag):
546 raise self.abort('unexpected tagged response: %s' % resp)
547
548 typ = self.mo.group('type')
549 dat = self.mo.group('data')
550 self.tagged_commands[tag] = (typ, [dat])
551 else:
552 dat2 = None
553
554 # '*' (untagged) responses?
555
556 if not self._match(Untagged_response, resp):
557 if self._match(Untagged_status, resp):
558 dat2 = self.mo.group('data2')
559
560 if self.mo is None:
561 # Only other possibility is '+' (continuation) rsponse...
562
563 if self._match(Continuation, resp):
564 self.continuation_response = self.mo.group('data')
565 return None # NB: indicates continuation
566
567 raise self.abort('unexpected response: %s' % resp)
568
569 typ = self.mo.group('type')
570 dat = self.mo.group('data')
571 if dat2: dat = dat + ' ' + dat2
572
573 # Is there a literal to come?
574
575 while self._match(Literal, dat):
576
577 # Read literal direct from connection.
578
579 size = string.atoi(self.mo.group('size'))
580 if __debug__ and self.debug >= 4:
581 print '\tread literal size %s' % size
582 data = self.file.read(size)
583
584 # Store response with literal as tuple
585
586 self._append_untagged(typ, (dat, data))
587
588 # Read trailer - possibly containing another literal
589
590 dat = self._get_line()[:-2]
591
592 self._append_untagged(typ, dat)
593
594 # Bracketed response information?
595
596 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
597 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
598
599 return resp
600
601
602 def _get_tagged_response(self, tag):
603
604 while 1:
605 result = self.tagged_commands[tag]
606 if result is not None:
607 del self.tagged_commands[tag]
608 return result
609 self._get_response()
610
611
612 def _get_line(self):
613
614 line = self.file.readline()
615 if not line:
616 raise EOFError
617
618 # Protocol mandates all lines terminated by CRLF
619
620 if __debug__ and self.debug >= 4:
621 print '\t< %s' % line[:-2]
622 return line
623
624
625 def _match(self, cre, s):
626
627 # Run compiled regular expression match method on 's'.
628 # Save result, return success.
629
630 self.mo = cre.match(s)
631 if __debug__ and self.mo is not None and self.debug >= 5:
632 print "\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)
633 return self.mo is not None
634
635
636 def _new_tag(self):
637
638 tag = '%s%s' % (self.tagpre, self.tagnum)
639 self.tagnum = self.tagnum + 1
640 self.tagged_commands[tag] = None
641 return tag
642
643
644 def _simple_command(self, name, dat1=None, dat2=None):
645
646 return self._command_complete(name, self._command(name, dat1, dat2))
647
648
649 def _untagged_response(self, typ, name):
650
651 if not self.untagged_responses.has_key(name):
652 return typ, [None]
653 data = self.untagged_responses[name]
654 del self.untagged_responses[name]
655 return typ, data
656
657
658
659Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
660 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
661
662def Internaldate2tuple(resp):
663
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000664 """Convert IMAP4 INTERNALDATE to UT.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000665
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000666 Returns Python time module tuple.
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000667 """
668
669 mo = InternalDate.match(resp)
670 if not mo:
671 return None
672
673 mon = Mon2num[mo.group('mon')]
674 zonen = mo.group('zonen')
675
676 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
677 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
678
679 # INTERNALDATE timezone must be subtracted to get UT
680
681 zone = (zoneh*60 + zonem)*60
682 if zonen == '-':
683 zone = -zone
684
685 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
686
687 utc = time.mktime(tt)
688
689 # Following is necessary because the time module has no 'mkgmtime'.
690 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
691
692 lt = time.localtime(utc)
693 if time.daylight and lt[-1]:
694 zone = zone + time.altzone
695 else:
696 zone = zone + time.timezone
697
698 return time.localtime(utc - zone)
699
700
701
702def Int2AP(num):
703
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000704 """Convert integer to A-P string representation."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000705
706 val = ''; AP = 'ABCDEFGHIJKLMNOP'
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000707 num = int(abs(num))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000708 while num:
709 num, mod = divmod(num, 16)
710 val = AP[mod] + val
711 return val
712
713
714
715def ParseFlags(resp):
716
Guido van Rossumeeec0af1998-04-09 14:20:31 +0000717 """Convert IMAP4 flags response to python tuple."""
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000718
719 mo = Flags.match(resp)
720 if not mo:
721 return ()
722
723 return tuple(string.split(mo.group('flags')))
724
725
726def Time2Internaldate(date_time):
727
728 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
729
730 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
731 """
732
733 dttype = type(date_time)
734 if dttype is type(1):
735 tt = time.localtime(date_time)
736 elif dttype is type(()):
737 tt = date_time
738 elif dttype is type(""):
739 return date_time # Assume in correct format
740 else: raise ValueError
741
742 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
743 if dt[0] == '0':
744 dt = ' ' + dt[1:]
745 if time.daylight and tt[-1]:
746 zone = -time.altzone
747 else:
748 zone = -time.timezone
749 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
750
751
752
753if __debug__ and __name__ == '__main__':
754
755 import getpass
756 USER = getpass.getuser()
757 PASSWD = getpass.getpass()
758
759 test_seq1 = (
760 ('login', (USER, PASSWD)),
761 ('create', ('/tmp/xxx',)),
762 ('rename', ('/tmp/xxx', '/tmp/yyy')),
763 ('CREATE', ('/tmp/yyz',)),
764 ('append', ('/tmp/yyz', None, None, 'From: anon@x.y.z\n\ndata...')),
765 ('select', ('/tmp/yyz',)),
766 ('recent', ()),
767 ('uid', ('SEARCH', 'ALL')),
768 ('fetch', ('1', '(INTERNALDATE RFC822)')),
769 ('store', ('1', 'FLAGS', '(\Deleted)')),
770 ('expunge', ()),
771 ('close', ()),
772 )
773
774 test_seq2 = (
775 ('select', ()),
776 ('response',('UIDVALIDITY',)),
777 ('uid', ('SEARCH', 'ALL')),
778 ('recent', ()),
779 ('response', ('EXISTS',)),
780 ('logout', ()),
781 )
782
783 def run(cmd, args):
784 typ, dat = apply(eval('M.%s' % cmd), args)
785 print ' %s %s\n => %s %s' % (cmd, args, typ, dat)
786 return dat
787
788 Debug = 4
Guido van Rossumbe14e691998-04-11 03:11:51 +0000789 M = IMAP4()
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000790 print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000791
792 for cmd,args in test_seq1:
793 run(cmd, args)
794
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000795 for ml in run('list', ('/tmp/', 'yy%')):
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000796 path = string.split(ml)[-1]
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000797 run('delete', (path,))
Guido van Rossumc2c07fa1998-04-09 13:51:46 +0000798
799 for cmd,args in test_seq2:
800 dat = run(cmd, args)
801
Guido van Rossum38d8f4e1998-04-11 01:22:34 +0000802 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
803 continue
804
805 uid = string.split(dat[0])[-1]
806 run('uid', ('FETCH',
807 '%s (FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822)' % uid))