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.
18 # IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
19 # GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
20 # PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002.
24 import binascii
, os
, random
, re
, socket
, sys
, time
26 __all__
= ["IMAP4", "IMAP4_SSL", "Internaldate2tuple",
27 "Int2AP", "ParseFlags", "Time2Internaldate"]
35 AllowedVersions
= ('IMAP4REV1', 'IMAP4') # Most recent first
41 'APPEND': ('AUTH', 'SELECTED'),
42 'AUTHENTICATE': ('NONAUTH',),
43 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
44 'CHECK': ('SELECTED',),
45 'CLOSE': ('SELECTED',),
46 'COPY': ('SELECTED',),
47 'CREATE': ('AUTH', 'SELECTED'),
48 'DELETE': ('AUTH', 'SELECTED'),
49 'EXAMINE': ('AUTH', 'SELECTED'),
50 'EXPUNGE': ('SELECTED',),
51 'FETCH': ('SELECTED',),
52 'GETACL': ('AUTH', 'SELECTED'),
53 'GETQUOTA': ('AUTH', 'SELECTED'),
54 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
55 'LIST': ('AUTH', 'SELECTED'),
56 'LOGIN': ('NONAUTH',),
57 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
58 'LSUB': ('AUTH', 'SELECTED'),
59 'NAMESPACE': ('AUTH', 'SELECTED'),
60 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
61 'PARTIAL': ('SELECTED',), # NB: obsolete
62 'PROXYAUTH': ('AUTH',),
63 'RENAME': ('AUTH', 'SELECTED'),
64 'SEARCH': ('SELECTED',),
65 'SELECT': ('AUTH', 'SELECTED'),
66 'SETACL': ('AUTH', 'SELECTED'),
67 'SETQUOTA': ('AUTH', 'SELECTED'),
68 'SORT': ('SELECTED',),
69 'STATUS': ('AUTH', 'SELECTED'),
70 'STORE': ('SELECTED',),
71 'SUBSCRIBE': ('AUTH', 'SELECTED'),
73 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
76 # Patterns to match server responses
78 Continuation
= re
.compile(r
'\+( (?P<data>.*))?')
79 Flags
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
80 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
81 r
'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
82 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
83 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
85 Literal
= re
.compile(r
'.*{(?P<size>\d+)}$')
86 MapCRLF
= re
.compile(r
'\r\n|\r|\n')
87 Response_code
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
88 Untagged_response
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
89 Untagged_status
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
95 """IMAP4 client class.
97 Instantiate with: IMAP4([host[, port]])
99 host - host's name (default: localhost);
100 port - port number (default: standard IMAP4 port).
102 All IMAP4rev1 commands are supported by methods of the same
103 name (in lower-case).
105 All arguments to commands are converted to strings, except for
106 AUTHENTICATE, and the last argument to APPEND which is passed as
107 an IMAP4 literal. If necessary (the string contains any
108 non-printing characters or white-space and isn't enclosed with
109 either parentheses or double quotes) each string is quoted.
110 However, the 'password' argument to the LOGIN command is always
111 quoted. If you want to avoid having an argument string quoted
112 (eg: the 'flags' argument to STORE) then enclose the string in
113 parentheses (eg: "(\Deleted)").
115 Each command returns a tuple: (type, [data, ...]) where 'type'
116 is usually 'OK' or 'NO', and 'data' is either the text from the
117 tagged response, or untagged results from command. Each 'data'
118 is either a string, or a tuple. If a tuple, then the first part
119 is the header of the response, and the second part contains
120 the data (ie: 'literal' value).
122 Errors raise the exception class <instance>.error("<reason>").
123 IMAP4 server errors raise <instance>.abort("<reason>"),
124 which is a sub-class of 'error'. Mailbox status changes
125 from READ-WRITE to READ-ONLY raise the exception class
126 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
128 "error" exceptions imply a program error.
129 "abort" exceptions imply the connection should be reset, and
130 the command re-tried.
131 "readonly" exceptions imply the command should be re-tried.
133 Note: to use this module, you must read the RFCs pertaining
134 to the IMAP4 protocol, as the semantics of the arguments to
135 each IMAP4 command are left to the invoker, not to mention
139 class error(Exception): pass # Logical errors - debug required
140 class abort(error
): pass # Service errors - close and retry
141 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
143 mustquote
= re
.compile(r
"[^\w!#$%&'*+,.:;<=>?^`|~-]")
145 def __init__(self
, host
= '', port
= IMAP4_PORT
):
147 self
.state
= 'LOGOUT'
148 self
.literal
= None # A literal argument to a command
149 self
.tagged_commands
= {} # Tagged commands awaiting response
150 self
.untagged_responses
= {} # {typ: [data, ...], ...}
151 self
.continuation_response
= '' # Last continuation response
152 self
.is_readonly
= None # READ-ONLY desired state
155 # Open socket to server.
157 self
.open(host
, port
)
159 # Create unique tag for this session,
160 # and compile tagged response matcher.
162 self
.tagpre
= Int2AP(random
.randint(0, 31999))
163 self
.tagre
= re
.compile(r
'(?P<tag>'
165 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
167 # Get server welcome message,
168 # request and store CAPABILITY response.
171 self
._cmd
_log
_len
= 10
172 self
._cmd
_log
_idx
= 0
173 self
._cmd
_log
= {} # Last `_cmd_log_len' interactions
175 self
._mesg
('imaplib version %s' % __version__
)
176 self
._mesg
('new IMAP4 connection, tag=%s' % self
.tagpre
)
178 self
.welcome
= self
._get
_response
()
179 if 'PREAUTH' in self
.untagged_responses
:
181 elif 'OK' in self
.untagged_responses
:
182 self
.state
= 'NONAUTH'
184 raise self
.error(self
.welcome
)
187 self
._simple
_command
(cap
)
188 if not cap
in self
.untagged_responses
:
189 raise self
.error('no CAPABILITY response from server')
190 self
.capabilities
= tuple(self
.untagged_responses
[cap
][-1].upper().split())
194 self
._mesg
('CAPABILITIES: %s' % `self
.capabilities`
)
196 for version
in AllowedVersions
:
197 if not version
in self
.capabilities
:
199 self
.PROTOCOL_VERSION
= version
202 raise self
.error('server not IMAP4 compliant')
205 def __getattr__(self
, attr
):
206 # Allow UPPERCASE variants of IMAP4 command methods.
208 return getattr(self
, attr
.lower())
209 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
213 # Overridable methods
216 def open(self
, host
= '', port
= IMAP4_PORT
):
217 """Setup connection to remote server on "host:port"
218 (default: localhost:standard IMAP4 port).
219 This connection will be used by the routines:
220 read, readline, send, shutdown.
224 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
225 self
.sock
.connect((host
, port
))
226 self
.file = self
.sock
.makefile('rb')
229 def read(self
, size
):
230 """Read 'size' bytes from remote."""
231 return self
.file.read(size
)
235 """Read line from remote."""
236 return self
.file.readline()
239 def send(self
, data
):
240 """Send data to remote."""
241 self
.sock
.sendall(data
)
245 """Close I/O established in "open"."""
251 """Return socket instance used to connect to IMAP4 server.
253 socket = <instance>.socket()
263 """Return most recent 'RECENT' responses if any exist,
264 else prompt server for an update using the 'NOOP' command.
266 (typ, [data]) = <instance>.recent()
268 'data' is None if no new messages,
269 else list of RECENT responses, most recent last.
272 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
275 typ
, dat
= self
.noop() # Prod server for response
276 return self
._untagged
_response
(typ
, dat
, name
)
279 def response(self
, code
):
280 """Return data for response 'code' if received, or None.
282 Old value for response 'code' is cleared.
284 (code, [data]) = <instance>.response(code)
286 return self
._untagged
_response
(code
, [None], code
.upper())
293 def append(self
, mailbox
, flags
, date_time
, message
):
294 """Append message to named mailbox.
296 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
298 All args except `message' can be None.
304 if (flags
[0],flags
[-1]) != ('(',')'):
305 flags
= '(%s)' % flags
309 date_time
= Time2Internaldate(date_time
)
312 self
.literal
= MapCRLF
.sub(CRLF
, message
)
313 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
316 def authenticate(self
, mechanism
, authobject
):
317 """Authenticate command - requires response processing.
319 'mechanism' specifies which authentication mechanism is to
320 be used - it must appear in <instance>.capabilities in the
321 form AUTH=<mechanism>.
323 'authobject' must be a callable object:
325 data = authobject(response)
327 It will be called to process server continuation responses.
328 It should return data that will be encoded and sent to server.
329 It should return None if the client abort response '*' should
332 mech
= mechanism
.upper()
333 # XXX: shouldn't this code be removed, not commented out?
334 #cap = 'AUTH=%s' % mech
335 #if not cap in self.capabilities: # Let the server decide!
336 # raise self.error("Server doesn't allow %s authentication." % mech)
337 self
.literal
= _Authenticator(authobject
).process
338 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
340 raise self
.error(dat
[-1])
346 """Checkpoint mailbox on server.
348 (typ, [data]) = <instance>.check()
350 return self
._simple
_command
('CHECK')
354 """Close currently selected mailbox.
356 Deleted messages are removed from writable mailbox.
357 This is the recommended command before 'LOGOUT'.
359 (typ, [data]) = <instance>.close()
362 typ
, dat
= self
._simple
_command
('CLOSE')
368 def copy(self
, message_set
, new_mailbox
):
369 """Copy 'message_set' messages onto end of 'new_mailbox'.
371 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
373 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
376 def create(self
, mailbox
):
377 """Create new mailbox.
379 (typ, [data]) = <instance>.create(mailbox)
381 return self
._simple
_command
('CREATE', mailbox
)
384 def delete(self
, mailbox
):
385 """Delete old mailbox.
387 (typ, [data]) = <instance>.delete(mailbox)
389 return self
._simple
_command
('DELETE', mailbox
)
393 """Permanently remove deleted items from selected mailbox.
395 Generates 'EXPUNGE' response for each deleted message.
397 (typ, [data]) = <instance>.expunge()
399 'data' is list of 'EXPUNGE'd message numbers in order received.
402 typ
, dat
= self
._simple
_command
(name
)
403 return self
._untagged
_response
(typ
, dat
, name
)
406 def fetch(self
, message_set
, message_parts
):
407 """Fetch (parts of) messages.
409 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
411 'message_parts' should be a string of selected parts
412 enclosed in parentheses, eg: "(UID BODY[TEXT])".
414 'data' are tuples of message part envelope and data.
417 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
418 return self
._untagged
_response
(typ
, dat
, name
)
421 def getacl(self
, mailbox
):
422 """Get the ACLs for a mailbox.
424 (typ, [data]) = <instance>.getacl(mailbox)
426 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
427 return self
._untagged
_response
(typ
, dat
, 'ACL')
430 def getquota(self
, root
):
431 """Get the quota root's resource usage and limits.
433 Part of the IMAP4 QUOTA extension defined in rfc2087.
435 (typ, [data]) = <instance>.getquota(root)
437 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
438 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
441 def getquotaroot(self
, mailbox
):
442 """Get the list of quota roots for the named mailbox.
444 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
446 typ
, dat
= self
._simple
_command
('GETQUOTA', mailbox
)
447 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
448 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
449 return typ
, [quotaroot
, quota
]
452 def list(self
, directory
='""', pattern
='*'):
453 """List mailbox names in directory matching pattern.
455 (typ, [data]) = <instance>.list(directory='""', pattern='*')
457 'data' is list of LIST responses.
460 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
461 return self
._untagged
_response
(typ
, dat
, name
)
464 def login(self
, user
, password
):
465 """Identify client using plaintext password.
467 (typ, [data]) = <instance>.login(user, password)
469 NB: 'password' will be quoted.
471 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
473 raise self
.error(dat
[-1])
478 def login_cram_md5(self
, user
, password
):
479 """ Force use of CRAM-MD5 authentication.
481 (typ, [data]) = <instance>.login_cram_md5(user, password)
483 self
.user
, self
.password
= user
, password
484 return self
.authenticate('CRAM-MD5', self
._CRAM
_MD
5_AUTH
)
487 def _CRAM_MD5_AUTH(self
, challenge
):
488 """ Authobject to use with CRAM-MD5 authentication. """
490 return self
.user
+ " " + hmac
.HMAC(self
.password
, challenge
).hexdigest()
494 """Shutdown connection to server.
496 (typ, [data]) = <instance>.logout()
498 Returns server 'BYE' response.
500 self
.state
= 'LOGOUT'
501 try: typ
, dat
= self
._simple
_command
('LOGOUT')
502 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
504 if 'BYE' in self
.untagged_responses
:
505 return 'BYE', self
.untagged_responses
['BYE']
509 def lsub(self
, directory
='""', pattern
='*'):
510 """List 'subscribed' mailbox names in directory matching pattern.
512 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
514 'data' are tuples of message part envelope and data.
517 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
518 return self
._untagged
_response
(typ
, dat
, name
)
522 """ Returns IMAP namespaces ala rfc2342
524 (typ, [data, ...]) = <instance>.namespace()
527 typ
, dat
= self
._simple
_command
(name
)
528 return self
._untagged
_response
(typ
, dat
, name
)
532 """Send NOOP command.
534 (typ, [data]) = <instance>.noop()
538 self
._dump
_ur
(self
.untagged_responses
)
539 return self
._simple
_command
('NOOP')
542 def partial(self
, message_num
, message_part
, start
, length
):
543 """Fetch truncated part of a message.
545 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
547 'data' is tuple of message part envelope and data.
550 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
551 return self
._untagged
_response
(typ
, dat
, 'FETCH')
554 def proxyauth(self
, user
):
555 """Assume authentication as "user".
557 Allows an authorised administrator to proxy into any user's
560 (typ, [data]) = <instance>.proxyauth(user)
564 return self
._simple
_command
('PROXYAUTH', user
)
567 def rename(self
, oldmailbox
, newmailbox
):
568 """Rename old mailbox name to new.
570 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
572 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
575 def search(self
, charset
, *criteria
):
576 """Search mailbox for matching messages.
578 (typ, [data]) = <instance>.search(charset, criterion, ...)
580 'data' is space separated list of matching message numbers.
584 typ
, dat
= self
._simple
_command
(name
, 'CHARSET', charset
, *criteria
)
586 typ
, dat
= self
._simple
_command
(name
, *criteria
)
587 return self
._untagged
_response
(typ
, dat
, name
)
590 def select(self
, mailbox
='INBOX', readonly
=None):
593 Flush all untagged responses.
595 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
597 'data' is count of messages in mailbox ('EXISTS' response).
599 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
600 self
.untagged_responses
= {} # Flush old responses.
601 self
.is_readonly
= readonly
602 if readonly
is not None:
606 typ
, dat
= self
._simple
_command
(name
, mailbox
)
608 self
.state
= 'AUTH' # Might have been 'SELECTED'
610 self
.state
= 'SELECTED'
611 if 'READ-ONLY' in self
.untagged_responses \
615 self
._dump
_ur
(self
.untagged_responses
)
616 raise self
.readonly('%s is not writable' % mailbox
)
617 return typ
, self
.untagged_responses
.get('EXISTS', [None])
620 def setacl(self
, mailbox
, who
, what
):
621 """Set a mailbox acl.
623 (typ, [data]) = <instance>.create(mailbox, who, what)
625 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
628 def setquota(self
, root
, limits
):
629 """Set the quota root's resource limits.
631 (typ, [data]) = <instance>.setquota(root, limits)
633 typ
, dat
= self
._simple
_command
('SETQUOTA', root
, limits
)
634 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
637 def sort(self
, sort_criteria
, charset
, *search_criteria
):
638 """IMAP4rev1 extension SORT command.
640 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
643 #if not name in self.capabilities: # Let the server decide!
644 # raise self.error('unimplemented extension command: %s' % name)
645 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
646 sort_criteria
= '(%s)' % sort_criteria
647 typ
, dat
= self
._simple
_command
(name
, sort_criteria
, charset
, *search_criteria
)
648 return self
._untagged
_response
(typ
, dat
, name
)
651 def status(self
, mailbox
, names
):
652 """Request named status conditions for mailbox.
654 (typ, [data]) = <instance>.status(mailbox, names)
657 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
658 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
659 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
660 return self
._untagged
_response
(typ
, dat
, name
)
663 def store(self
, message_set
, command
, flags
):
664 """Alters flag dispositions for messages in mailbox.
666 (typ, [data]) = <instance>.store(message_set, command, flags)
668 if (flags
[0],flags
[-1]) != ('(',')'):
669 flags
= '(%s)' % flags
# Avoid quoting the flags
670 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
671 return self
._untagged
_response
(typ
, dat
, 'FETCH')
674 def subscribe(self
, mailbox
):
675 """Subscribe to new mailbox.
677 (typ, [data]) = <instance>.subscribe(mailbox)
679 return self
._simple
_command
('SUBSCRIBE', mailbox
)
682 def uid(self
, command
, *args
):
683 """Execute "command arg ..." with messages identified by UID,
684 rather than message number.
686 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
688 Returns response appropriate to 'command'.
690 command
= command
.upper()
691 if not command
in Commands
:
692 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
693 if self
.state
not in Commands
[command
]:
694 raise self
.error('command %s illegal in state %s'
695 % (command
, self
.state
))
697 typ
, dat
= self
._simple
_command
(name
, command
, *args
)
698 if command
in ('SEARCH', 'SORT'):
702 return self
._untagged
_response
(typ
, dat
, name
)
705 def unsubscribe(self
, mailbox
):
706 """Unsubscribe from old mailbox.
708 (typ, [data]) = <instance>.unsubscribe(mailbox)
710 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
713 def xatom(self
, name
, *args
):
714 """Allow simple extension commands
715 notified by server in CAPABILITY response.
717 Assumes command is legal in current state.
719 (typ, [data]) = <instance>.xatom(name, arg, ...)
721 Returns response appropriate to extension command `name'.
724 #if not name in self.capabilities: # Let the server decide!
725 # raise self.error('unknown extension command: %s' % name)
726 if not name
in Commands
:
727 Commands
[name
] = (self
.state
,)
728 return self
._simple
_command
(name
, *args
)
735 def _append_untagged(self
, typ
, dat
):
737 if dat
is None: dat
= ''
738 ur
= self
.untagged_responses
741 self
._mesg
('untagged_responses[%s] %s += ["%s"]' %
742 (typ
, len(ur
.get(typ
,'')), dat
))
749 def _check_bye(self
):
750 bye
= self
.untagged_responses
.get('BYE')
752 raise self
.abort(bye
[-1])
755 def _command(self
, name
, *args
):
757 if self
.state
not in Commands
[name
]:
760 'command %s illegal in state %s' % (name
, self
.state
))
762 for typ
in ('OK', 'NO', 'BAD'):
763 if typ
in self
.untagged_responses
:
764 del self
.untagged_responses
[typ
]
766 if 'READ-ONLY' in self
.untagged_responses \
767 and not self
.is_readonly
:
768 raise self
.readonly('mailbox status changed to READ-ONLY')
770 tag
= self
._new
_tag
()
771 data
= '%s %s' % (tag
, name
)
773 if arg
is None: continue
774 data
= '%s %s' % (data
, self
._checkquote
(arg
))
776 literal
= self
.literal
777 if literal
is not None:
779 if type(literal
) is type(self
._command
):
783 data
= '%s {%s}' % (data
, len(literal
))
787 self
._mesg
('> %s' % data
)
789 self
._log
('> %s' % data
)
792 self
.send('%s%s' % (data
, CRLF
))
793 except (socket
.error
, OSError), val
:
794 raise self
.abort('socket error: %s' % val
)
800 # Wait for continuation response
802 while self
._get
_response
():
803 if self
.tagged_commands
[tag
]: # BAD/NO?
809 literal
= literator(self
.continuation_response
)
813 self
._mesg
('write literal size %s' % len(literal
))
818 except (socket
.error
, OSError), val
:
819 raise self
.abort('socket error: %s' % val
)
827 def _command_complete(self
, name
, tag
):
830 typ
, data
= self
._get
_tagged
_response
(tag
)
831 except self
.abort
, val
:
832 raise self
.abort('command: %s => %s' % (name
, val
))
833 except self
.error
, val
:
834 raise self
.error('command: %s => %s' % (name
, val
))
837 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
841 def _get_response(self
):
843 # Read response and store.
845 # Returns None for continuation responses,
846 # otherwise first response line received.
848 resp
= self
._get
_line
()
850 # Command completion response?
852 if self
._match
(self
.tagre
, resp
):
853 tag
= self
.mo
.group('tag')
854 if not tag
in self
.tagged_commands
:
855 raise self
.abort('unexpected tagged response: %s' % resp
)
857 typ
= self
.mo
.group('type')
858 dat
= self
.mo
.group('data')
859 self
.tagged_commands
[tag
] = (typ
, [dat
])
863 # '*' (untagged) responses?
865 if not self
._match
(Untagged_response
, resp
):
866 if self
._match
(Untagged_status
, resp
):
867 dat2
= self
.mo
.group('data2')
870 # Only other possibility is '+' (continuation) response...
872 if self
._match
(Continuation
, resp
):
873 self
.continuation_response
= self
.mo
.group('data')
874 return None # NB: indicates continuation
876 raise self
.abort("unexpected response: '%s'" % resp
)
878 typ
= self
.mo
.group('type')
879 dat
= self
.mo
.group('data')
880 if dat
is None: dat
= '' # Null untagged response
881 if dat2
: dat
= dat
+ ' ' + dat2
883 # Is there a literal to come?
885 while self
._match
(Literal
, dat
):
887 # Read literal direct from connection.
889 size
= int(self
.mo
.group('size'))
892 self
._mesg
('read literal size %s' % size
)
893 data
= self
.read(size
)
895 # Store response with literal as tuple
897 self
._append
_untagged
(typ
, (dat
, data
))
899 # Read trailer - possibly containing another literal
901 dat
= self
._get
_line
()
903 self
._append
_untagged
(typ
, dat
)
905 # Bracketed response information?
907 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
908 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
911 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
912 self
._mesg
('%s response: %s' % (typ
, dat
))
917 def _get_tagged_response(self
, tag
):
920 result
= self
.tagged_commands
[tag
]
921 if result
is not None:
922 del self
.tagged_commands
[tag
]
925 # Some have reported "unexpected response" exceptions.
926 # Note that ignoring them here causes loops.
927 # Instead, send me details of the unexpected response and
928 # I'll update the code in `_get_response()'.
932 except self
.abort
, val
:
941 line
= self
.readline()
943 raise self
.abort('socket error: EOF')
945 # Protocol mandates all lines terminated by CRLF
950 self
._mesg
('< %s' % line
)
952 self
._log
('< %s' % line
)
956 def _match(self
, cre
, s
):
958 # Run compiled regular expression match method on 's'.
959 # Save result, return success.
961 self
.mo
= cre
.match(s
)
963 if self
.mo
is not None and self
.debug
>= 5:
964 self
._mesg
("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
965 return self
.mo
is not None
970 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
971 self
.tagnum
= self
.tagnum
+ 1
972 self
.tagged_commands
[tag
] = None
976 def _checkquote(self
, arg
):
978 # Must quote command args if non-alphanumeric chars present,
979 # and not already quoted.
981 if type(arg
) is not type(''):
983 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
985 if self
.mustquote
.search(arg
) is None:
987 return self
._quote
(arg
)
990 def _quote(self
, arg
):
992 arg
= arg
.replace('\\', '\\\\')
993 arg
= arg
.replace('"', '\\"')
998 def _simple_command(self
, name
, *args
):
1000 return self
._command
_complete
(name
, self
._command
(name
, *args
))
1003 def _untagged_response(self
, typ
, dat
, name
):
1007 if not name
in self
.untagged_responses
:
1009 data
= self
.untagged_responses
.pop(name
)
1012 self
._mesg
('untagged_responses[%s] => %s' % (name
, data
))
1018 def _mesg(self
, s
, secs
=None):
1021 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1022 sys
.stderr
.write(' %s.%02d %s\n' % (tm
, (secs
*100)%100, s
))
1025 def _dump_ur(self
, dict):
1026 # Dump untagged responses (in `dict').
1030 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1031 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1033 def _log(self
, line
):
1034 # Keep log of last `_cmd_log_len' interactions for debugging.
1035 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, time
.time())
1036 self
._cmd
_log
_idx
+= 1
1037 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1038 self
._cmd
_log
_idx
= 0
1040 def print_log(self
):
1041 self
._mesg
('last %d IMAP4 interactions:' % len(self
._cmd
_log
))
1042 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1045 self
._mesg
(*self
._cmd
_log
[i
])
1049 if i
>= self
._cmd
_log
_len
:
1055 class IMAP4_SSL(IMAP4
):
1057 """IMAP4 client class over SSL connection
1059 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1061 host - host's name (default: localhost);
1062 port - port number (default: standard IMAP4 SSL port).
1063 keyfile - PEM formatted file that contains your private key (default: None);
1064 certfile - PEM formatted certificate chain file (default: None);
1066 for more documentation see the docstring of the parent class IMAP4.
1070 def __init__(self
, host
= '', port
= IMAP4_SSL_PORT
, keyfile
= None, certfile
= None):
1071 self
.keyfile
= keyfile
1072 self
.certfile
= certfile
1073 IMAP4
.__init__(self
, host
, port
)
1076 def open(self
, host
= '', port
= IMAP4_SSL_PORT
):
1077 """Setup connection to remote server on "host:port".
1078 (default: localhost:standard IMAP4 SSL port).
1079 This connection will be used by the routines:
1080 read, readline, send, shutdown.
1084 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
1085 self
.sock
.connect((host
, port
))
1086 self
.sslobj
= socket
.ssl(self
.sock
, self
.keyfile
, self
.certfile
)
1089 def read(self
, size
):
1090 """Read 'size' bytes from remote."""
1091 # sslobj.read() sometimes returns < size bytes
1092 data
= self
.sslobj
.read(size
)
1093 while len(data
) < size
:
1094 data
+= self
.sslobj
.read(size
-len(data
))
1100 """Read line from remote."""
1101 # NB: socket.ssl needs a "readline" method, or perhaps a "makefile" method.
1104 char
= self
.sslobj
.read(1)
1106 if char
== "\n": return line
1109 def send(self
, data
):
1110 """Send data to remote."""
1111 # NB: socket.ssl needs a "sendall" method to match socket objects.
1114 sent
= self
.sslobj
.write(data
)
1118 bytes
= bytes
- sent
1122 """Close I/O established in "open"."""
1127 """Return socket instance used to connect to IMAP4 server.
1129 socket = <instance>.socket()
1135 """Return SSLObject instance used to communicate with the IMAP4 server.
1137 ssl = <instance>.socket.ssl()
1143 class IMAP4_stream(IMAP4
):
1145 """IMAP4 client class over a stream
1147 Instantiate with: IMAP4_stream(command)
1149 where "command" is a string that can be passed to os.popen2()
1151 for more documentation see the docstring of the parent class IMAP4.
1155 def __init__(self
, command
):
1156 self
.command
= command
1157 IMAP4
.__init__(self
)
1160 def open(self
, host
= None, port
= None):
1161 """Setup a stream connection.
1162 This connection will be used by the routines:
1163 read, readline, send, shutdown.
1165 self
.host
= None # For compatibility with parent class
1169 self
.writefile
, self
.readfile
= os
.popen2(self
.command
)
1172 def read(self
, size
):
1173 """Read 'size' bytes from remote."""
1174 return self
.readfile
.read(size
)
1178 """Read line from remote."""
1179 return self
.readfile
.readline()
1182 def send(self
, data
):
1183 """Send data to remote."""
1184 self
.writefile
.write(data
)
1185 self
.writefile
.flush()
1189 """Close I/O established in "open"."""
1190 self
.readfile
.close()
1191 self
.writefile
.close()
1195 class _Authenticator
:
1197 """Private class to provide en/decoding
1198 for base64-based authentication conversation.
1201 def __init__(self
, mechinst
):
1202 self
.mech
= mechinst
# Callable object to provide/process data
1204 def process(self
, data
):
1205 ret
= self
.mech(self
.decode(data
))
1207 return '*' # Abort conversation
1208 return self
.encode(ret
)
1210 def encode(self
, inp
):
1212 # Invoke binascii.b2a_base64 iteratively with
1213 # short even length buffers, strip the trailing
1214 # line feed from the result and append. "Even"
1215 # means a number that factors to both 6 and 8,
1216 # so when it gets to the end of the 8-bit input
1217 # there's no partial 6-bit output.
1227 e
= binascii
.b2a_base64(t
)
1232 def decode(self
, inp
):
1235 return binascii
.a2b_base64(inp
)
1239 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1240 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1242 def Internaldate2tuple(resp
):
1243 """Convert IMAP4 INTERNALDATE to UT.
1245 Returns Python time module tuple.
1248 mo
= InternalDate
.match(resp
)
1252 mon
= Mon2num
[mo
.group('mon')]
1253 zonen
= mo
.group('zonen')
1255 day
= int(mo
.group('day'))
1256 year
= int(mo
.group('year'))
1257 hour
= int(mo
.group('hour'))
1258 min = int(mo
.group('min'))
1259 sec
= int(mo
.group('sec'))
1260 zoneh
= int(mo
.group('zoneh'))
1261 zonem
= int(mo
.group('zonem'))
1263 # INTERNALDATE timezone must be subtracted to get UT
1265 zone
= (zoneh
*60 + zonem
)*60
1269 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1271 utc
= time
.mktime(tt
)
1273 # Following is necessary because the time module has no 'mkgmtime'.
1274 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1276 lt
= time
.localtime(utc
)
1277 if time
.daylight
and lt
[-1]:
1278 zone
= zone
+ time
.altzone
1280 zone
= zone
+ time
.timezone
1282 return time
.localtime(utc
- zone
)
1288 """Convert integer to A-P string representation."""
1290 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1293 num
, mod
= divmod(num
, 16)
1299 def ParseFlags(resp
):
1301 """Convert IMAP4 flags response to python tuple."""
1303 mo
= Flags
.match(resp
)
1307 return tuple(mo
.group('flags').split())
1310 def Time2Internaldate(date_time
):
1312 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1314 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1317 if isinstance(date_time
, (int, float)):
1318 tt
= time
.localtime(date_time
)
1319 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1321 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1322 return date_time
# Assume in correct format
1324 raise ValueError("date_time not of a known type")
1326 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1329 if time
.daylight
and tt
[-1]:
1330 zone
= -time
.altzone
1332 zone
= -time
.timezone
1333 return '"' + dt
+ " %+03d%02d" % divmod(zone
/60, 60) + '"'
1337 if __name__
== '__main__':
1339 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1340 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1341 # to test the IMAP4_stream class
1343 import getopt
, getpass
1346 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:s:')
1347 except getopt
.error
, val
:
1348 optlist
, args
= (), ()
1350 stream_command
= None
1351 for opt
,val
in optlist
:
1355 stream_command
= val
1356 if not args
: args
= (stream_command
,)
1358 if not args
: args
= ('',)
1362 USER
= getpass
.getuser()
1363 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1365 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':'\n'}
1367 ('login', (USER
, PASSWD
)),
1368 ('create', ('/tmp/xxx 1',)),
1369 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1370 ('CREATE', ('/tmp/yyz 2',)),
1371 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1372 ('list', ('/tmp', 'yy*')),
1373 ('select', ('/tmp/yyz 2',)),
1374 ('search', (None, 'SUBJECT', 'test')),
1375 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1376 ('store', ('1', 'FLAGS', '(\Deleted)')),
1385 ('response',('UIDVALIDITY',)),
1386 ('uid', ('SEARCH', 'ALL')),
1387 ('response', ('EXISTS',)),
1388 ('append', (None, None, None, test_mesg
)),
1394 M
._mesg
('%s %s' % (cmd
, args
))
1395 typ
, dat
= getattr(M
, cmd
)(*args
)
1396 M
._mesg
('%s => %s %s' % (cmd
, typ
, dat
))
1397 if typ
== 'NO': raise dat
[0]
1402 M
= IMAP4_stream(stream_command
)
1405 if M
.state
== 'AUTH':
1406 test_seq1
= test_seq1
[1:] # Login not needed
1407 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1408 M
._mesg
('CAPABILITIES = %s' % `M
.capabilities`
)
1410 for cmd
,args
in test_seq1
:
1413 for ml
in run('list', ('/tmp/', 'yy%')):
1414 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1415 if mo
: path
= mo
.group(1)
1416 else: path
= ml
.split()[-1]
1417 run('delete', (path
,))
1419 for cmd
,args
in test_seq2
:
1420 dat
= run(cmd
, args
)
1422 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1425 uid
= dat
[-1].split()
1426 if not uid
: continue
1427 run('uid', ('FETCH', '%s' % uid
[-1],
1428 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1430 print '\nAll tests OK.'
1433 print '\nTests failed.'
1437 If you would like to see debugging output,