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.
17 # GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
21 import binascii
, re
, socket
, time
, random
, sys
23 __all__
= ["IMAP4", "Internaldate2tuple",
24 "Int2AP", "ParseFlags", "Time2Internaldate"]
31 AllowedVersions
= ('IMAP4REV1', 'IMAP4') # Most recent first
37 'APPEND': ('AUTH', 'SELECTED'),
38 'AUTHENTICATE': ('NONAUTH',),
39 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
40 'CHECK': ('SELECTED',),
41 'CLOSE': ('SELECTED',),
42 'COPY': ('SELECTED',),
43 'CREATE': ('AUTH', 'SELECTED'),
44 'DELETE': ('AUTH', 'SELECTED'),
45 'EXAMINE': ('AUTH', 'SELECTED'),
46 'EXPUNGE': ('SELECTED',),
47 'FETCH': ('SELECTED',),
48 'GETACL': ('AUTH', 'SELECTED'),
49 'LIST': ('AUTH', 'SELECTED'),
50 'LOGIN': ('NONAUTH',),
51 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
52 'LSUB': ('AUTH', 'SELECTED'),
53 'NAMESPACE': ('AUTH', 'SELECTED'),
54 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
55 'PARTIAL': ('SELECTED',),
56 'RENAME': ('AUTH', 'SELECTED'),
57 'SEARCH': ('SELECTED',),
58 'SELECT': ('AUTH', 'SELECTED'),
59 'SETACL': ('AUTH', 'SELECTED'),
60 'SORT': ('SELECTED',),
61 'STATUS': ('AUTH', 'SELECTED'),
62 'STORE': ('SELECTED',),
63 'SUBSCRIBE': ('AUTH', 'SELECTED'),
65 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
68 # Patterns to match server responses
70 Continuation
= re
.compile(r
'\+( (?P<data>.*))?')
71 Flags
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
72 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
73 r
'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
74 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
75 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
77 Literal
= re
.compile(r
'.*{(?P<size>\d+)}$')
78 Response_code
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
79 Untagged_response
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
80 Untagged_status
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
86 """IMAP4 client class.
88 Instantiate with: IMAP4([host[, port]])
90 host - host's name (default: localhost);
91 port - port number (default: standard IMAP4 port).
93 All IMAP4rev1 commands are supported by methods of the same
96 All arguments to commands are converted to strings, except for
97 AUTHENTICATE, and the last argument to APPEND which is passed as
98 an IMAP4 literal. If necessary (the string contains any
99 non-printing characters or white-space and isn't enclosed with
100 either parentheses or double quotes) each string is quoted.
101 However, the 'password' argument to the LOGIN command is always
102 quoted. If you want to avoid having an argument string quoted
103 (eg: the 'flags' argument to STORE) then enclose the string in
104 parentheses (eg: "(\Deleted)").
106 Each command returns a tuple: (type, [data, ...]) where 'type'
107 is usually 'OK' or 'NO', and 'data' is either the text from the
108 tagged response, or untagged results from command.
110 Errors raise the exception class <instance>.error("<reason>").
111 IMAP4 server errors raise <instance>.abort("<reason>"),
112 which is a sub-class of 'error'. Mailbox status changes
113 from READ-WRITE to READ-ONLY raise the exception class
114 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
116 "error" exceptions imply a program error.
117 "abort" exceptions imply the connection should be reset, and
118 the command re-tried.
119 "readonly" exceptions imply the command should be re-tried.
121 Note: to use this module, you must read the RFCs pertaining
122 to the IMAP4 protocol, as the semantics of the arguments to
123 each IMAP4 command are left to the invoker, not to mention
127 class error(Exception): pass # Logical errors - debug required
128 class abort(error
): pass # Service errors - close and retry
129 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
131 mustquote
= re
.compile(r
"[^\w!#$%&'*+,.:;<=>?^`|~-]")
133 def __init__(self
, host
= '', port
= IMAP4_PORT
):
137 self
.state
= 'LOGOUT'
138 self
.literal
= None # A literal argument to a command
139 self
.tagged_commands
= {} # Tagged commands awaiting response
140 self
.untagged_responses
= {} # {typ: [data, ...], ...}
141 self
.continuation_response
= '' # Last continuation response
142 self
.is_readonly
= None # READ-ONLY desired state
145 # Open socket to server.
147 self
.open(host
, port
)
149 # Create unique tag for this session,
150 # and compile tagged response matcher.
152 self
.tagpre
= Int2AP(random
.randint(0, 31999))
153 self
.tagre
= re
.compile(r
'(?P<tag>'
155 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
157 # Get server welcome message,
158 # request and store CAPABILITY response.
162 _mesg('imaplib version %s' % __version__
)
163 _mesg('new IMAP4 connection, tag=%s' % self
.tagpre
)
165 self
.welcome
= self
._get
_response
()
166 if self
.untagged_responses
.has_key('PREAUTH'):
168 elif self
.untagged_responses
.has_key('OK'):
169 self
.state
= 'NONAUTH'
171 raise self
.error(self
.welcome
)
174 self
._simple
_command
(cap
)
175 if not self
.untagged_responses
.has_key(cap
):
176 raise self
.error('no CAPABILITY response from server')
177 self
.capabilities
= tuple(self
.untagged_responses
[cap
][-1].upper().split())
181 _mesg('CAPABILITIES: %s' % `self
.capabilities`
)
183 for version
in AllowedVersions
:
184 if not version
in self
.capabilities
:
186 self
.PROTOCOL_VERSION
= version
189 raise self
.error('server not IMAP4 compliant')
192 def __getattr__(self
, attr
):
193 # Allow UPPERCASE variants of IMAP4 command methods.
194 if Commands
.has_key(attr
):
195 return getattr(self
, attr
.lower())
196 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
200 # Overridable methods
203 def open(self
, host
, port
):
204 """Setup connection to remote server on "host:port".
205 This connection will be used by the routines:
206 read, readline, send, shutdown.
208 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
209 self
.sock
.connect((self
.host
, self
.port
))
210 self
.file = self
.sock
.makefile('r')
213 def read(self
, size
):
214 """Read 'size' bytes from remote."""
215 return self
.file.read(size
)
219 """Read line from remote."""
220 return self
.file.readline()
223 def send(self
, data
):
224 """Send data to remote."""
229 """Close I/O established in "open"."""
235 """Return socket instance used to connect to IMAP4 server.
237 socket = <instance>.socket()
247 """Return most recent 'RECENT' responses if any exist,
248 else prompt server for an update using the 'NOOP' command.
250 (typ, [data]) = <instance>.recent()
252 'data' is None if no new messages,
253 else list of RECENT responses, most recent last.
256 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
259 typ
, dat
= self
.noop() # Prod server for response
260 return self
._untagged
_response
(typ
, dat
, name
)
263 def response(self
, code
):
264 """Return data for response 'code' if received, or None.
266 Old value for response 'code' is cleared.
268 (code, [data]) = <instance>.response(code)
270 return self
._untagged
_response
(code
, [None], code
.upper())
277 def append(self
, mailbox
, flags
, date_time
, message
):
278 """Append message to named mailbox.
280 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
282 All args except `message' can be None.
288 if (flags
[0],flags
[-1]) != ('(',')'):
289 flags
= '(%s)' % flags
293 date_time
= Time2Internaldate(date_time
)
296 self
.literal
= message
297 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
300 def authenticate(self
, mechanism
, authobject
):
301 """Authenticate command - requires response processing.
303 'mechanism' specifies which authentication mechanism is to
304 be used - it must appear in <instance>.capabilities in the
305 form AUTH=<mechanism>.
307 'authobject' must be a callable object:
309 data = authobject(response)
311 It will be called to process server continuation responses.
312 It should return data that will be encoded and sent to server.
313 It should return None if the client abort response '*' should
316 mech
= mechanism
.upper()
317 cap
= 'AUTH=%s' % mech
318 if not cap
in self
.capabilities
:
319 raise self
.error("Server doesn't allow %s authentication." % mech
)
320 self
.literal
= _Authenticator(authobject
).process
321 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
323 raise self
.error(dat
[-1])
329 """Checkpoint mailbox on server.
331 (typ, [data]) = <instance>.check()
333 return self
._simple
_command
('CHECK')
337 """Close currently selected mailbox.
339 Deleted messages are removed from writable mailbox.
340 This is the recommended command before 'LOGOUT'.
342 (typ, [data]) = <instance>.close()
345 typ
, dat
= self
._simple
_command
('CLOSE')
351 def copy(self
, message_set
, new_mailbox
):
352 """Copy 'message_set' messages onto end of 'new_mailbox'.
354 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
356 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
359 def create(self
, mailbox
):
360 """Create new mailbox.
362 (typ, [data]) = <instance>.create(mailbox)
364 return self
._simple
_command
('CREATE', mailbox
)
367 def delete(self
, mailbox
):
368 """Delete old mailbox.
370 (typ, [data]) = <instance>.delete(mailbox)
372 return self
._simple
_command
('DELETE', mailbox
)
376 """Permanently remove deleted items from selected mailbox.
378 Generates 'EXPUNGE' response for each deleted message.
380 (typ, [data]) = <instance>.expunge()
382 'data' is list of 'EXPUNGE'd message numbers in order received.
385 typ
, dat
= self
._simple
_command
(name
)
386 return self
._untagged
_response
(typ
, dat
, name
)
389 def fetch(self
, message_set
, message_parts
):
390 """Fetch (parts of) messages.
392 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
394 'message_parts' should be a string of selected parts
395 enclosed in parentheses, eg: "(UID BODY[TEXT])".
397 'data' are tuples of message part envelope and data.
400 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
401 return self
._untagged
_response
(typ
, dat
, name
)
404 def getacl(self
, mailbox
):
405 """Get the ACLs for a mailbox.
407 (typ, [data]) = <instance>.getacl(mailbox)
409 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
410 return self
._untagged
_response
(typ
, dat
, 'ACL')
413 def list(self
, directory
='""', pattern
='*'):
414 """List mailbox names in directory matching pattern.
416 (typ, [data]) = <instance>.list(directory='""', pattern='*')
418 'data' is list of LIST responses.
421 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
422 return self
._untagged
_response
(typ
, dat
, name
)
425 def login(self
, user
, password
):
426 """Identify client using plaintext password.
428 (typ, [data]) = <instance>.login(user, password)
430 NB: 'password' will be quoted.
432 #if not 'AUTH=LOGIN' in self.capabilities:
433 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
434 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
436 raise self
.error(dat
[-1])
442 """Shutdown connection to server.
444 (typ, [data]) = <instance>.logout()
446 Returns server 'BYE' response.
448 self
.state
= 'LOGOUT'
449 try: typ
, dat
= self
._simple
_command
('LOGOUT')
450 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
452 if self
.untagged_responses
.has_key('BYE'):
453 return 'BYE', self
.untagged_responses
['BYE']
457 def lsub(self
, directory
='""', pattern
='*'):
458 """List 'subscribed' mailbox names in directory matching pattern.
460 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
462 'data' are tuples of message part envelope and data.
465 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
466 return self
._untagged
_response
(typ
, dat
, name
)
470 """ Returns IMAP namespaces ala rfc2342
472 (typ, [data, ...]) = <instance>.namespace()
475 typ
, dat
= self
._simple
_command
(name
)
476 return self
._untagged
_response
(typ
, dat
, name
)
480 """Send NOOP command.
482 (typ, data) = <instance>.noop()
486 _dump_ur(self
.untagged_responses
)
487 return self
._simple
_command
('NOOP')
490 def partial(self
, message_num
, message_part
, start
, length
):
491 """Fetch truncated part of a message.
493 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
495 'data' is tuple of message part envelope and data.
498 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
499 return self
._untagged
_response
(typ
, dat
, 'FETCH')
502 def rename(self
, oldmailbox
, newmailbox
):
503 """Rename old mailbox name to new.
505 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
507 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
510 def search(self
, charset
, *criteria
):
511 """Search mailbox for matching messages.
513 (typ, [data]) = <instance>.search(charset, criterium, ...)
515 'data' is space separated list of matching message numbers.
519 typ
, dat
= apply(self
._simple
_command
, (name
, 'CHARSET', charset
) + criteria
)
521 typ
, dat
= apply(self
._simple
_command
, (name
,) + criteria
)
522 return self
._untagged
_response
(typ
, dat
, name
)
525 def select(self
, mailbox
='INBOX', readonly
=None):
528 Flush all untagged responses.
530 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
532 'data' is count of messages in mailbox ('EXISTS' response).
534 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
535 self
.untagged_responses
= {} # Flush old responses.
536 self
.is_readonly
= readonly
541 typ
, dat
= self
._simple
_command
(name
, mailbox
)
543 self
.state
= 'AUTH' # Might have been 'SELECTED'
545 self
.state
= 'SELECTED'
546 if self
.untagged_responses
.has_key('READ-ONLY') \
550 _dump_ur(self
.untagged_responses
)
551 raise self
.readonly('%s is not writable' % mailbox
)
552 return typ
, self
.untagged_responses
.get('EXISTS', [None])
555 def setacl(self
, mailbox
, who
, what
):
556 """Set a mailbox acl.
558 (typ, [data]) = <instance>.create(mailbox, who, what)
560 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
563 def sort(self
, sort_criteria
, charset
, *search_criteria
):
564 """IMAP4rev1 extension SORT command.
566 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
569 #if not name in self.capabilities: # Let the server decide!
570 # raise self.error('unimplemented extension command: %s' % name)
571 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
572 sort_criteria
= '(%s)' % sort_criteria
573 typ
, dat
= apply(self
._simple
_command
, (name
, sort_criteria
, charset
) + search_criteria
)
574 return self
._untagged
_response
(typ
, dat
, name
)
577 def status(self
, mailbox
, names
):
578 """Request named status conditions for mailbox.
580 (typ, [data]) = <instance>.status(mailbox, names)
583 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
584 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
585 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
586 return self
._untagged
_response
(typ
, dat
, name
)
589 def store(self
, message_set
, command
, flags
):
590 """Alters flag dispositions for messages in mailbox.
592 (typ, [data]) = <instance>.store(message_set, command, flags)
594 if (flags
[0],flags
[-1]) != ('(',')'):
595 flags
= '(%s)' % flags
# Avoid quoting the flags
596 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
597 return self
._untagged
_response
(typ
, dat
, 'FETCH')
600 def subscribe(self
, mailbox
):
601 """Subscribe to new mailbox.
603 (typ, [data]) = <instance>.subscribe(mailbox)
605 return self
._simple
_command
('SUBSCRIBE', mailbox
)
608 def uid(self
, command
, *args
):
609 """Execute "command arg ..." with messages identified by UID,
610 rather than message number.
612 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
614 Returns response appropriate to 'command'.
616 command
= command
.upper()
617 if not Commands
.has_key(command
):
618 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
619 if self
.state
not in Commands
[command
]:
620 raise self
.error('command %s illegal in state %s'
621 % (command
, self
.state
))
623 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
624 if command
in ('SEARCH', 'SORT'):
628 return self
._untagged
_response
(typ
, dat
, name
)
631 def unsubscribe(self
, mailbox
):
632 """Unsubscribe from old mailbox.
634 (typ, [data]) = <instance>.unsubscribe(mailbox)
636 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
639 def xatom(self
, name
, *args
):
640 """Allow simple extension commands
641 notified by server in CAPABILITY response.
643 Assumes command is legal in current state.
645 (typ, [data]) = <instance>.xatom(name, arg, ...)
647 Returns response appropriate to extension command `name'.
650 #if not name in self.capabilities: # Let the server decide!
651 # raise self.error('unknown extension command: %s' % name)
652 if not Commands
.has_key(name
):
653 Commands
[name
] = (self
.state
,)
654 return apply(self
._simple
_command
, (name
,) + args
)
661 def _append_untagged(self
, typ
, dat
):
663 if dat
is None: dat
= ''
664 ur
= self
.untagged_responses
667 _mesg('untagged_responses[%s] %s += ["%s"]' %
668 (typ
, len(ur
.get(typ
,'')), dat
))
675 def _check_bye(self
):
676 bye
= self
.untagged_responses
.get('BYE')
678 raise self
.abort(bye
[-1])
681 def _command(self
, name
, *args
):
683 if self
.state
not in Commands
[name
]:
686 'command %s illegal in state %s' % (name
, self
.state
))
688 for typ
in ('OK', 'NO', 'BAD'):
689 if self
.untagged_responses
.has_key(typ
):
690 del self
.untagged_responses
[typ
]
692 if self
.untagged_responses
.has_key('READ-ONLY') \
693 and not self
.is_readonly
:
694 raise self
.readonly('mailbox status changed to READ-ONLY')
696 tag
= self
._new
_tag
()
697 data
= '%s %s' % (tag
, name
)
699 if arg
is None: continue
700 data
= '%s %s' % (data
, self
._checkquote
(arg
))
702 literal
= self
.literal
703 if literal
is not None:
705 if type(literal
) is type(self
._command
):
709 data
= '%s {%s}' % (data
, len(literal
))
718 self
.send('%s%s' % (data
, CRLF
))
719 except (socket
.error
, OSError), val
:
720 raise self
.abort('socket error: %s' % val
)
726 # Wait for continuation response
728 while self
._get
_response
():
729 if self
.tagged_commands
[tag
]: # BAD/NO?
735 literal
= literator(self
.continuation_response
)
739 _mesg('write literal size %s' % len(literal
))
744 except (socket
.error
, OSError), val
:
745 raise self
.abort('socket error: %s' % val
)
753 def _command_complete(self
, name
, tag
):
756 typ
, data
= self
._get
_tagged
_response
(tag
)
757 except self
.abort
, val
:
758 raise self
.abort('command: %s => %s' % (name
, val
))
759 except self
.error
, val
:
760 raise self
.error('command: %s => %s' % (name
, val
))
763 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
767 def _get_response(self
):
769 # Read response and store.
771 # Returns None for continuation responses,
772 # otherwise first response line received.
774 resp
= self
._get
_line
()
776 # Command completion response?
778 if self
._match
(self
.tagre
, resp
):
779 tag
= self
.mo
.group('tag')
780 if not self
.tagged_commands
.has_key(tag
):
781 raise self
.abort('unexpected tagged response: %s' % resp
)
783 typ
= self
.mo
.group('type')
784 dat
= self
.mo
.group('data')
785 self
.tagged_commands
[tag
] = (typ
, [dat
])
789 # '*' (untagged) responses?
791 if not self
._match
(Untagged_response
, resp
):
792 if self
._match
(Untagged_status
, resp
):
793 dat2
= self
.mo
.group('data2')
796 # Only other possibility is '+' (continuation) response...
798 if self
._match
(Continuation
, resp
):
799 self
.continuation_response
= self
.mo
.group('data')
800 return None # NB: indicates continuation
802 raise self
.abort("unexpected response: '%s'" % resp
)
804 typ
= self
.mo
.group('type')
805 dat
= self
.mo
.group('data')
806 if dat
is None: dat
= '' # Null untagged response
807 if dat2
: dat
= dat
+ ' ' + dat2
809 # Is there a literal to come?
811 while self
._match
(Literal
, dat
):
813 # Read literal direct from connection.
815 size
= int(self
.mo
.group('size'))
818 _mesg('read literal size %s' % size
)
819 data
= self
.read(size
)
821 # Store response with literal as tuple
823 self
._append
_untagged
(typ
, (dat
, data
))
825 # Read trailer - possibly containing another literal
827 dat
= self
._get
_line
()
829 self
._append
_untagged
(typ
, dat
)
831 # Bracketed response information?
833 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
834 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
837 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
838 _mesg('%s response: %s' % (typ
, dat
))
843 def _get_tagged_response(self
, tag
):
846 result
= self
.tagged_commands
[tag
]
847 if result
is not None:
848 del self
.tagged_commands
[tag
]
851 # Some have reported "unexpected response" exceptions.
852 # Note that ignoring them here causes loops.
853 # Instead, send me details of the unexpected response and
854 # I'll update the code in `_get_response()'.
858 except self
.abort
, val
:
867 line
= self
.readline()
869 raise self
.abort('socket error: EOF')
871 # Protocol mandates all lines terminated by CRLF
882 def _match(self
, cre
, s
):
884 # Run compiled regular expression match method on 's'.
885 # Save result, return success.
887 self
.mo
= cre
.match(s
)
889 if self
.mo
is not None and self
.debug
>= 5:
890 _mesg("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
891 return self
.mo
is not None
896 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
897 self
.tagnum
= self
.tagnum
+ 1
898 self
.tagged_commands
[tag
] = None
902 def _checkquote(self
, arg
):
904 # Must quote command args if non-alphanumeric chars present,
905 # and not already quoted.
907 if type(arg
) is not type(''):
909 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
911 if self
.mustquote
.search(arg
) is None:
913 return self
._quote
(arg
)
916 def _quote(self
, arg
):
918 arg
= arg
.replace('\\', '\\\\')
919 arg
= arg
.replace('"', '\\"')
924 def _simple_command(self
, name
, *args
):
926 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
929 def _untagged_response(self
, typ
, dat
, name
):
933 if not self
.untagged_responses
.has_key(name
):
935 data
= self
.untagged_responses
[name
]
938 _mesg('untagged_responses[%s] => %s' % (name
, data
))
939 del self
.untagged_responses
[name
]
944 class _Authenticator
:
946 """Private class to provide en/decoding
947 for base64-based authentication conversation.
950 def __init__(self
, mechinst
):
951 self
.mech
= mechinst
# Callable object to provide/process data
953 def process(self
, data
):
954 ret
= self
.mech(self
.decode(data
))
956 return '*' # Abort conversation
957 return self
.encode(ret
)
959 def encode(self
, inp
):
961 # Invoke binascii.b2a_base64 iteratively with
962 # short even length buffers, strip the trailing
963 # line feed from the result and append. "Even"
964 # means a number that factors to both 6 and 8,
965 # so when it gets to the end of the 8-bit input
966 # there's no partial 6-bit output.
976 e
= binascii
.b2a_base64(t
)
981 def decode(self
, inp
):
984 return binascii
.a2b_base64(inp
)
988 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
989 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
991 def Internaldate2tuple(resp
):
992 """Convert IMAP4 INTERNALDATE to UT.
994 Returns Python time module tuple.
997 mo
= InternalDate
.match(resp
)
1001 mon
= Mon2num
[mo
.group('mon')]
1002 zonen
= mo
.group('zonen')
1004 day
= int(mo
.group('day'))
1005 year
= int(mo
.group('year'))
1006 hour
= int(mo
.group('hour'))
1007 min = int(mo
.group('min'))
1008 sec
= int(mo
.group('sec'))
1009 zoneh
= int(mo
.group('zoneh'))
1010 zonem
= int(mo
.group('zonem'))
1012 # INTERNALDATE timezone must be subtracted to get UT
1014 zone
= (zoneh
*60 + zonem
)*60
1018 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1020 utc
= time
.mktime(tt
)
1022 # Following is necessary because the time module has no 'mkgmtime'.
1023 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1025 lt
= time
.localtime(utc
)
1026 if time
.daylight
and lt
[-1]:
1027 zone
= zone
+ time
.altzone
1029 zone
= zone
+ time
.timezone
1031 return time
.localtime(utc
- zone
)
1037 """Convert integer to A-P string representation."""
1039 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1042 num
, mod
= divmod(num
, 16)
1048 def ParseFlags(resp
):
1050 """Convert IMAP4 flags response to python tuple."""
1052 mo
= Flags
.match(resp
)
1056 return tuple(mo
.group('flags').split())
1059 def Time2Internaldate(date_time
):
1061 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1063 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1066 dttype
= type(date_time
)
1067 if dttype
is type(1) or dttype
is type(1.1):
1068 tt
= time
.localtime(date_time
)
1069 elif dttype
is type(()):
1071 elif dttype
is type(""):
1072 return date_time
# Assume in correct format
1073 else: raise ValueError
1075 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1078 if time
.daylight
and tt
[-1]:
1079 zone
= -time
.altzone
1081 zone
= -time
.timezone
1082 return '"' + dt
+ " %+02d%02d" % divmod(zone
/60, 60) + '"'
1088 def _mesg(s
, secs
=None):
1091 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1092 sys
.stderr
.write(' %s.%02d %s\n' % (tm
, (secs
*100)%100, s
))
1096 # Dump untagged responses (in `dict').
1100 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1101 _mesg('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1103 _cmd_log
= [] # Last `_cmd_log_len' interactions
1107 # Keep log of last `_cmd_log_len' interactions for debugging.
1108 if len(_cmd_log
) == _cmd_log_len
:
1110 _cmd_log
.append((time
.time(), line
))
1113 _mesg('last %d IMAP4 interactions:' % len(_cmd_log
))
1114 for secs
,line
in _cmd_log
:
1119 if __name__
== '__main__':
1121 import getopt
, getpass
1124 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:')
1125 except getopt
.error
, val
:
1128 for opt
,val
in optlist
:
1132 if not args
: args
= ('',)
1136 USER
= getpass
.getuser()
1137 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1139 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':CRLF
}
1141 ('login', (USER
, PASSWD
)),
1142 ('create', ('/tmp/xxx 1',)),
1143 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1144 ('CREATE', ('/tmp/yyz 2',)),
1145 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1146 ('list', ('/tmp', 'yy*')),
1147 ('select', ('/tmp/yyz 2',)),
1148 ('search', (None, 'SUBJECT', 'test')),
1149 ('partial', ('1', 'RFC822', 1, 1024)),
1150 ('store', ('1', 'FLAGS', '(\Deleted)')),
1159 ('response',('UIDVALIDITY',)),
1160 ('uid', ('SEARCH', 'ALL')),
1161 ('response', ('EXISTS',)),
1162 ('append', (None, None, None, test_mesg
)),
1168 _mesg('%s %s' % (cmd
, args
))
1169 typ
, dat
= apply(getattr(M
, cmd
), args
)
1170 _mesg('%s => %s %s' % (cmd
, typ
, dat
))
1175 _mesg('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1176 _mesg('CAPABILITIES = %s' % `M
.capabilities`
)
1178 for cmd
,args
in test_seq1
:
1181 for ml
in run('list', ('/tmp/', 'yy%')):
1182 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1183 if mo
: path
= mo
.group(1)
1184 else: path
= ml
.split()[-1]
1185 run('delete', (path
,))
1187 for cmd
,args
in test_seq2
:
1188 dat
= run(cmd
, args
)
1190 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1193 uid
= dat
[-1].split()
1194 if not uid
: continue
1195 run('uid', ('FETCH', '%s' % uid
[-1],
1196 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1198 print '\nAll tests OK.'
1201 print '\nTests failed.'
1205 If you would like to see debugging output,