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'),
63 # Patterns to match server responses
65 Continuation
= re
.compile(r
'\+( (?P<data>.*))?')
66 Flags
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
67 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
68 r
'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
69 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
70 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
72 Literal
= re
.compile(r
'.*{(?P<size>\d+)}$')
73 Response_code
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
74 Untagged_response
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
75 Untagged_status
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
81 """IMAP4 client class.
83 Instantiate with: IMAP4([host[, port]])
85 host - host's name (default: localhost);
86 port - port number (default: standard IMAP4 port).
88 All IMAP4rev1 commands are supported by methods of the same
91 All arguments to commands are converted to strings, except for
92 AUTHENTICATE, and the last argument to APPEND which is passed as
93 an IMAP4 literal. If necessary (the string contains any
94 non-printing characters or white-space and isn't enclosed with
95 either parentheses or double quotes) each string is quoted.
96 However, the 'password' argument to the LOGIN command is always
97 quoted. If you want to avoid having an argument string quoted
98 (eg: the 'flags' argument to STORE) then enclose the string in
99 parentheses (eg: "(\Deleted)").
101 Each command returns a tuple: (type, [data, ...]) where 'type'
102 is usually 'OK' or 'NO', and 'data' is either the text from the
103 tagged response, or untagged results from command.
105 Errors raise the exception class <instance>.error("<reason>").
106 IMAP4 server errors raise <instance>.abort("<reason>"),
107 which is a sub-class of 'error'. Mailbox status changes
108 from READ-WRITE to READ-ONLY raise the exception class
109 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
111 "error" exceptions imply a program error.
112 "abort" exceptions imply the connection should be reset, and
113 the command re-tried.
114 "readonly" exceptions imply the command should be re-tried.
116 Note: to use this module, you must read the RFCs pertaining
117 to the IMAP4 protocol, as the semantics of the arguments to
118 each IMAP4 command are left to the invoker, not to mention
122 class error(Exception): pass # Logical errors - debug required
123 class abort(error
): pass # Service errors - close and retry
124 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
126 mustquote
= re
.compile(r
"[^\w!#$%&'*+,.:;<=>?^`|~-]")
128 def __init__(self
, host
= '', port
= IMAP4_PORT
):
132 self
.state
= 'LOGOUT'
133 self
.literal
= None # A literal argument to a command
134 self
.tagged_commands
= {} # Tagged commands awaiting response
135 self
.untagged_responses
= {} # {typ: [data, ...], ...}
136 self
.continuation_response
= '' # Last continuation response
137 self
.is_readonly
= None # READ-ONLY desired state
140 # Open socket to server.
142 self
.open(host
, port
)
144 # Create unique tag for this session,
145 # and compile tagged response matcher.
147 self
.tagpre
= Int2AP(random
.randint(0, 31999))
148 self
.tagre
= re
.compile(r
'(?P<tag>'
150 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
152 # Get server welcome message,
153 # request and store CAPABILITY response.
157 _mesg('new IMAP4 connection, tag=%s' % self
.tagpre
)
159 self
.welcome
= self
._get
_response
()
160 if self
.untagged_responses
.has_key('PREAUTH'):
162 elif self
.untagged_responses
.has_key('OK'):
163 self
.state
= 'NONAUTH'
165 raise self
.error(self
.welcome
)
168 self
._simple
_command
(cap
)
169 if not self
.untagged_responses
.has_key(cap
):
170 raise self
.error('no CAPABILITY response from server')
171 self
.capabilities
= tuple(self
.untagged_responses
[cap
][-1].upper().split())
175 _mesg('CAPABILITIES: %s' % `self
.capabilities`
)
177 for version
in AllowedVersions
:
178 if not version
in self
.capabilities
:
180 self
.PROTOCOL_VERSION
= version
183 raise self
.error('server not IMAP4 compliant')
186 def __getattr__(self
, attr
):
187 # Allow UPPERCASE variants of IMAP4 command methods.
188 if Commands
.has_key(attr
):
189 return eval("self.%s" % attr
.lower())
190 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
197 def open(self
, host
, port
):
198 """Setup 'self.sock' and 'self.file'."""
199 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
200 self
.sock
.connect((self
.host
, self
.port
))
201 self
.file = self
.sock
.makefile('r')
205 """Return most recent 'RECENT' responses if any exist,
206 else prompt server for an update using the 'NOOP' command.
208 (typ, [data]) = <instance>.recent()
210 'data' is None if no new messages,
211 else list of RECENT responses, most recent last.
214 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
217 typ
, dat
= self
.noop() # Prod server for response
218 return self
._untagged
_response
(typ
, dat
, name
)
221 def response(self
, code
):
222 """Return data for response 'code' if received, or None.
224 Old value for response 'code' is cleared.
226 (code, [data]) = <instance>.response(code)
228 return self
._untagged
_response
(code
, [None], code
.upper())
232 """Return socket instance used to connect to IMAP4 server.
234 socket = <instance>.socket()
243 def append(self
, mailbox
, flags
, date_time
, message
):
244 """Append message to named mailbox.
246 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
248 All args except `message' can be None.
254 if (flags
[0],flags
[-1]) != ('(',')'):
255 flags
= '(%s)' % flags
259 date_time
= Time2Internaldate(date_time
)
262 self
.literal
= message
263 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
266 def authenticate(self
, mechanism
, authobject
):
267 """Authenticate command - requires response processing.
269 'mechanism' specifies which authentication mechanism is to
270 be used - it must appear in <instance>.capabilities in the
271 form AUTH=<mechanism>.
273 'authobject' must be a callable object:
275 data = authobject(response)
277 It will be called to process server continuation responses.
278 It should return data that will be encoded and sent to server.
279 It should return None if the client abort response '*' should
282 mech
= mechanism
.upper()
283 cap
= 'AUTH=%s' % mech
284 if not cap
in self
.capabilities
:
285 raise self
.error("Server doesn't allow %s authentication." % mech
)
286 self
.literal
= _Authenticator(authobject
).process
287 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
289 raise self
.error(dat
[-1])
295 """Checkpoint mailbox on server.
297 (typ, [data]) = <instance>.check()
299 return self
._simple
_command
('CHECK')
303 """Close currently selected mailbox.
305 Deleted messages are removed from writable mailbox.
306 This is the recommended command before 'LOGOUT'.
308 (typ, [data]) = <instance>.close()
311 typ
, dat
= self
._simple
_command
('CLOSE')
317 def copy(self
, message_set
, new_mailbox
):
318 """Copy 'message_set' messages onto end of 'new_mailbox'.
320 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
322 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
325 def create(self
, mailbox
):
326 """Create new mailbox.
328 (typ, [data]) = <instance>.create(mailbox)
330 return self
._simple
_command
('CREATE', mailbox
)
333 def delete(self
, mailbox
):
334 """Delete old mailbox.
336 (typ, [data]) = <instance>.delete(mailbox)
338 return self
._simple
_command
('DELETE', mailbox
)
342 """Permanently remove deleted items from selected mailbox.
344 Generates 'EXPUNGE' response for each deleted message.
346 (typ, [data]) = <instance>.expunge()
348 'data' is list of 'EXPUNGE'd message numbers in order received.
351 typ
, dat
= self
._simple
_command
(name
)
352 return self
._untagged
_response
(typ
, dat
, name
)
355 def fetch(self
, message_set
, message_parts
):
356 """Fetch (parts of) messages.
358 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
360 'message_parts' should be a string of selected parts
361 enclosed in parentheses, eg: "(UID BODY[TEXT])".
363 'data' are tuples of message part envelope and data.
366 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
367 return self
._untagged
_response
(typ
, dat
, name
)
370 def list(self
, directory
='""', pattern
='*'):
371 """List mailbox names in directory matching pattern.
373 (typ, [data]) = <instance>.list(directory='""', pattern='*')
375 'data' is list of LIST responses.
378 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
379 return self
._untagged
_response
(typ
, dat
, name
)
382 def login(self
, user
, password
):
383 """Identify client using plaintext password.
385 (typ, [data]) = <instance>.login(user, password)
387 NB: 'password' will be quoted.
389 #if not 'AUTH=LOGIN' in self.capabilities:
390 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
391 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
393 raise self
.error(dat
[-1])
399 """Shutdown connection to server.
401 (typ, [data]) = <instance>.logout()
403 Returns server 'BYE' response.
405 self
.state
= 'LOGOUT'
406 try: typ
, dat
= self
._simple
_command
('LOGOUT')
407 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
410 if self
.untagged_responses
.has_key('BYE'):
411 return 'BYE', self
.untagged_responses
['BYE']
415 def lsub(self
, directory
='""', pattern
='*'):
416 """List 'subscribed' mailbox names in directory matching pattern.
418 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
420 'data' are tuples of message part envelope and data.
423 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
424 return self
._untagged
_response
(typ
, dat
, name
)
428 """Send NOOP command.
430 (typ, data) = <instance>.noop()
434 _dump_ur(self
.untagged_responses
)
435 return self
._simple
_command
('NOOP')
438 def partial(self
, message_num
, message_part
, start
, length
):
439 """Fetch truncated part of a message.
441 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
443 'data' is tuple of message part envelope and data.
446 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
447 return self
._untagged
_response
(typ
, dat
, 'FETCH')
450 def rename(self
, oldmailbox
, newmailbox
):
451 """Rename old mailbox name to new.
453 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
455 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
458 def search(self
, charset
, *criteria
):
459 """Search mailbox for matching messages.
461 (typ, [data]) = <instance>.search(charset, criterium, ...)
463 'data' is space separated list of matching message numbers.
467 charset
= 'CHARSET ' + charset
468 typ
, dat
= apply(self
._simple
_command
, (name
, charset
) + criteria
)
469 return self
._untagged
_response
(typ
, dat
, name
)
472 def select(self
, mailbox
='INBOX', readonly
=None):
475 Flush all untagged responses.
477 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
479 'data' is count of messages in mailbox ('EXISTS' response).
481 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
482 self
.untagged_responses
= {} # Flush old responses.
483 self
.is_readonly
= readonly
488 typ
, dat
= self
._simple
_command
(name
, mailbox
)
490 self
.state
= 'AUTH' # Might have been 'SELECTED'
492 self
.state
= 'SELECTED'
493 if self
.untagged_responses
.has_key('READ-ONLY') \
497 _dump_ur(self
.untagged_responses
)
498 raise self
.readonly('%s is not writable' % mailbox
)
499 return typ
, self
.untagged_responses
.get('EXISTS', [None])
502 def status(self
, mailbox
, names
):
503 """Request named status conditions for mailbox.
505 (typ, [data]) = <instance>.status(mailbox, names)
508 if self
.PROTOCOL_VERSION
== 'IMAP4':
509 raise self
.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name
)
510 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
511 return self
._untagged
_response
(typ
, dat
, name
)
514 def store(self
, message_set
, command
, flags
):
515 """Alters flag dispositions for messages in mailbox.
517 (typ, [data]) = <instance>.store(message_set, command, flags)
519 if (flags
[0],flags
[-1]) != ('(',')'):
520 flags
= '(%s)' % flags
# Avoid quoting the flags
521 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
522 return self
._untagged
_response
(typ
, dat
, 'FETCH')
525 def subscribe(self
, mailbox
):
526 """Subscribe to new mailbox.
528 (typ, [data]) = <instance>.subscribe(mailbox)
530 return self
._simple
_command
('SUBSCRIBE', mailbox
)
533 def uid(self
, command
, *args
):
534 """Execute "command arg ..." with messages identified by UID,
535 rather than message number.
537 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
539 Returns response appropriate to 'command'.
541 command
= command
.upper()
542 if not Commands
.has_key(command
):
543 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
544 if self
.state
not in Commands
[command
]:
545 raise self
.error('command %s illegal in state %s'
546 % (command
, self
.state
))
548 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
549 if command
== 'SEARCH':
553 return self
._untagged
_response
(typ
, dat
, name
)
556 def unsubscribe(self
, mailbox
):
557 """Unsubscribe from old mailbox.
559 (typ, [data]) = <instance>.unsubscribe(mailbox)
561 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
564 def xatom(self
, name
, *args
):
565 """Allow simple extension commands
566 notified by server in CAPABILITY response.
568 (typ, [data]) = <instance>.xatom(name, arg, ...)
570 if name
[0] != 'X' or not name
in self
.capabilities
:
571 raise self
.error('unknown extension command: %s' % name
)
572 return apply(self
._simple
_command
, (name
,) + args
)
579 def _append_untagged(self
, typ
, dat
):
581 if dat
is None: dat
= ''
582 ur
= self
.untagged_responses
585 _mesg('untagged_responses[%s] %s += ["%s"]' %
586 (typ
, len(ur
.get(typ
,'')), dat
))
593 def _check_bye(self
):
594 bye
= self
.untagged_responses
.get('BYE')
596 raise self
.abort(bye
[-1])
599 def _command(self
, name
, *args
):
601 if self
.state
not in Commands
[name
]:
604 'command %s illegal in state %s' % (name
, self
.state
))
606 for typ
in ('OK', 'NO', 'BAD'):
607 if self
.untagged_responses
.has_key(typ
):
608 del self
.untagged_responses
[typ
]
610 if self
.untagged_responses
.has_key('READ-ONLY') \
611 and not self
.is_readonly
:
612 raise self
.readonly('mailbox status changed to READ-ONLY')
614 tag
= self
._new
_tag
()
615 data
= '%s %s' % (tag
, name
)
617 if arg
is None: continue
618 data
= '%s %s' % (data
, self
._checkquote
(arg
))
620 literal
= self
.literal
621 if literal
is not None:
623 if type(literal
) is type(self
._command
):
627 data
= '%s {%s}' % (data
, len(literal
))
636 self
.sock
.send('%s%s' % (data
, CRLF
))
637 except socket
.error
, val
:
638 raise self
.abort('socket error: %s' % val
)
644 # Wait for continuation response
646 while self
._get
_response
():
647 if self
.tagged_commands
[tag
]: # BAD/NO?
653 literal
= literator(self
.continuation_response
)
657 _mesg('write literal size %s' % len(literal
))
660 self
.sock
.send(literal
)
662 except socket
.error
, val
:
663 raise self
.abort('socket error: %s' % val
)
671 def _command_complete(self
, name
, tag
):
674 typ
, data
= self
._get
_tagged
_response
(tag
)
675 except self
.abort
, val
:
676 raise self
.abort('command: %s => %s' % (name
, val
))
677 except self
.error
, val
:
678 raise self
.error('command: %s => %s' % (name
, val
))
681 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
685 def _get_response(self
):
687 # Read response and store.
689 # Returns None for continuation responses,
690 # otherwise first response line received.
692 resp
= self
._get
_line
()
694 # Command completion response?
696 if self
._match
(self
.tagre
, resp
):
697 tag
= self
.mo
.group('tag')
698 if not self
.tagged_commands
.has_key(tag
):
699 raise self
.abort('unexpected tagged response: %s' % resp
)
701 typ
= self
.mo
.group('type')
702 dat
= self
.mo
.group('data')
703 self
.tagged_commands
[tag
] = (typ
, [dat
])
707 # '*' (untagged) responses?
709 if not self
._match
(Untagged_response
, resp
):
710 if self
._match
(Untagged_status
, resp
):
711 dat2
= self
.mo
.group('data2')
714 # Only other possibility is '+' (continuation) response...
716 if self
._match
(Continuation
, resp
):
717 self
.continuation_response
= self
.mo
.group('data')
718 return None # NB: indicates continuation
720 raise self
.abort("unexpected response: '%s'" % resp
)
722 typ
= self
.mo
.group('type')
723 dat
= self
.mo
.group('data')
724 if dat
is None: dat
= '' # Null untagged response
725 if dat2
: dat
= dat
+ ' ' + dat2
727 # Is there a literal to come?
729 while self
._match
(Literal
, dat
):
731 # Read literal direct from connection.
733 size
= int(self
.mo
.group('size'))
736 _mesg('read literal size %s' % size
)
737 data
= self
.file.read(size
)
739 # Store response with literal as tuple
741 self
._append
_untagged
(typ
, (dat
, data
))
743 # Read trailer - possibly containing another literal
745 dat
= self
._get
_line
()
747 self
._append
_untagged
(typ
, dat
)
749 # Bracketed response information?
751 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
752 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
755 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
756 _mesg('%s response: %s' % (typ
, dat
))
761 def _get_tagged_response(self
, tag
):
764 result
= self
.tagged_commands
[tag
]
765 if result
is not None:
766 del self
.tagged_commands
[tag
]
769 # Some have reported "unexpected response" exceptions.
770 # Note that ignoring them here causes loops.
771 # Instead, send me details of the unexpected response and
772 # I'll update the code in `_get_response()'.
776 except self
.abort
, val
:
785 line
= self
.file.readline()
787 raise self
.abort('socket error: EOF')
789 # Protocol mandates all lines terminated by CRLF
800 def _match(self
, cre
, s
):
802 # Run compiled regular expression match method on 's'.
803 # Save result, return success.
805 self
.mo
= cre
.match(s
)
807 if self
.mo
is not None and self
.debug
>= 5:
808 _mesg("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
809 return self
.mo
is not None
814 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
815 self
.tagnum
= self
.tagnum
+ 1
816 self
.tagged_commands
[tag
] = None
820 def _checkquote(self
, arg
):
822 # Must quote command args if non-alphanumeric chars present,
823 # and not already quoted.
825 if type(arg
) is not type(''):
827 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
829 if self
.mustquote
.search(arg
) is None:
831 return self
._quote
(arg
)
834 def _quote(self
, arg
):
836 arg
= arg
.replace('\\', '\\\\')
837 arg
= arg
.replace('"', '\\"')
842 def _simple_command(self
, name
, *args
):
844 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
847 def _untagged_response(self
, typ
, dat
, name
):
851 if not self
.untagged_responses
.has_key(name
):
853 data
= self
.untagged_responses
[name
]
856 _mesg('untagged_responses[%s] => %s' % (name
, data
))
857 del self
.untagged_responses
[name
]
862 class _Authenticator
:
864 """Private class to provide en/decoding
865 for base64-based authentication conversation.
868 def __init__(self
, mechinst
):
869 self
.mech
= mechinst
# Callable object to provide/process data
871 def process(self
, data
):
872 ret
= self
.mech(self
.decode(data
))
874 return '*' # Abort conversation
875 return self
.encode(ret
)
877 def encode(self
, inp
):
879 # Invoke binascii.b2a_base64 iteratively with
880 # short even length buffers, strip the trailing
881 # line feed from the result and append. "Even"
882 # means a number that factors to both 6 and 8,
883 # so when it gets to the end of the 8-bit input
884 # there's no partial 6-bit output.
894 e
= binascii
.b2a_base64(t
)
899 def decode(self
, inp
):
902 return binascii
.a2b_base64(inp
)
906 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
907 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
909 def Internaldate2tuple(resp
):
910 """Convert IMAP4 INTERNALDATE to UT.
912 Returns Python time module tuple.
915 mo
= InternalDate
.match(resp
)
919 mon
= Mon2num
[mo
.group('mon')]
920 zonen
= mo
.group('zonen')
922 day
= int(mo
.group('day'))
923 year
= int(mo
.group('year'))
924 hour
= int(mo
.group('hour'))
925 min = int(mo
.group('min'))
926 sec
= int(mo
.group('sec'))
927 zoneh
= int(mo
.group('zoneh'))
928 zonem
= int(mo
.group('zonem'))
930 # INTERNALDATE timezone must be subtracted to get UT
932 zone
= (zoneh
*60 + zonem
)*60
936 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
938 utc
= time
.mktime(tt
)
940 # Following is necessary because the time module has no 'mkgmtime'.
941 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
943 lt
= time
.localtime(utc
)
944 if time
.daylight
and lt
[-1]:
945 zone
= zone
+ time
.altzone
947 zone
= zone
+ time
.timezone
949 return time
.localtime(utc
- zone
)
955 """Convert integer to A-P string representation."""
957 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
960 num
, mod
= divmod(num
, 16)
966 def ParseFlags(resp
):
968 """Convert IMAP4 flags response to python tuple."""
970 mo
= Flags
.match(resp
)
974 return tuple(mo
.group('flags').split())
977 def Time2Internaldate(date_time
):
979 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
981 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
984 dttype
= type(date_time
)
985 if dttype
is type(1) or dttype
is type(1.1):
986 tt
= time
.localtime(date_time
)
987 elif dttype
is type(()):
989 elif dttype
is type(""):
990 return date_time
# Assume in correct format
991 else: raise ValueError
993 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
996 if time
.daylight
and tt
[-1]:
999 zone
= -time
.timezone
1000 return '"' + dt
+ " %+02d%02d" % divmod(zone
/60, 60) + '"'
1006 def _mesg(s
, secs
=None):
1009 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1010 sys
.stderr
.write(' %s.%02d %s\n' % (tm
, (secs
*100)%100, s
))
1014 # Dump untagged responses (in `dict').
1018 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1019 _mesg('untagged responses dump:%s%s' % (t
, j(l
, t
)))
1021 _cmd_log
= [] # Last `_cmd_log_len' interactions
1025 # Keep log of last `_cmd_log_len' interactions for debugging.
1026 if len(_cmd_log
) == _cmd_log_len
:
1028 _cmd_log
.append((time
.time(), line
))
1031 _mesg('last %d IMAP4 interactions:' % len(_cmd_log
))
1032 for secs
,line
in _cmd_log
:
1037 if __name__
== '__main__':
1039 import getopt
, getpass
, sys
1042 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:')
1043 except getopt
.error
, val
:
1046 for opt
,val
in optlist
:
1050 if not args
: args
= ('',)
1054 USER
= getpass
.getuser()
1055 PASSWD
= getpass
.getpass("IMAP password for %s on %s:" % (USER
, host
or "localhost"))
1057 test_mesg
= 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
1059 ('login', (USER
, PASSWD
)),
1060 ('create', ('/tmp/xxx 1',)),
1061 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1062 ('CREATE', ('/tmp/yyz 2',)),
1063 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1064 ('list', ('/tmp', 'yy*')),
1065 ('select', ('/tmp/yyz 2',)),
1066 ('search', (None, 'SUBJECT', 'test')),
1067 ('partial', ('1', 'RFC822', 1, 1024)),
1068 ('store', ('1', 'FLAGS', '(\Deleted)')),
1076 ('response',('UIDVALIDITY',)),
1077 ('uid', ('SEARCH', 'ALL')),
1078 ('response', ('EXISTS',)),
1079 ('append', (None, None, None, test_mesg
)),
1085 _mesg('%s %s' % (cmd
, args
))
1086 typ
, dat
= apply(eval('M.%s' % cmd
), args
)
1087 _mesg('%s => %s %s' % (cmd
, typ
, dat
))
1092 _mesg('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1094 for cmd
,args
in test_seq1
:
1097 for ml
in run('list', ('/tmp/', 'yy%')):
1098 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1099 if mo
: path
= mo
.group(1)
1100 else: path
= ml
.split()[-1]
1101 run('delete', (path
,))
1103 for cmd
,args
in test_seq2
:
1104 dat
= run(cmd
, args
)
1106 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1109 uid
= dat
[-1].split()
1110 if not uid
: continue
1111 run('uid', ('FETCH', '%s' % uid
[-1],
1112 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1114 print '\nAll tests OK.'
1117 print '\nTests failed.'
1121 If you would like to see debugging output,