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. However, the 'password'
93 argument to the LOGIN command is always quoted.
95 Each command returns a tuple: (type, [data, ...]) where 'type'
96 is usually 'OK' or 'NO', and 'data' is either the text from the
97 tagged response, or untagged results from command.
99 Errors raise the exception class <instance>.error("<reason>").
100 IMAP4 server errors raise <instance>.abort("<reason>"),
101 which is a sub-class of 'error'. Mailbox status changes
102 from READ-WRITE to READ-ONLY raise the exception class
103 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
105 "error" exceptions imply a program error.
106 "abort" exceptions imply the connection should be reset, and
107 the command re-tried.
108 "readonly" exceptions imply the command should be re-tried.
110 Note: to use this module, you must read the RFCs pertaining
111 to the IMAP4 protocol, as the semantics of the arguments to
112 each IMAP4 command are left to the invoker, not to mention
116 class error(Exception): pass # Logical errors - debug required
117 class abort(error
): pass # Service errors - close and retry
118 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
120 mustquote
= re
.compile(r
'\W') # Match any non-alphanumeric character
122 def __init__(self
, host
= '', port
= IMAP4_PORT
):
126 self
.state
= 'LOGOUT'
127 self
.literal
= None # A literal argument to a command
128 self
.tagged_commands
= {} # Tagged commands awaiting response
129 self
.untagged_responses
= {} # {typ: [data, ...], ...}
130 self
.continuation_response
= '' # Last continuation response
133 # Open socket to server.
135 self
.open(host
, port
)
137 # Create unique tag for this session,
138 # and compile tagged response matcher.
140 self
.tagpre
= Int2AP(random
.randint(0, 31999))
141 self
.tagre
= re
.compile(r
'(?P<tag>'
143 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
145 # Get server welcome message,
146 # request and store CAPABILITY response.
150 _mesg('new IMAP4 connection, tag=%s' % self
.tagpre
)
152 self
.welcome
= self
._get
_response
()
153 if self
.untagged_responses
.has_key('PREAUTH'):
155 elif self
.untagged_responses
.has_key('OK'):
156 self
.state
= 'NONAUTH'
158 raise self
.error(self
.welcome
)
161 self
._simple
_command
(cap
)
162 if not self
.untagged_responses
.has_key(cap
):
163 raise self
.error('no CAPABILITY response from server')
164 self
.capabilities
= tuple(string
.split(string
.upper(self
.untagged_responses
[cap
][-1])))
168 _mesg('CAPABILITIES: %s' % `self
.capabilities`
)
170 for version
in AllowedVersions
:
171 if not version
in self
.capabilities
:
173 self
.PROTOCOL_VERSION
= version
176 raise self
.error('server not IMAP4 compliant')
179 def __getattr__(self
, attr
):
180 # Allow UPPERCASE variants of IMAP4 command methods.
181 if Commands
.has_key(attr
):
182 return eval("self.%s" % string
.lower(attr
))
183 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
190 def open(self
, host
, port
):
191 """Setup 'self.sock' and 'self.file'."""
192 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
193 self
.sock
.connect(self
.host
, self
.port
)
194 self
.file = self
.sock
.makefile('r')
198 """Return most recent 'RECENT' responses if any exist,
199 else prompt server for an update using the 'NOOP' command.
201 (typ, [data]) = <instance>.recent()
203 'data' is None if no new messages,
204 else list of RECENT responses, most recent last.
207 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
210 typ
, dat
= self
.noop() # Prod server for response
211 return self
._untagged
_response
(typ
, dat
, name
)
214 def response(self
, code
):
215 """Return data for response 'code' if received, or None.
217 Old value for response 'code' is cleared.
219 (code, [data]) = <instance>.response(code)
221 return self
._untagged
_response
(code
, [None], string
.upper(code
))
225 """Return socket instance used to connect to IMAP4 server.
227 socket = <instance>.socket()
236 def append(self
, mailbox
, flags
, date_time
, message
):
237 """Append message to named mailbox.
239 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
241 All args except `message' can be None.
247 if (flags
[0],flags
[-1]) != ('(',')'):
248 flags
= '(%s)' % flags
252 date_time
= Time2Internaldate(date_time
)
255 self
.literal
= message
256 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
259 def authenticate(self
, mechanism
, authobject
):
260 """Authenticate command - requires response processing.
262 'mechanism' specifies which authentication mechanism is to
263 be used - it must appear in <instance>.capabilities in the
264 form AUTH=<mechanism>.
266 'authobject' must be a callable object:
268 data = authobject(response)
270 It will be called to process server continuation responses.
271 It should return data that will be encoded and sent to server.
272 It should return None if the client abort response '*' should
275 mech
= string
.upper(mechanism
)
276 cap
= 'AUTH=%s' % mech
277 if not cap
in self
.capabilities
:
278 raise self
.error("Server doesn't allow %s authentication." % mech
)
279 self
.literal
= _Authenticator(authobject
).process
280 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
282 raise self
.error(dat
[-1])
288 """Checkpoint mailbox on server.
290 (typ, [data]) = <instance>.check()
292 return self
._simple
_command
('CHECK')
296 """Close currently selected mailbox.
298 Deleted messages are removed from writable mailbox.
299 This is the recommended command before 'LOGOUT'.
301 (typ, [data]) = <instance>.close()
304 typ
, dat
= self
._simple
_command
('CLOSE')
310 def copy(self
, message_set
, new_mailbox
):
311 """Copy 'message_set' messages onto end of 'new_mailbox'.
313 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
315 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
318 def create(self
, mailbox
):
319 """Create new mailbox.
321 (typ, [data]) = <instance>.create(mailbox)
323 return self
._simple
_command
('CREATE', mailbox
)
326 def delete(self
, mailbox
):
327 """Delete old mailbox.
329 (typ, [data]) = <instance>.delete(mailbox)
331 return self
._simple
_command
('DELETE', mailbox
)
335 """Permanently remove deleted items from selected mailbox.
337 Generates 'EXPUNGE' response for each deleted message.
339 (typ, [data]) = <instance>.expunge()
341 'data' is list of 'EXPUNGE'd message numbers in order received.
344 typ
, dat
= self
._simple
_command
(name
)
345 return self
._untagged
_response
(typ
, dat
, name
)
348 def fetch(self
, message_set
, message_parts
):
349 """Fetch (parts of) messages.
351 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
353 'data' are tuples of message part envelope and data.
356 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
357 return self
._untagged
_response
(typ
, dat
, name
)
360 def list(self
, directory
='""', pattern
='*'):
361 """List mailbox names in directory matching pattern.
363 (typ, [data]) = <instance>.list(directory='""', pattern='*')
365 'data' is list of LIST responses.
368 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
369 return self
._untagged
_response
(typ
, dat
, name
)
372 def login(self
, user
, password
):
373 """Identify client using plaintext password.
375 (typ, [data]) = <instance>.login(user, password)
377 NB: 'password' will be quoted.
379 #if not 'AUTH=LOGIN' in self.capabilities:
380 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
381 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
383 raise self
.error(dat
[-1])
389 """Shutdown connection to server.
391 (typ, [data]) = <instance>.logout()
393 Returns server 'BYE' response.
395 self
.state
= 'LOGOUT'
396 try: typ
, dat
= self
._simple
_command
('LOGOUT')
397 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
400 if self
.untagged_responses
.has_key('BYE'):
401 return 'BYE', self
.untagged_responses
['BYE']
405 def lsub(self
, directory
='""', pattern
='*'):
406 """List 'subscribed' mailbox names in directory matching pattern.
408 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
410 'data' are tuples of message part envelope and data.
413 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
414 return self
._untagged
_response
(typ
, dat
, name
)
418 """Send NOOP command.
420 (typ, data) = <instance>.noop()
424 _dump_ur(self
.untagged_responses
)
425 return self
._simple
_command
('NOOP')
428 def partial(self
, message_num
, message_part
, start
, length
):
429 """Fetch truncated part of a message.
431 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
433 'data' is tuple of message part envelope and data.
436 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
437 return self
._untagged
_response
(typ
, dat
, 'FETCH')
440 def rename(self
, oldmailbox
, newmailbox
):
441 """Rename old mailbox name to new.
443 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
445 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
448 def search(self
, charset
, criteria
):
449 """Search mailbox for matching messages.
451 (typ, [data]) = <instance>.search(charset, criteria)
453 'data' is space separated list of matching message numbers.
457 charset
= 'CHARSET ' + charset
458 typ
, dat
= self
._simple
_command
(name
, charset
, criteria
)
459 return self
._untagged
_response
(typ
, dat
, name
)
462 def select(self
, mailbox
='INBOX', readonly
=None):
465 Flush all untagged responses.
467 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
469 'data' is count of messages in mailbox ('EXISTS' response).
471 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
472 self
.untagged_responses
= {} # Flush old responses.
477 typ
, dat
= self
._simple
_command
(name
, mailbox
)
479 self
.state
= 'AUTH' # Might have been 'SELECTED'
481 self
.state
= 'SELECTED'
482 if not self
.untagged_responses
.has_key('READ-WRITE') \
486 _dump_ur(self
.untagged_responses
)
487 raise self
.readonly('%s is not writable' % mailbox
)
488 return typ
, self
.untagged_responses
.get('EXISTS', [None])
491 def status(self
, mailbox
, names
):
492 """Request named status conditions for mailbox.
494 (typ, [data]) = <instance>.status(mailbox, names)
497 if self
.PROTOCOL_VERSION
== 'IMAP4':
498 raise self
.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name
)
499 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
500 return self
._untagged
_response
(typ
, dat
, name
)
503 def store(self
, message_set
, command
, flag_list
):
504 """Alters flag dispositions for messages in mailbox.
506 (typ, [data]) = <instance>.store(message_set, command, flag_list)
508 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flag_list
)
509 return self
._untagged
_response
(typ
, dat
, 'FETCH')
512 def subscribe(self
, mailbox
):
513 """Subscribe to new mailbox.
515 (typ, [data]) = <instance>.subscribe(mailbox)
517 return self
._simple
_command
('SUBSCRIBE', mailbox
)
520 def uid(self
, command
, *args
):
521 """Execute "command arg ..." with messages identified by UID,
522 rather than message number.
524 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
526 Returns response appropriate to 'command'.
528 command
= string
.upper(command
)
529 if not Commands
.has_key(command
):
530 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
531 if self
.state
not in Commands
[command
]:
532 raise self
.error('command %s illegal in state %s'
533 % (command
, self
.state
))
535 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
536 if command
== 'SEARCH':
540 return self
._untagged
_response
(typ
, dat
, name
)
543 def unsubscribe(self
, mailbox
):
544 """Unsubscribe from old mailbox.
546 (typ, [data]) = <instance>.unsubscribe(mailbox)
548 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
551 def xatom(self
, name
, *args
):
552 """Allow simple extension commands
553 notified by server in CAPABILITY response.
555 (typ, [data]) = <instance>.xatom(name, arg, ...)
557 if name
[0] != 'X' or not name
in self
.capabilities
:
558 raise self
.error('unknown extension command: %s' % name
)
559 return apply(self
._simple
_command
, (name
,) + args
)
566 def _append_untagged(self
, typ
, dat
):
568 if dat
is None: dat
= ''
569 ur
= self
.untagged_responses
572 _mesg('untagged_responses[%s] %s += ["%s"]' %
573 (typ
, len(ur
.get(typ
,'')), dat
))
580 def _check_bye(self
):
581 bye
= self
.untagged_responses
.get('BYE')
583 raise self
.abort(bye
[-1])
586 def _command(self
, name
, *args
):
588 if self
.state
not in Commands
[name
]:
591 'command %s illegal in state %s' % (name
, self
.state
))
593 for typ
in ('OK', 'NO', 'BAD'):
594 if self
.untagged_responses
.has_key(typ
):
595 del self
.untagged_responses
[typ
]
597 if self
.untagged_responses
.has_key('READ-WRITE') \
598 and self
.untagged_responses
.has_key('READ-ONLY'):
599 del self
.untagged_responses
['READ-WRITE']
600 raise self
.readonly('mailbox status changed to READ-ONLY')
602 tag
= self
._new
_tag
()
603 data
= '%s %s' % (tag
, name
)
605 if arg
is None: continue
606 data
= '%s %s' % (data
, self
._checkquote
(arg
))
608 literal
= self
.literal
609 if literal
is not None:
611 if type(literal
) is type(self
._command
):
615 data
= '%s {%s}' % (data
, len(literal
))
624 self
.sock
.send('%s%s' % (data
, CRLF
))
625 except socket
.error
, val
:
626 raise self
.abort('socket error: %s' % val
)
632 # Wait for continuation response
634 while self
._get
_response
():
635 if self
.tagged_commands
[tag
]: # BAD/NO?
641 literal
= literator(self
.continuation_response
)
645 _mesg('write literal size %s' % len(literal
))
648 self
.sock
.send(literal
)
650 except socket
.error
, val
:
651 raise self
.abort('socket error: %s' % val
)
659 def _command_complete(self
, name
, tag
):
662 typ
, data
= self
._get
_tagged
_response
(tag
)
663 except self
.abort
, val
:
664 raise self
.abort('command: %s => %s' % (name
, val
))
665 except self
.error
, val
:
666 raise self
.error('command: %s => %s' % (name
, val
))
669 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
673 def _get_response(self
):
675 # Read response and store.
677 # Returns None for continuation responses,
678 # otherwise first response line received.
680 resp
= self
._get
_line
()
682 # Command completion response?
684 if self
._match
(self
.tagre
, resp
):
685 tag
= self
.mo
.group('tag')
686 if not self
.tagged_commands
.has_key(tag
):
687 raise self
.abort('unexpected tagged response: %s' % resp
)
689 typ
= self
.mo
.group('type')
690 dat
= self
.mo
.group('data')
691 self
.tagged_commands
[tag
] = (typ
, [dat
])
695 # '*' (untagged) responses?
697 if not self
._match
(Untagged_response
, resp
):
698 if self
._match
(Untagged_status
, resp
):
699 dat2
= self
.mo
.group('data2')
702 # Only other possibility is '+' (continuation) rsponse...
704 if self
._match
(Continuation
, resp
):
705 self
.continuation_response
= self
.mo
.group('data')
706 return None # NB: indicates continuation
708 raise self
.abort("unexpected response: '%s'" % resp
)
710 typ
= self
.mo
.group('type')
711 dat
= self
.mo
.group('data')
712 if dat
is None: dat
= '' # Null untagged response
713 if dat2
: dat
= dat
+ ' ' + dat2
715 # Is there a literal to come?
717 while self
._match
(Literal
, dat
):
719 # Read literal direct from connection.
721 size
= string
.atoi(self
.mo
.group('size'))
724 _mesg('read literal size %s' % size
)
725 data
= self
.file.read(size
)
727 # Store response with literal as tuple
729 self
._append
_untagged
(typ
, (dat
, data
))
731 # Read trailer - possibly containing another literal
733 dat
= self
._get
_line
()
735 self
._append
_untagged
(typ
, dat
)
737 # Bracketed response information?
739 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
740 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
743 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
744 _mesg('%s response: %s' % (typ
, dat
))
749 def _get_tagged_response(self
, tag
):
752 result
= self
.tagged_commands
[tag
]
753 if result
is not None:
754 del self
.tagged_commands
[tag
]
761 line
= self
.file.readline()
763 raise self
.abort('socket error: EOF')
765 # Protocol mandates all lines terminated by CRLF
776 def _match(self
, cre
, s
):
778 # Run compiled regular expression match method on 's'.
779 # Save result, return success.
781 self
.mo
= cre
.match(s
)
783 if self
.mo
is not None and self
.debug
>= 5:
784 _mesg("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
785 return self
.mo
is not None
790 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
791 self
.tagnum
= self
.tagnum
+ 1
792 self
.tagged_commands
[tag
] = None
796 def _checkquote(self
, arg
):
798 # Must quote command args if non-alphanumeric chars present,
799 # and not already quoted.
801 if type(arg
) is not type(''):
803 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
805 if self
.mustquote
.search(arg
) is None:
807 return self
._quote
(arg
)
810 def _quote(self
, arg
):
812 arg
= string
.replace(arg
, '\\', '\\\\')
813 arg
= string
.replace(arg
, '"', '\\"')
818 def _simple_command(self
, name
, *args
):
820 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
823 def _untagged_response(self
, typ
, dat
, name
):
827 if not self
.untagged_responses
.has_key(name
):
829 data
= self
.untagged_responses
[name
]
832 _mesg('untagged_responses[%s] => %s' % (name
, data
))
833 del self
.untagged_responses
[name
]
838 class _Authenticator
:
840 """Private class to provide en/decoding
841 for base64-based authentication conversation.
844 def __init__(self
, mechinst
):
845 self
.mech
= mechinst
# Callable object to provide/process data
847 def process(self
, data
):
848 ret
= self
.mech(self
.decode(data
))
850 return '*' # Abort conversation
851 return self
.encode(ret
)
853 def encode(self
, inp
):
855 # Invoke binascii.b2a_base64 iteratively with
856 # short even length buffers, strip the trailing
857 # line feed from the result and append. "Even"
858 # means a number that factors to both 6 and 8,
859 # so when it gets to the end of the 8-bit input
860 # there's no partial 6-bit output.
870 e
= binascii
.b2a_base64(t
)
875 def decode(self
, inp
):
878 return binascii
.a2b_base64(inp
)
882 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
883 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
885 def Internaldate2tuple(resp
):
887 """Convert IMAP4 INTERNALDATE to UT.
889 Returns Python time module tuple.
892 mo
= InternalDate
.match(resp
)
896 mon
= Mon2num
[mo
.group('mon')]
897 zonen
= mo
.group('zonen')
899 for name
in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
900 exec "%s = string.atoi(mo.group('%s'))" % (name
, name
)
902 # INTERNALDATE timezone must be subtracted to get UT
904 zone
= (zoneh
*60 + zonem
)*60
908 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
910 utc
= time
.mktime(tt
)
912 # Following is necessary because the time module has no 'mkgmtime'.
913 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
915 lt
= time
.localtime(utc
)
916 if time
.daylight
and lt
[-1]:
917 zone
= zone
+ time
.altzone
919 zone
= zone
+ time
.timezone
921 return time
.localtime(utc
- zone
)
927 """Convert integer to A-P string representation."""
929 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
932 num
, mod
= divmod(num
, 16)
938 def ParseFlags(resp
):
940 """Convert IMAP4 flags response to python tuple."""
942 mo
= Flags
.match(resp
)
946 return tuple(string
.split(mo
.group('flags')))
949 def Time2Internaldate(date_time
):
951 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
953 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
956 dttype
= type(date_time
)
957 if dttype
is type(1) or dttype
is type(1.1):
958 tt
= time
.localtime(date_time
)
959 elif dttype
is type(()):
961 elif dttype
is type(""):
962 return date_time
# Assume in correct format
963 else: raise ValueError
965 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
968 if time
.daylight
and tt
[-1]:
971 zone
= -time
.timezone
972 return '"' + dt
+ " %+02d%02d" % divmod(zone
/60, 60) + '"'
978 def _mesg(s
, secs
=None):
981 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
982 sys
.stderr
.write(' %s.%02d %s\n' % (tm
, (secs
*100)%100, s
))
986 # Dump untagged responses (in `dict').
991 l
= map(lambda x
,j
=j
:'%s: "%s"' % (x
[0], x
[1][0] and j(x
[1], '" "') or ''), l
)
992 _mesg('untagged responses dump:%s%s' % (t
, j(l
, t
)))
994 _cmd_log
= [] # Last `_cmd_log_len' interactions
998 # Keep log of last `_cmd_log_len' interactions for debugging.
999 if len(_cmd_log
) == _cmd_log_len
:
1001 _cmd_log
.append((time
.time(), line
))
1004 _mesg('last %d IMAP4 interactions:' % len(_cmd_log
))
1005 for secs
,line
in _cmd_log
:
1010 if __name__
== '__main__':
1015 if sys
.argv
[1:]: host
= sys
.argv
[1]
1017 USER
= getpass
.getuser()
1018 PASSWD
= getpass
.getpass("IMAP password for %s: " % (host
or "localhost"))
1021 ('login', (USER
, PASSWD
)),
1022 ('create', ('/tmp/xxx 1',)),
1023 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1024 ('CREATE', ('/tmp/yyz 2',)),
1025 ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
1026 ('list', ('/tmp', 'yy*')),
1027 ('select', ('/tmp/yyz 2',)),
1028 ('search', (None, '(TO zork)')),
1029 ('partial', ('1', 'RFC822', 1, 1024)),
1030 ('store', ('1', 'FLAGS', '(\Deleted)')),
1038 ('response',('UIDVALIDITY',)),
1039 ('uid', ('SEARCH', 'ALL')),
1040 ('response', ('EXISTS',)),
1041 ('append', (None, None, None, 'From: anon@x.y.z\n\ndata...')),
1047 _mesg('%s %s' % (cmd
, args
))
1048 typ
, dat
= apply(eval('M.%s' % cmd
), args
)
1049 _mesg('%s => %s %s' % (cmd
, typ
, dat
))
1054 _mesg('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1056 for cmd
,args
in test_seq1
:
1059 for ml
in run('list', ('/tmp/', 'yy%')):
1060 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1061 if mo
: path
= mo
.group(1)
1062 else: path
= string
.split(ml
)[-1]
1063 run('delete', (path
,))
1065 for cmd
,args
in test_seq2
:
1066 dat
= run(cmd
, args
)
1068 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1071 uid
= string
.split(dat
[-1])
1072 if not uid
: continue
1073 run('uid', ('FETCH', '%s' % uid
[-1],
1074 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))