6 Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
8 Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
11 Public variable: Debug
12 Public functions: Internaldate2tuple
20 import binascii
, re
, socket
, string
, time
, random
, sys
27 AllowedVersions
= ('IMAP4REV1', 'IMAP4') # Most recent first
33 'APPEND': ('AUTH', 'SELECTED'),
34 'AUTHENTICATE': ('NONAUTH',),
35 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
36 'CHECK': ('SELECTED',),
37 'CLOSE': ('SELECTED',),
38 'COPY': ('SELECTED',),
39 'CREATE': ('AUTH', 'SELECTED'),
40 'DELETE': ('AUTH', 'SELECTED'),
41 'EXAMINE': ('AUTH', 'SELECTED'),
42 'EXPUNGE': ('SELECTED',),
43 'FETCH': ('SELECTED',),
44 'LIST': ('AUTH', 'SELECTED'),
45 'LOGIN': ('NONAUTH',),
46 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
47 'LSUB': ('AUTH', 'SELECTED'),
48 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
49 'PARTIAL': ('SELECTED',),
50 'RENAME': ('AUTH', 'SELECTED'),
51 'SEARCH': ('SELECTED',),
52 'SELECT': ('AUTH', 'SELECTED'),
53 'STATUS': ('AUTH', 'SELECTED'),
54 'STORE': ('SELECTED',),
55 'SUBSCRIBE': ('AUTH', 'SELECTED'),
57 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
60 # Patterns to match server responses
62 Continuation
= re
.compile(r
'\+( (?P<data>.*))?')
63 Flags
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
64 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
65 r
'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
66 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
67 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
69 Literal
= re
.compile(r
'(?P<data>.*) {(?P<size>\d+)}$')
70 Response_code
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
71 Untagged_response
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
72 Untagged_status
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
78 """IMAP4 client class.
80 Instantiate with: IMAP4([host[, port]])
82 host - host's name (default: localhost);
83 port - port number (default: standard IMAP4 port).
85 All IMAP4rev1 commands are supported by methods of the same
88 All arguments to commands are converted to strings, except for
89 AUTHENTICATE, and the last argument to APPEND which is passed as
90 an IMAP4 literal. If necessary (the string contains
91 white-space and isn't enclosed with either parentheses or
92 double quotes) each string is quoted.
94 Each command returns a tuple: (type, [data, ...]) where 'type'
95 is usually 'OK' or 'NO', and 'data' is either the text from the
96 tagged response, or untagged results from command.
98 Errors raise the exception class <instance>.error("<reason>").
99 IMAP4 server errors raise <instance>.abort("<reason>"),
100 which is a sub-class of 'error'. Mailbox status changes
101 from READ-WRITE to READ-ONLY raise the exception class
102 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
104 Note: to use this module, you must read the RFCs pertaining
105 to the IMAP4 protocol, as the semantics of the arguments to
106 each IMAP4 command are left to the invoker, not to mention
110 class error(Exception): pass # Logical errors - debug required
111 class abort(error
): pass # Service errors - close and retry
112 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
115 def __init__(self
, host
= '', port
= IMAP4_PORT
):
119 self
.state
= 'LOGOUT'
120 self
.literal
= None # A literal argument to a command
121 self
.tagged_commands
= {} # Tagged commands awaiting response
122 self
.untagged_responses
= {} # {typ: [data, ...], ...}
123 self
.continuation_response
= '' # Last continuation response
126 # Open socket to server.
128 self
.open(host
, port
)
130 # Create unique tag for this session,
131 # and compile tagged response matcher.
133 self
.tagpre
= Int2AP(random
.randint(0, 31999))
134 self
.tagre
= re
.compile(r
'(?P<tag>'
136 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
138 # Get server welcome message,
139 # request and store CAPABILITY response.
141 if __debug__
and self
.debug
>= 1:
142 _mesg('new IMAP4 connection, tag=%s' % self
.tagpre
)
144 self
.welcome
= self
._get
_response
()
145 if self
.untagged_responses
.has_key('PREAUTH'):
147 elif self
.untagged_responses
.has_key('OK'):
148 self
.state
= 'NONAUTH'
149 # elif self.untagged_responses.has_key('BYE'):
151 raise self
.error(self
.welcome
)
154 self
._simple
_command
(cap
)
155 if not self
.untagged_responses
.has_key(cap
):
156 raise self
.error('no CAPABILITY response from server')
157 self
.capabilities
= tuple(string
.split(string
.upper(self
.untagged_responses
[cap
][-1])))
159 if __debug__
and self
.debug
>= 3:
160 _mesg('CAPABILITIES: %s' % `self
.capabilities`
)
162 for version
in AllowedVersions
:
163 if not version
in self
.capabilities
:
165 self
.PROTOCOL_VERSION
= version
168 raise self
.error('server not IMAP4 compliant')
171 def __getattr__(self
, attr
):
172 # Allow UPPERCASE variants of IMAP4 command methods.
173 if Commands
.has_key(attr
):
174 return eval("self.%s" % string
.lower(attr
))
175 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
182 def open(self
, host
, port
):
183 """Setup 'self.sock' and 'self.file'."""
184 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
185 self
.sock
.connect(self
.host
, self
.port
)
186 self
.file = self
.sock
.makefile('r')
190 """Return most recent 'RECENT' responses if any exist,
191 else prompt server for an update using the 'NOOP' command.
193 (typ, [data]) = <instance>.recent()
195 'data' is None if no new messages,
196 else list of RECENT responses, most recent last.
199 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
202 typ
, dat
= self
.noop() # Prod server for response
203 return self
._untagged
_response
(typ
, dat
, name
)
206 def response(self
, code
):
207 """Return data for response 'code' if received, or None.
209 Old value for response 'code' is cleared.
211 (code, [data]) = <instance>.response(code)
213 return self
._untagged
_response
(code
, [None], string
.upper(code
))
217 """Return socket instance used to connect to IMAP4 server.
219 socket = <instance>.socket()
228 def append(self
, mailbox
, flags
, date_time
, message
):
229 """Append message to named mailbox.
231 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
235 if (flags
[0],flags
[-1]) != ('(',')'):
236 flags
= '(%s)' % flags
240 date_time
= Time2Internaldate(date_time
)
243 self
.literal
= message
244 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
247 def authenticate(self
, mechanism
, authobject
):
248 """Authenticate command - requires response processing.
250 'mechanism' specifies which authentication mechanism is to
251 be used - it must appear in <instance>.capabilities in the
252 form AUTH=<mechanism>.
254 'authobject' must be a callable object:
256 data = authobject(response)
258 It will be called to process server continuation responses.
259 It should return data that will be encoded and sent to server.
260 It should return None if the client abort response '*' should
263 mech
= string
.upper(mechanism
)
264 cap
= 'AUTH=%s' % mech
265 if not cap
in self
.capabilities
:
266 raise self
.error("Server doesn't allow %s authentication." % mech
)
267 self
.literal
= _Authenticator(authobject
).process
268 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
270 raise self
.error(dat
[-1])
276 """Checkpoint mailbox on server.
278 (typ, [data]) = <instance>.check()
280 return self
._simple
_command
('CHECK')
284 """Close currently selected mailbox.
286 Deleted messages are removed from writable mailbox.
287 This is the recommended command before 'LOGOUT'.
289 (typ, [data]) = <instance>.close()
292 typ
, dat
= self
._simple
_command
('CLOSE')
298 def copy(self
, message_set
, new_mailbox
):
299 """Copy 'message_set' messages onto end of 'new_mailbox'.
301 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
303 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
306 def create(self
, mailbox
):
307 """Create new mailbox.
309 (typ, [data]) = <instance>.create(mailbox)
311 return self
._simple
_command
('CREATE', mailbox
)
314 def delete(self
, mailbox
):
315 """Delete old mailbox.
317 (typ, [data]) = <instance>.delete(mailbox)
319 return self
._simple
_command
('DELETE', mailbox
)
323 """Permanently remove deleted items from selected mailbox.
325 Generates 'EXPUNGE' response for each deleted message.
327 (typ, [data]) = <instance>.expunge()
329 'data' is list of 'EXPUNGE'd message numbers in order received.
332 typ
, dat
= self
._simple
_command
(name
)
333 return self
._untagged
_response
(typ
, dat
, name
)
336 def fetch(self
, message_set
, message_parts
):
337 """Fetch (parts of) messages.
339 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
341 'data' are tuples of message part envelope and data.
344 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
345 return self
._untagged
_response
(typ
, dat
, name
)
348 def list(self
, directory
='""', pattern
='*'):
349 """List mailbox names in directory matching pattern.
351 (typ, [data]) = <instance>.list(directory='""', pattern='*')
353 'data' is list of LIST responses.
356 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
357 return self
._untagged
_response
(typ
, dat
, name
)
360 def login(self
, user
, password
):
361 """Identify client using plaintext password.
363 (typ, [data]) = <instance>.list(user, password)
365 typ
, dat
= self
._simple
_command
('LOGIN', user
, password
)
367 raise self
.error(dat
[-1])
373 """Shutdown connection to server.
375 (typ, [data]) = <instance>.logout()
377 Returns server 'BYE' response.
379 self
.state
= 'LOGOUT'
380 try: typ
, dat
= self
._simple
_command
('LOGOUT')
381 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
384 if self
.untagged_responses
.has_key('BYE'):
385 return 'BYE', self
.untagged_responses
['BYE']
389 def lsub(self
, directory
='""', pattern
='*'):
390 """List 'subscribed' mailbox names in directory matching pattern.
392 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
394 'data' are tuples of message part envelope and data.
397 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
398 return self
._untagged
_response
(typ
, dat
, name
)
402 """Send NOOP command.
404 (typ, data) = <instance>.noop()
406 if __debug__
and self
.debug
>= 3:
407 _dump_ur(self
.untagged_responses
)
408 return self
._simple
_command
('NOOP')
411 def partial(self
, message_num
, message_part
, start
, length
):
412 """Fetch truncated part of a message.
414 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
416 'data' is tuple of message part envelope and data.
419 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
420 return self
._untagged
_response
(typ
, dat
, 'FETCH')
423 def rename(self
, oldmailbox
, newmailbox
):
424 """Rename old mailbox name to new.
426 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
428 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
431 def search(self
, charset
, criteria
):
432 """Search mailbox for matching messages.
434 (typ, [data]) = <instance>.search(charset, criteria)
436 'data' is space separated list of matching message numbers.
440 charset
= 'CHARSET ' + charset
441 typ
, dat
= self
._simple
_command
(name
, charset
, criteria
)
442 return self
._untagged
_response
(typ
, dat
, name
)
445 def select(self
, mailbox
='INBOX', readonly
=None):
448 Flush all untagged responses.
450 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
452 'data' is count of messages in mailbox ('EXISTS' response).
454 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
455 self
.untagged_responses
= {} # Flush old responses.
460 typ
, dat
= self
._simple
_command
(name
, mailbox
)
462 self
.state
= 'AUTH' # Might have been 'SELECTED'
464 self
.state
= 'SELECTED'
465 if not self
.untagged_responses
.has_key('READ-WRITE') \
467 if __debug__
and self
.debug
>= 1: _dump_ur(self
.untagged_responses
)
468 raise self
.readonly('%s is not writable' % mailbox
)
469 return typ
, self
.untagged_responses
.get('EXISTS', [None])
472 def status(self
, mailbox
, names
):
473 """Request named status conditions for mailbox.
475 (typ, [data]) = <instance>.status(mailbox, names)
478 if self
.PROTOCOL_VERSION
== 'IMAP4':
479 raise self
.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name
)
480 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
481 return self
._untagged
_response
(typ
, dat
, name
)
484 def store(self
, message_set
, command
, flag_list
):
485 """Alters flag dispositions for messages in mailbox.
487 (typ, [data]) = <instance>.store(message_set, command, flag_list)
489 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flag_list
)
490 return self
._untagged
_response
(typ
, dat
, 'FETCH')
493 def subscribe(self
, mailbox
):
494 """Subscribe to new mailbox.
496 (typ, [data]) = <instance>.subscribe(mailbox)
498 return self
._simple
_command
('SUBSCRIBE', mailbox
)
501 def uid(self
, command
, *args
):
502 """Execute "command arg ..." with messages identified by UID,
503 rather than message number.
505 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
507 Returns response appropriate to 'command'.
509 command
= string
.upper(command
)
510 if not Commands
.has_key(command
):
511 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
512 if self
.state
not in Commands
[command
]:
513 raise self
.error('command %s illegal in state %s'
514 % (command
, self
.state
))
516 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
517 if command
== 'SEARCH':
521 return self
._untagged
_response
(typ
, dat
, name
)
524 def unsubscribe(self
, mailbox
):
525 """Unsubscribe from old mailbox.
527 (typ, [data]) = <instance>.unsubscribe(mailbox)
529 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
532 def xatom(self
, name
, *args
):
533 """Allow simple extension commands
534 notified by server in CAPABILITY response.
536 (typ, [data]) = <instance>.xatom(name, arg, ...)
538 if name
[0] != 'X' or not name
in self
.capabilities
:
539 raise self
.error('unknown extension command: %s' % name
)
540 return apply(self
._simple
_command
, (name
,) + args
)
547 def _append_untagged(self
, typ
, dat
):
549 ur
= self
.untagged_responses
550 if __debug__
and self
.debug
>= 5:
551 _mesg('untagged_responses[%s] %s += %s' %
552 (typ
, len(ur
.get(typ
,'')), dat
))
559 def _command(self
, name
, *args
):
561 if self
.state
not in Commands
[name
]:
564 'command %s illegal in state %s' % (name
, self
.state
))
566 for typ
in ('OK', 'NO', 'BAD'):
567 if self
.untagged_responses
.has_key(typ
):
568 del self
.untagged_responses
[typ
]
570 if self
.untagged_responses
.has_key('READ-WRITE') \
571 and self
.untagged_responses
.has_key('READ-ONLY'):
572 del self
.untagged_responses
['READ-WRITE']
573 raise self
.readonly('mailbox status changed to READ-ONLY')
575 tag
= self
._new
_tag
()
576 data
= '%s %s' % (tag
, name
)
578 if d
is None: continue
579 if type(d
) is type(''):
580 l
= len(string
.split(d
))
583 if l
== 0 or l
> 1 and (d
[0],d
[-1]) not in (('(',')'),('"','"')):
584 data
= '%s "%s"' % (data
, d
)
586 data
= '%s %s' % (data
, d
)
588 literal
= self
.literal
589 if literal
is not None:
591 if type(literal
) is type(self
._command
):
595 data
= '%s {%s}' % (data
, len(literal
))
598 self
.sock
.send('%s%s' % (data
, CRLF
))
599 except socket
.error
, val
:
600 raise self
.abort('socket error: %s' % val
)
602 if __debug__
and self
.debug
>= 4:
609 # Wait for continuation response
611 while self
._get
_response
():
612 if self
.tagged_commands
[tag
]: # BAD/NO?
618 literal
= literator(self
.continuation_response
)
620 if __debug__
and self
.debug
>= 4:
621 _mesg('write literal size %s' % len(literal
))
624 self
.sock
.send(literal
)
626 except socket
.error
, val
:
627 raise self
.abort('socket error: %s' % val
)
635 def _command_complete(self
, name
, tag
):
637 typ
, data
= self
._get
_tagged
_response
(tag
)
638 except self
.abort
, val
:
639 raise self
.abort('command: %s => %s' % (name
, val
))
640 except self
.error
, val
:
641 raise self
.error('command: %s => %s' % (name
, val
))
642 if self
.untagged_responses
.has_key('BYE') and name
!= 'LOGOUT':
643 raise self
.abort(self
.untagged_responses
['BYE'][-1])
645 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
649 def _get_response(self
):
651 # Read response and store.
653 # Returns None for continuation responses,
654 # otherwise first response line received.
656 resp
= self
._get
_line
()
658 # Command completion response?
660 if self
._match
(self
.tagre
, resp
):
661 tag
= self
.mo
.group('tag')
662 if not self
.tagged_commands
.has_key(tag
):
663 raise self
.abort('unexpected tagged response: %s' % resp
)
665 typ
= self
.mo
.group('type')
666 dat
= self
.mo
.group('data')
667 self
.tagged_commands
[tag
] = (typ
, [dat
])
671 # '*' (untagged) responses?
673 if not self
._match
(Untagged_response
, resp
):
674 if self
._match
(Untagged_status
, resp
):
675 dat2
= self
.mo
.group('data2')
678 # Only other possibility is '+' (continuation) rsponse...
680 if self
._match
(Continuation
, resp
):
681 self
.continuation_response
= self
.mo
.group('data')
682 return None # NB: indicates continuation
684 raise self
.abort("unexpected response: '%s'" % resp
)
686 typ
= self
.mo
.group('type')
687 dat
= self
.mo
.group('data')
688 if dat
is None: dat
= '' # Null untagged response
689 if dat2
: dat
= dat
+ ' ' + dat2
691 # Is there a literal to come?
693 while self
._match
(Literal
, dat
):
695 # Read literal direct from connection.
697 size
= string
.atoi(self
.mo
.group('size'))
698 if __debug__
and self
.debug
>= 4:
699 _mesg('read literal size %s' % size
)
700 data
= self
.file.read(size
)
702 # Store response with literal as tuple
704 self
._append
_untagged
(typ
, (dat
, data
))
706 # Read trailer - possibly containing another literal
708 dat
= self
._get
_line
()
710 self
._append
_untagged
(typ
, dat
)
712 # Bracketed response information?
714 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
715 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
717 if __debug__
and self
.debug
>= 1 and typ
in ('NO', 'BAD'):
718 _mesg('%s response: %s' % (typ
, dat
))
723 def _get_tagged_response(self
, tag
):
726 result
= self
.tagged_commands
[tag
]
727 if result
is not None:
728 del self
.tagged_commands
[tag
]
735 line
= self
.file.readline()
737 raise self
.abort('socket error: EOF')
739 # Protocol mandates all lines terminated by CRLF
742 if __debug__
and self
.debug
>= 4:
747 def _match(self
, cre
, s
):
749 # Run compiled regular expression match method on 's'.
750 # Save result, return success.
752 self
.mo
= cre
.match(s
)
753 if __debug__
and self
.mo
is not None and self
.debug
>= 5:
754 _mesg("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
755 return self
.mo
is not None
760 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
761 self
.tagnum
= self
.tagnum
+ 1
762 self
.tagged_commands
[tag
] = None
766 def _simple_command(self
, name
, *args
):
768 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
771 def _untagged_response(self
, typ
, dat
, name
):
775 if not self
.untagged_responses
.has_key(name
):
777 data
= self
.untagged_responses
[name
]
778 if __debug__
and self
.debug
>= 5:
779 _mesg('untagged_responses[%s] => %s' % (name
, data
))
780 del self
.untagged_responses
[name
]
785 class _Authenticator
:
787 """Private class to provide en/decoding
788 for base64-based authentication conversation.
791 def __init__(self
, mechinst
):
792 self
.mech
= mechinst
# Callable object to provide/process data
794 def process(self
, data
):
795 ret
= self
.mech(self
.decode(data
))
797 return '*' # Abort conversation
798 return self
.encode(ret
)
800 def encode(self
, inp
):
802 # Invoke binascii.b2a_base64 iteratively with
803 # short even length buffers, strip the trailing
804 # line feed from the result and append. "Even"
805 # means a number that factors to both 6 and 8,
806 # so when it gets to the end of the 8-bit input
807 # there's no partial 6-bit output.
817 e
= binascii
.b2a_base64(t
)
822 def decode(self
, inp
):
825 return binascii
.a2b_base64(inp
)
829 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
830 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
832 def Internaldate2tuple(resp
):
834 """Convert IMAP4 INTERNALDATE to UT.
836 Returns Python time module tuple.
839 mo
= InternalDate
.match(resp
)
843 mon
= Mon2num
[mo
.group('mon')]
844 zonen
= mo
.group('zonen')
846 for name
in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
847 exec "%s = string.atoi(mo.group('%s'))" % (name
, name
)
849 # INTERNALDATE timezone must be subtracted to get UT
851 zone
= (zoneh
*60 + zonem
)*60
855 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
857 utc
= time
.mktime(tt
)
859 # Following is necessary because the time module has no 'mkgmtime'.
860 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
862 lt
= time
.localtime(utc
)
863 if time
.daylight
and lt
[-1]:
864 zone
= zone
+ time
.altzone
866 zone
= zone
+ time
.timezone
868 return time
.localtime(utc
- zone
)
874 """Convert integer to A-P string representation."""
876 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
879 num
, mod
= divmod(num
, 16)
885 def ParseFlags(resp
):
887 """Convert IMAP4 flags response to python tuple."""
889 mo
= Flags
.match(resp
)
893 return tuple(string
.split(mo
.group('flags')))
896 def Time2Internaldate(date_time
):
898 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
900 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
903 dttype
= type(date_time
)
904 if dttype
is type(1):
905 tt
= time
.localtime(date_time
)
906 elif dttype
is type(()):
908 elif dttype
is type(""):
909 return date_time
# Assume in correct format
910 else: raise ValueError
912 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
915 if time
.daylight
and tt
[-1]:
918 zone
= -time
.timezone
919 return '"' + dt
+ " %+02d%02d" % divmod(zone
/60, 60) + '"'
926 # if len(s) > 70: s = '%.70s..' % s
927 sys
.stderr
.write('\t'+s
+'\n')
931 # Dump untagged responses (in `dict').
936 l
= map(lambda x
,j
=j
:'%s: "%s"' % (x
[0], x
[1][0] and j(x
[1], '" "') or ''), l
)
937 _mesg('untagged responses dump:%s%s' % (t
, j(l
, t
)))
941 if __debug__
and __name__
== '__main__':
946 if sys
.argv
[1:]: host
= sys
.argv
[1]
948 USER
= getpass
.getuser()
949 PASSWD
= getpass
.getpass("IMAP password for %s: " % (host
or "localhost"))
952 ('login', (USER
, PASSWD
)),
953 ('create', ('/tmp/xxx 1',)),
954 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
955 ('CREATE', ('/tmp/yyz 2',)),
956 ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
957 ('select', ('/tmp/yyz 2',)),
958 ('search', (None, '(TO zork)')),
959 ('partial', ('1', 'RFC822', 1, 1024)),
960 ('store', ('1', 'FLAGS', '(\Deleted)')),
968 ('response',('UIDVALIDITY',)),
969 ('uid', ('SEARCH', 'ALL')),
970 ('response', ('EXISTS',)),
976 typ
, dat
= apply(eval('M.%s' % cmd
), args
)
977 _mesg(' %s %s\n => %s %s' % (cmd
, args
, typ
, dat
))
982 _mesg('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
984 for cmd
,args
in test_seq1
:
987 for ml
in run('list', ('/tmp/', 'yy%')):
988 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
989 if mo
: path
= mo
.group(1)
990 else: path
= string
.split(ml
)[-1]
991 run('delete', (path
,))
993 for cmd
,args
in test_seq2
:
996 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
999 uid
= string
.split(dat
[-1])[-1]
1000 run('uid', ('FETCH', '%s' % uid
,
1001 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))