7 Public functions: Internaldate2tuple
13 # Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
15 # Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
16 # String method conversion by ESR, February 2001.
20 import binascii
, re
, socket
, time
, random
, sys
22 __all__
= ["IMAP4", "Internaldate2tuple",
23 "Int2AP", "ParseFlags", "Time2Internaldate"]
30 AllowedVersions
= ('IMAP4REV1', 'IMAP4') # Most recent first
36 'APPEND': ('AUTH', 'SELECTED'),
37 'AUTHENTICATE': ('NONAUTH',),
38 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
39 'CHECK': ('SELECTED',),
40 'CLOSE': ('SELECTED',),
41 'COPY': ('SELECTED',),
42 'CREATE': ('AUTH', 'SELECTED'),
43 'DELETE': ('AUTH', 'SELECTED'),
44 'EXAMINE': ('AUTH', 'SELECTED'),
45 'EXPUNGE': ('SELECTED',),
46 'FETCH': ('SELECTED',),
47 'LIST': ('AUTH', 'SELECTED'),
48 'LOGIN': ('NONAUTH',),
49 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
50 'LSUB': ('AUTH', 'SELECTED'),
51 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
52 'PARTIAL': ('SELECTED',),
53 'RENAME': ('AUTH', 'SELECTED'),
54 'SEARCH': ('SELECTED',),
55 'SELECT': ('AUTH', 'SELECTED'),
56 'STATUS': ('AUTH', 'SELECTED'),
57 'STORE': ('SELECTED',),
58 'SUBSCRIBE': ('AUTH', 'SELECTED'),
60 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
61 'NAMESPACE': ('AUTH', 'SELECTED'),
64 # Patterns to match server responses
66 Continuation
= re
.compile(r
'\+( (?P<data>.*))?')
67 Flags
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
68 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
69 r
'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
70 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
71 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
73 Literal
= re
.compile(r
'.*{(?P<size>\d+)}$')
74 Response_code
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
75 Untagged_response
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
76 Untagged_status
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
82 """IMAP4 client class.
84 Instantiate with: IMAP4([host[, port]])
86 host - host's name (default: localhost);
87 port - port number (default: standard IMAP4 port).
89 All IMAP4rev1 commands are supported by methods of the same
92 All arguments to commands are converted to strings, except for
93 AUTHENTICATE, and the last argument to APPEND which is passed as
94 an IMAP4 literal. If necessary (the string contains any
95 non-printing characters or white-space and isn't enclosed with
96 either parentheses or double quotes) each string is quoted.
97 However, the 'password' argument to the LOGIN command is always
98 quoted. If you want to avoid having an argument string quoted
99 (eg: the 'flags' argument to STORE) then enclose the string in
100 parentheses (eg: "(\Deleted)").
102 Each command returns a tuple: (type, [data, ...]) where 'type'
103 is usually 'OK' or 'NO', and 'data' is either the text from the
104 tagged response, or untagged results from command.
106 Errors raise the exception class <instance>.error("<reason>").
107 IMAP4 server errors raise <instance>.abort("<reason>"),
108 which is a sub-class of 'error'. Mailbox status changes
109 from READ-WRITE to READ-ONLY raise the exception class
110 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
112 "error" exceptions imply a program error.
113 "abort" exceptions imply the connection should be reset, and
114 the command re-tried.
115 "readonly" exceptions imply the command should be re-tried.
117 Note: to use this module, you must read the RFCs pertaining
118 to the IMAP4 protocol, as the semantics of the arguments to
119 each IMAP4 command are left to the invoker, not to mention
123 class error(Exception): pass # Logical errors - debug required
124 class abort(error
): pass # Service errors - close and retry
125 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
127 mustquote
= re
.compile(r
"[^\w!#$%&'*+,.:;<=>?^`|~-]")
129 def __init__(self
, host
= '', port
= IMAP4_PORT
):
133 self
.state
= 'LOGOUT'
134 self
.literal
= None # A literal argument to a command
135 self
.tagged_commands
= {} # Tagged commands awaiting response
136 self
.untagged_responses
= {} # {typ: [data, ...], ...}
137 self
.continuation_response
= '' # Last continuation response
138 self
.is_readonly
= None # READ-ONLY desired state
141 # Open socket to server.
143 self
.open(host
, port
)
145 # Create unique tag for this session,
146 # and compile tagged response matcher.
148 self
.tagpre
= Int2AP(random
.randint(0, 31999))
149 self
.tagre
= re
.compile(r
'(?P<tag>'
151 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
153 # Get server welcome message,
154 # request and store CAPABILITY response.
158 _mesg('new IMAP4 connection, tag=%s' % self
.tagpre
)
160 self
.welcome
= self
._get
_response
()
161 if self
.untagged_responses
.has_key('PREAUTH'):
163 elif self
.untagged_responses
.has_key('OK'):
164 self
.state
= 'NONAUTH'
166 raise self
.error(self
.welcome
)
169 self
._simple
_command
(cap
)
170 if not self
.untagged_responses
.has_key(cap
):
171 raise self
.error('no CAPABILITY response from server')
172 self
.capabilities
= tuple(self
.untagged_responses
[cap
][-1].upper().split())
176 _mesg('CAPABILITIES: %s' % `self
.capabilities`
)
178 for version
in AllowedVersions
:
179 if not version
in self
.capabilities
:
181 self
.PROTOCOL_VERSION
= version
184 raise self
.error('server not IMAP4 compliant')
187 def __getattr__(self
, attr
):
188 # Allow UPPERCASE variants of IMAP4 command methods.
189 if Commands
.has_key(attr
):
190 return eval("self.%s" % attr
.lower())
191 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
198 def open(self
, host
, port
):
199 """Setup 'self.sock' and 'self.file'."""
200 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
201 self
.sock
.connect((self
.host
, self
.port
))
202 self
.file = self
.sock
.makefile('r')
206 """Return most recent 'RECENT' responses if any exist,
207 else prompt server for an update using the 'NOOP' command.
209 (typ, [data]) = <instance>.recent()
211 'data' is None if no new messages,
212 else list of RECENT responses, most recent last.
215 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
218 typ
, dat
= self
.noop() # Prod server for response
219 return self
._untagged
_response
(typ
, dat
, name
)
222 def response(self
, code
):
223 """Return data for response 'code' if received, or None.
225 Old value for response 'code' is cleared.
227 (code, [data]) = <instance>.response(code)
229 return self
._untagged
_response
(code
, [None], code
.upper())
233 """Return socket instance used to connect to IMAP4 server.
235 socket = <instance>.socket()
244 def append(self
, mailbox
, flags
, date_time
, message
):
245 """Append message to named mailbox.
247 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
249 All args except `message' can be None.
255 if (flags
[0],flags
[-1]) != ('(',')'):
256 flags
= '(%s)' % flags
260 date_time
= Time2Internaldate(date_time
)
263 self
.literal
= message
264 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
267 def authenticate(self
, mechanism
, authobject
):
268 """Authenticate command - requires response processing.
270 'mechanism' specifies which authentication mechanism is to
271 be used - it must appear in <instance>.capabilities in the
272 form AUTH=<mechanism>.
274 'authobject' must be a callable object:
276 data = authobject(response)
278 It will be called to process server continuation responses.
279 It should return data that will be encoded and sent to server.
280 It should return None if the client abort response '*' should
283 mech
= mechanism
.upper()
284 cap
= 'AUTH=%s' % mech
285 if not cap
in self
.capabilities
:
286 raise self
.error("Server doesn't allow %s authentication." % mech
)
287 self
.literal
= _Authenticator(authobject
).process
288 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
290 raise self
.error(dat
[-1])
296 """Checkpoint mailbox on server.
298 (typ, [data]) = <instance>.check()
300 return self
._simple
_command
('CHECK')
304 """Close currently selected mailbox.
306 Deleted messages are removed from writable mailbox.
307 This is the recommended command before 'LOGOUT'.
309 (typ, [data]) = <instance>.close()
312 typ
, dat
= self
._simple
_command
('CLOSE')
318 def copy(self
, message_set
, new_mailbox
):
319 """Copy 'message_set' messages onto end of 'new_mailbox'.
321 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
323 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
326 def create(self
, mailbox
):
327 """Create new mailbox.
329 (typ, [data]) = <instance>.create(mailbox)
331 return self
._simple
_command
('CREATE', mailbox
)
334 def delete(self
, mailbox
):
335 """Delete old mailbox.
337 (typ, [data]) = <instance>.delete(mailbox)
339 return self
._simple
_command
('DELETE', mailbox
)
343 """Permanently remove deleted items from selected mailbox.
345 Generates 'EXPUNGE' response for each deleted message.
347 (typ, [data]) = <instance>.expunge()
349 'data' is list of 'EXPUNGE'd message numbers in order received.
352 typ
, dat
= self
._simple
_command
(name
)
353 return self
._untagged
_response
(typ
, dat
, name
)
356 def fetch(self
, message_set
, message_parts
):
357 """Fetch (parts of) messages.
359 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
361 'message_parts' should be a string of selected parts
362 enclosed in parentheses, eg: "(UID BODY[TEXT])".
364 'data' are tuples of message part envelope and data.
367 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
368 return self
._untagged
_response
(typ
, dat
, name
)
371 def list(self
, directory
='""', pattern
='*'):
372 """List mailbox names in directory matching pattern.
374 (typ, [data]) = <instance>.list(directory='""', pattern='*')
376 'data' is list of LIST responses.
379 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
380 return self
._untagged
_response
(typ
, dat
, name
)
383 def login(self
, user
, password
):
384 """Identify client using plaintext password.
386 (typ, [data]) = <instance>.login(user, password)
388 NB: 'password' will be quoted.
390 #if not 'AUTH=LOGIN' in self.capabilities:
391 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
392 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
394 raise self
.error(dat
[-1])
400 """Shutdown connection to server.
402 (typ, [data]) = <instance>.logout()
404 Returns server 'BYE' response.
406 self
.state
= 'LOGOUT'
407 try: typ
, dat
= self
._simple
_command
('LOGOUT')
408 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
411 if self
.untagged_responses
.has_key('BYE'):
412 return 'BYE', self
.untagged_responses
['BYE']
416 def lsub(self
, directory
='""', pattern
='*'):
417 """List 'subscribed' mailbox names in directory matching pattern.
419 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
421 'data' are tuples of message part envelope and data.
424 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
425 return self
._untagged
_response
(typ
, dat
, name
)
429 """Send NOOP command.
431 (typ, data) = <instance>.noop()
435 _dump_ur(self
.untagged_responses
)
436 return self
._simple
_command
('NOOP')
439 def partial(self
, message_num
, message_part
, start
, length
):
440 """Fetch truncated part of a message.
442 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
444 'data' is tuple of message part envelope and data.
447 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
448 return self
._untagged
_response
(typ
, dat
, 'FETCH')
451 def rename(self
, oldmailbox
, newmailbox
):
452 """Rename old mailbox name to new.
454 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
456 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
459 def search(self
, charset
, *criteria
):
460 """Search mailbox for matching messages.
462 (typ, [data]) = <instance>.search(charset, criterium, ...)
464 'data' is space separated list of matching message numbers.
468 charset
= 'CHARSET ' + charset
469 typ
, dat
= apply(self
._simple
_command
, (name
, charset
) + criteria
)
470 return self
._untagged
_response
(typ
, dat
, name
)
473 def select(self
, mailbox
='INBOX', readonly
=None):
476 Flush all untagged responses.
478 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
480 'data' is count of messages in mailbox ('EXISTS' response).
482 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
483 self
.untagged_responses
= {} # Flush old responses.
484 self
.is_readonly
= readonly
489 typ
, dat
= self
._simple
_command
(name
, mailbox
)
491 self
.state
= 'AUTH' # Might have been 'SELECTED'
493 self
.state
= 'SELECTED'
494 if self
.untagged_responses
.has_key('READ-ONLY') \
498 _dump_ur(self
.untagged_responses
)
499 raise self
.readonly('%s is not writable' % mailbox
)
500 return typ
, self
.untagged_responses
.get('EXISTS', [None])
503 def status(self
, mailbox
, names
):
504 """Request named status conditions for mailbox.
506 (typ, [data]) = <instance>.status(mailbox, names)
509 if self
.PROTOCOL_VERSION
== 'IMAP4':
510 raise self
.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name
)
511 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
512 return self
._untagged
_response
(typ
, dat
, name
)
515 def store(self
, message_set
, command
, flags
):
516 """Alters flag dispositions for messages in mailbox.
518 (typ, [data]) = <instance>.store(message_set, command, flags)
520 if (flags
[0],flags
[-1]) != ('(',')'):
521 flags
= '(%s)' % flags
# Avoid quoting the flags
522 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
523 return self
._untagged
_response
(typ
, dat
, 'FETCH')
526 def subscribe(self
, mailbox
):
527 """Subscribe to new mailbox.
529 (typ, [data]) = <instance>.subscribe(mailbox)
531 return self
._simple
_command
('SUBSCRIBE', mailbox
)
534 def uid(self
, command
, *args
):
535 """Execute "command arg ..." with messages identified by UID,
536 rather than message number.
538 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
540 Returns response appropriate to 'command'.
542 command
= command
.upper()
543 if not Commands
.has_key(command
):
544 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
545 if self
.state
not in Commands
[command
]:
546 raise self
.error('command %s illegal in state %s'
547 % (command
, self
.state
))
549 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
550 if command
== 'SEARCH':
554 return self
._untagged
_response
(typ
, dat
, name
)
557 def unsubscribe(self
, mailbox
):
558 """Unsubscribe from old mailbox.
560 (typ, [data]) = <instance>.unsubscribe(mailbox)
562 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
565 def xatom(self
, name
, *args
):
566 """Allow simple extension commands
567 notified by server in CAPABILITY response.
569 (typ, [data]) = <instance>.xatom(name, arg, ...)
571 if name
[0] != 'X' or not name
in self
.capabilities
:
572 raise self
.error('unknown extension command: %s' % name
)
573 return apply(self
._simple
_command
, (name
,) + args
)
576 """ Returns IMAP namespaces ala rfc2342
579 typ
, dat
= self
._simple
_command
(name
)
580 return self
._untagged
_response
(typ
, dat
, name
)
586 def _append_untagged(self
, typ
, dat
):
588 if dat
is None: dat
= ''
589 ur
= self
.untagged_responses
592 _mesg('untagged_responses[%s] %s += ["%s"]' %
593 (typ
, len(ur
.get(typ
,'')), dat
))
600 def _check_bye(self
):
601 bye
= self
.untagged_responses
.get('BYE')
603 raise self
.abort(bye
[-1])
606 def _command(self
, name
, *args
):
608 if self
.state
not in Commands
[name
]:
611 'command %s illegal in state %s' % (name
, self
.state
))
613 for typ
in ('OK', 'NO', 'BAD'):
614 if self
.untagged_responses
.has_key(typ
):
615 del self
.untagged_responses
[typ
]
617 if self
.untagged_responses
.has_key('READ-ONLY') \
618 and not self
.is_readonly
:
619 raise self
.readonly('mailbox status changed to READ-ONLY')
621 tag
= self
._new
_tag
()
622 data
= '%s %s' % (tag
, name
)
624 if arg
is None: continue
625 data
= '%s %s' % (data
, self
._checkquote
(arg
))
627 literal
= self
.literal
628 if literal
is not None:
630 if type(literal
) is type(self
._command
):
634 data
= '%s {%s}' % (data
, len(literal
))
643 self
.sock
.send('%s%s' % (data
, CRLF
))
644 except socket
.error
, val
:
645 raise self
.abort('socket error: %s' % val
)
651 # Wait for continuation response
653 while self
._get
_response
():
654 if self
.tagged_commands
[tag
]: # BAD/NO?
660 literal
= literator(self
.continuation_response
)
664 _mesg('write literal size %s' % len(literal
))
667 self
.sock
.send(literal
)
669 except socket
.error
, val
:
670 raise self
.abort('socket error: %s' % val
)
678 def _command_complete(self
, name
, tag
):
681 typ
, data
= self
._get
_tagged
_response
(tag
)
682 except self
.abort
, val
:
683 raise self
.abort('command: %s => %s' % (name
, val
))
684 except self
.error
, val
:
685 raise self
.error('command: %s => %s' % (name
, val
))
688 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
692 def _get_response(self
):
694 # Read response and store.
696 # Returns None for continuation responses,
697 # otherwise first response line received.
699 resp
= self
._get
_line
()
701 # Command completion response?
703 if self
._match
(self
.tagre
, resp
):
704 tag
= self
.mo
.group('tag')
705 if not self
.tagged_commands
.has_key(tag
):
706 raise self
.abort('unexpected tagged response: %s' % resp
)
708 typ
= self
.mo
.group('type')
709 dat
= self
.mo
.group('data')
710 self
.tagged_commands
[tag
] = (typ
, [dat
])
714 # '*' (untagged) responses?
716 if not self
._match
(Untagged_response
, resp
):
717 if self
._match
(Untagged_status
, resp
):
718 dat2
= self
.mo
.group('data2')
721 # Only other possibility is '+' (continuation) response...
723 if self
._match
(Continuation
, resp
):
724 self
.continuation_response
= self
.mo
.group('data')
725 return None # NB: indicates continuation
727 raise self
.abort("unexpected response: '%s'" % resp
)
729 typ
= self
.mo
.group('type')
730 dat
= self
.mo
.group('data')
731 if dat
is None: dat
= '' # Null untagged response
732 if dat2
: dat
= dat
+ ' ' + dat2
734 # Is there a literal to come?
736 while self
._match
(Literal
, dat
):
738 # Read literal direct from connection.
740 size
= int(self
.mo
.group('size'))
743 _mesg('read literal size %s' % size
)
744 data
= self
.file.read(size
)
746 # Store response with literal as tuple
748 self
._append
_untagged
(typ
, (dat
, data
))
750 # Read trailer - possibly containing another literal
752 dat
= self
._get
_line
()
754 self
._append
_untagged
(typ
, dat
)
756 # Bracketed response information?
758 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
759 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
762 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
763 _mesg('%s response: %s' % (typ
, dat
))
768 def _get_tagged_response(self
, tag
):
771 result
= self
.tagged_commands
[tag
]
772 if result
is not None:
773 del self
.tagged_commands
[tag
]
776 # Some have reported "unexpected response" exceptions.
777 # Note that ignoring them here causes loops.
778 # Instead, send me details of the unexpected response and
779 # I'll update the code in `_get_response()'.
783 except self
.abort
, val
:
792 line
= self
.file.readline()
794 raise self
.abort('socket error: EOF')
796 # Protocol mandates all lines terminated by CRLF
807 def _match(self
, cre
, s
):
809 # Run compiled regular expression match method on 's'.
810 # Save result, return success.
812 self
.mo
= cre
.match(s
)
814 if self
.mo
is not None and self
.debug
>= 5:
815 _mesg("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
816 return self
.mo
is not None
821 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
822 self
.tagnum
= self
.tagnum
+ 1
823 self
.tagged_commands
[tag
] = None
827 def _checkquote(self
, arg
):
829 # Must quote command args if non-alphanumeric chars present,
830 # and not already quoted.
832 if type(arg
) is not type(''):
834 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
836 if self
.mustquote
.search(arg
) is None:
838 return self
._quote
(arg
)
841 def _quote(self
, arg
):
843 arg
= arg
.replace('\\', '\\\\')
844 arg
= arg
.replace('"', '\\"')
849 def _simple_command(self
, name
, *args
):
851 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
854 def _untagged_response(self
, typ
, dat
, name
):
858 if not self
.untagged_responses
.has_key(name
):
860 data
= self
.untagged_responses
[name
]
863 _mesg('untagged_responses[%s] => %s' % (name
, data
))
864 del self
.untagged_responses
[name
]
869 class _Authenticator
:
871 """Private class to provide en/decoding
872 for base64-based authentication conversation.
875 def __init__(self
, mechinst
):
876 self
.mech
= mechinst
# Callable object to provide/process data
878 def process(self
, data
):
879 ret
= self
.mech(self
.decode(data
))
881 return '*' # Abort conversation
882 return self
.encode(ret
)
884 def encode(self
, inp
):
886 # Invoke binascii.b2a_base64 iteratively with
887 # short even length buffers, strip the trailing
888 # line feed from the result and append. "Even"
889 # means a number that factors to both 6 and 8,
890 # so when it gets to the end of the 8-bit input
891 # there's no partial 6-bit output.
901 e
= binascii
.b2a_base64(t
)
906 def decode(self
, inp
):
909 return binascii
.a2b_base64(inp
)
913 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
914 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
916 def Internaldate2tuple(resp
):
917 """Convert IMAP4 INTERNALDATE to UT.
919 Returns Python time module tuple.
922 mo
= InternalDate
.match(resp
)
926 mon
= Mon2num
[mo
.group('mon')]
927 zonen
= mo
.group('zonen')
929 day
= int(mo
.group('day'))
930 year
= int(mo
.group('year'))
931 hour
= int(mo
.group('hour'))
932 min = int(mo
.group('min'))
933 sec
= int(mo
.group('sec'))
934 zoneh
= int(mo
.group('zoneh'))
935 zonem
= int(mo
.group('zonem'))
937 # INTERNALDATE timezone must be subtracted to get UT
939 zone
= (zoneh
*60 + zonem
)*60
943 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
945 utc
= time
.mktime(tt
)
947 # Following is necessary because the time module has no 'mkgmtime'.
948 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
950 lt
= time
.localtime(utc
)
951 if time
.daylight
and lt
[-1]:
952 zone
= zone
+ time
.altzone
954 zone
= zone
+ time
.timezone
956 return time
.localtime(utc
- zone
)
962 """Convert integer to A-P string representation."""
964 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
967 num
, mod
= divmod(num
, 16)
973 def ParseFlags(resp
):
975 """Convert IMAP4 flags response to python tuple."""
977 mo
= Flags
.match(resp
)
981 return tuple(mo
.group('flags').split())
984 def Time2Internaldate(date_time
):
986 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
988 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
991 dttype
= type(date_time
)
992 if dttype
is type(1) or dttype
is type(1.1):
993 tt
= time
.localtime(date_time
)
994 elif dttype
is type(()):
996 elif dttype
is type(""):
997 return date_time
# Assume in correct format
998 else: raise ValueError
1000 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1003 if time
.daylight
and tt
[-1]:
1004 zone
= -time
.altzone
1006 zone
= -time
.timezone
1007 return '"' + dt
+ " %+02d%02d" % divmod(zone
/60, 60) + '"'
1013 def _mesg(s
, secs
=None):
1016 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1017 sys
.stderr
.write(' %s.%02d %s\n' % (tm
, (secs
*100)%100, s
))
1021 # Dump untagged responses (in `dict').
1025 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1026 _mesg('untagged responses dump:%s%s' % (t
, j(l
, t
)))
1028 _cmd_log
= [] # Last `_cmd_log_len' interactions
1032 # Keep log of last `_cmd_log_len' interactions for debugging.
1033 if len(_cmd_log
) == _cmd_log_len
:
1035 _cmd_log
.append((time
.time(), line
))
1038 _mesg('last %d IMAP4 interactions:' % len(_cmd_log
))
1039 for secs
,line
in _cmd_log
:
1044 if __name__
== '__main__':
1046 import getopt
, getpass
, sys
1049 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:')
1050 except getopt
.error
, val
:
1053 for opt
,val
in optlist
:
1057 if not args
: args
= ('',)
1061 USER
= getpass
.getuser()
1062 PASSWD
= getpass
.getpass("IMAP password for %s on %s:" % (USER
, host
or "localhost"))
1064 test_mesg
= 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
1066 ('login', (USER
, PASSWD
)),
1067 ('create', ('/tmp/xxx 1',)),
1068 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1069 ('CREATE', ('/tmp/yyz 2',)),
1070 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1071 ('list', ('/tmp', 'yy*')),
1072 ('select', ('/tmp/yyz 2',)),
1073 ('search', (None, 'SUBJECT', 'test')),
1074 ('partial', ('1', 'RFC822', 1, 1024)),
1075 ('store', ('1', 'FLAGS', '(\Deleted)')),
1083 ('response',('UIDVALIDITY',)),
1084 ('uid', ('SEARCH', 'ALL')),
1085 ('response', ('EXISTS',)),
1086 ('append', (None, None, None, test_mesg
)),
1092 _mesg('%s %s' % (cmd
, args
))
1093 typ
, dat
= apply(eval('M.%s' % cmd
), args
)
1094 _mesg('%s => %s %s' % (cmd
, typ
, dat
))
1099 _mesg('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1101 for cmd
,args
in test_seq1
:
1104 for ml
in run('list', ('/tmp/', 'yy%')):
1105 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1106 if mo
: path
= mo
.group(1)
1107 else: path
= ml
.split()[-1]
1108 run('delete', (path
,))
1110 for cmd
,args
in test_seq2
:
1111 dat
= run(cmd
, args
)
1113 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1116 uid
= dat
[-1].split()
1117 if not uid
: continue
1118 run('uid', ('FETCH', '%s' % uid
[-1],
1119 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1121 print '\nAll tests OK.'
1124 print '\nTests failed.'
1128 If you would like to see debugging output,