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.
21 # GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
25 import binascii
, os
, random
, re
, socket
, sys
, time
27 __all__
= ["IMAP4", "IMAP4_stream", "Internaldate2tuple",
28 "Int2AP", "ParseFlags", "Time2Internaldate"]
36 AllowedVersions
= ('IMAP4REV1', 'IMAP4') # Most recent first
42 'APPEND': ('AUTH', 'SELECTED'),
43 'AUTHENTICATE': ('NONAUTH',),
44 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
45 'CHECK': ('SELECTED',),
46 'CLOSE': ('SELECTED',),
47 'COPY': ('SELECTED',),
48 'CREATE': ('AUTH', 'SELECTED'),
49 'DELETE': ('AUTH', 'SELECTED'),
50 'DELETEACL': ('AUTH', 'SELECTED'),
51 'EXAMINE': ('AUTH', 'SELECTED'),
52 'EXPUNGE': ('SELECTED',),
53 'FETCH': ('SELECTED',),
54 'GETACL': ('AUTH', 'SELECTED'),
55 'GETANNOTATION':('AUTH', 'SELECTED'),
56 'GETQUOTA': ('AUTH', 'SELECTED'),
57 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
58 'MYRIGHTS': ('AUTH', 'SELECTED'),
59 'LIST': ('AUTH', 'SELECTED'),
60 'LOGIN': ('NONAUTH',),
61 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
62 'LSUB': ('AUTH', 'SELECTED'),
63 'NAMESPACE': ('AUTH', 'SELECTED'),
64 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
65 'PARTIAL': ('SELECTED',), # NB: obsolete
66 'PROXYAUTH': ('AUTH',),
67 'RENAME': ('AUTH', 'SELECTED'),
68 'SEARCH': ('SELECTED',),
69 'SELECT': ('AUTH', 'SELECTED'),
70 'SETACL': ('AUTH', 'SELECTED'),
71 'SETANNOTATION':('AUTH', 'SELECTED'),
72 'SETQUOTA': ('AUTH', 'SELECTED'),
73 'SORT': ('SELECTED',),
74 'STATUS': ('AUTH', 'SELECTED'),
75 'STORE': ('SELECTED',),
76 'SUBSCRIBE': ('AUTH', 'SELECTED'),
77 'THREAD': ('SELECTED',),
79 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
82 # Patterns to match server responses
84 Continuation
= re
.compile(r
'\+( (?P<data>.*))?')
85 Flags
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
86 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
87 r
'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
88 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
89 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
91 Literal
= re
.compile(r
'.*{(?P<size>\d+)}$')
92 MapCRLF
= re
.compile(r
'\r\n|\r|\n')
93 Response_code
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
94 Untagged_response
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
95 Untagged_status
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
101 """IMAP4 client class.
103 Instantiate with: IMAP4([host[, port]])
105 host - host's name (default: localhost);
106 port - port number (default: standard IMAP4 port).
108 All IMAP4rev1 commands are supported by methods of the same
109 name (in lower-case).
111 All arguments to commands are converted to strings, except for
112 AUTHENTICATE, and the last argument to APPEND which is passed as
113 an IMAP4 literal. If necessary (the string contains any
114 non-printing characters or white-space and isn't enclosed with
115 either parentheses or double quotes) each string is quoted.
116 However, the 'password' argument to the LOGIN command is always
117 quoted. If you want to avoid having an argument string quoted
118 (eg: the 'flags' argument to STORE) then enclose the string in
119 parentheses (eg: "(\Deleted)").
121 Each command returns a tuple: (type, [data, ...]) where 'type'
122 is usually 'OK' or 'NO', and 'data' is either the text from the
123 tagged response, or untagged results from command. Each 'data'
124 is either a string, or a tuple. If a tuple, then the first part
125 is the header of the response, and the second part contains
126 the data (ie: 'literal' value).
128 Errors raise the exception class <instance>.error("<reason>").
129 IMAP4 server errors raise <instance>.abort("<reason>"),
130 which is a sub-class of 'error'. Mailbox status changes
131 from READ-WRITE to READ-ONLY raise the exception class
132 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
134 "error" exceptions imply a program error.
135 "abort" exceptions imply the connection should be reset, and
136 the command re-tried.
137 "readonly" exceptions imply the command should be re-tried.
139 Note: to use this module, you must read the RFCs pertaining to the
140 IMAP4 protocol, as the semantics of the arguments to each IMAP4
141 command are left to the invoker, not to mention the results. Also,
142 most IMAP servers implement a sub-set of the commands available here.
145 class error(Exception): pass # Logical errors - debug required
146 class abort(error
): pass # Service errors - close and retry
147 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
149 mustquote
= re
.compile(r
"[^\w!#$%&'*+,.:;<=>?^`|~-]")
151 def __init__(self
, host
= '', port
= IMAP4_PORT
):
153 self
.state
= 'LOGOUT'
154 self
.literal
= None # A literal argument to a command
155 self
.tagged_commands
= {} # Tagged commands awaiting response
156 self
.untagged_responses
= {} # {typ: [data, ...], ...}
157 self
.continuation_response
= '' # Last continuation response
158 self
.is_readonly
= False # READ-ONLY desired state
161 # Open socket to server.
163 self
.open(host
, port
)
165 # Create unique tag for this session,
166 # and compile tagged response matcher.
168 self
.tagpre
= Int2AP(random
.randint(4096, 65535))
169 self
.tagre
= re
.compile(r
'(?P<tag>'
171 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
173 # Get server welcome message,
174 # request and store CAPABILITY response.
177 self
._cmd
_log
_len
= 10
178 self
._cmd
_log
_idx
= 0
179 self
._cmd
_log
= {} # Last `_cmd_log_len' interactions
181 self
._mesg
('imaplib version %s' % __version__
)
182 self
._mesg
('new IMAP4 connection, tag=%s' % self
.tagpre
)
184 self
.welcome
= self
._get
_response
()
185 if 'PREAUTH' in self
.untagged_responses
:
187 elif 'OK' in self
.untagged_responses
:
188 self
.state
= 'NONAUTH'
190 raise self
.error(self
.welcome
)
192 typ
, dat
= self
.capability()
194 raise self
.error('no CAPABILITY response from server')
195 self
.capabilities
= tuple(dat
[-1].upper().split())
199 self
._mesg
('CAPABILITIES: %r' % (self
.capabilities
,))
201 for version
in AllowedVersions
:
202 if not version
in self
.capabilities
:
204 self
.PROTOCOL_VERSION
= version
207 raise self
.error('server not IMAP4 compliant')
210 def __getattr__(self
, attr
):
211 # Allow UPPERCASE variants of IMAP4 command methods.
213 return getattr(self
, attr
.lower())
214 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
218 # Overridable methods
221 def open(self
, host
= '', port
= IMAP4_PORT
):
222 """Setup connection to remote server on "host:port"
223 (default: localhost:standard IMAP4 port).
224 This connection will be used by the routines:
225 read, readline, send, shutdown.
229 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
230 self
.sock
.connect((host
, port
))
231 self
.file = self
.sock
.makefile('rb')
234 def read(self
, size
):
235 """Read 'size' bytes from remote."""
236 return self
.file.read(size
)
240 """Read line from remote."""
241 return self
.file.readline()
244 def send(self
, data
):
245 """Send data to remote."""
246 self
.sock
.sendall(data
)
250 """Close I/O established in "open"."""
256 """Return socket instance used to connect to IMAP4 server.
258 socket = <instance>.socket()
268 """Return most recent 'RECENT' responses if any exist,
269 else prompt server for an update using the 'NOOP' command.
271 (typ, [data]) = <instance>.recent()
273 'data' is None if no new messages,
274 else list of RECENT responses, most recent last.
277 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
280 typ
, dat
= self
.noop() # Prod server for response
281 return self
._untagged
_response
(typ
, dat
, name
)
284 def response(self
, code
):
285 """Return data for response 'code' if received, or None.
287 Old value for response 'code' is cleared.
289 (code, [data]) = <instance>.response(code)
291 return self
._untagged
_response
(code
, [None], code
.upper())
298 def append(self
, mailbox
, flags
, date_time
, message
):
299 """Append message to named mailbox.
301 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
303 All args except `message' can be None.
309 if (flags
[0],flags
[-1]) != ('(',')'):
310 flags
= '(%s)' % flags
314 date_time
= Time2Internaldate(date_time
)
317 self
.literal
= MapCRLF
.sub(CRLF
, message
)
318 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
321 def authenticate(self
, mechanism
, authobject
):
322 """Authenticate command - requires response processing.
324 'mechanism' specifies which authentication mechanism is to
325 be used - it must appear in <instance>.capabilities in the
326 form AUTH=<mechanism>.
328 'authobject' must be a callable object:
330 data = authobject(response)
332 It will be called to process server continuation responses.
333 It should return data that will be encoded and sent to server.
334 It should return None if the client abort response '*' should
337 mech
= mechanism
.upper()
338 # XXX: shouldn't this code be removed, not commented out?
339 #cap = 'AUTH=%s' % mech
340 #if not cap in self.capabilities: # Let the server decide!
341 # raise self.error("Server doesn't allow %s authentication." % mech)
342 self
.literal
= _Authenticator(authobject
).process
343 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
345 raise self
.error(dat
[-1])
350 def capability(self
):
351 """(typ, [data]) = <instance>.capability()
352 Fetch capabilities list from server."""
355 typ
, dat
= self
._simple
_command
(name
)
356 return self
._untagged
_response
(typ
, dat
, name
)
360 """Checkpoint mailbox on server.
362 (typ, [data]) = <instance>.check()
364 return self
._simple
_command
('CHECK')
368 """Close currently selected mailbox.
370 Deleted messages are removed from writable mailbox.
371 This is the recommended command before 'LOGOUT'.
373 (typ, [data]) = <instance>.close()
376 typ
, dat
= self
._simple
_command
('CLOSE')
382 def copy(self
, message_set
, new_mailbox
):
383 """Copy 'message_set' messages onto end of 'new_mailbox'.
385 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
387 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
390 def create(self
, mailbox
):
391 """Create new mailbox.
393 (typ, [data]) = <instance>.create(mailbox)
395 return self
._simple
_command
('CREATE', mailbox
)
398 def delete(self
, mailbox
):
399 """Delete old mailbox.
401 (typ, [data]) = <instance>.delete(mailbox)
403 return self
._simple
_command
('DELETE', mailbox
)
405 def deleteacl(self
, mailbox
, who
):
406 """Delete the ACLs (remove any rights) set for who on mailbox.
408 (typ, [data]) = <instance>.deleteacl(mailbox, who)
410 return self
._simple
_command
('DELETEACL', mailbox
, who
)
413 """Permanently remove deleted items from selected mailbox.
415 Generates 'EXPUNGE' response for each deleted message.
417 (typ, [data]) = <instance>.expunge()
419 'data' is list of 'EXPUNGE'd message numbers in order received.
422 typ
, dat
= self
._simple
_command
(name
)
423 return self
._untagged
_response
(typ
, dat
, name
)
426 def fetch(self
, message_set
, message_parts
):
427 """Fetch (parts of) messages.
429 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
431 'message_parts' should be a string of selected parts
432 enclosed in parentheses, eg: "(UID BODY[TEXT])".
434 'data' are tuples of message part envelope and data.
437 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
438 return self
._untagged
_response
(typ
, dat
, name
)
441 def getacl(self
, mailbox
):
442 """Get the ACLs for a mailbox.
444 (typ, [data]) = <instance>.getacl(mailbox)
446 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
447 return self
._untagged
_response
(typ
, dat
, 'ACL')
450 def getannotation(self
, mailbox
, entry
, attribute
):
451 """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
452 Retrieve ANNOTATIONs."""
454 typ
, dat
= self
._simple
_command
('GETANNOTATION', mailbox
, entry
, attribute
)
455 return self
._untagged
_response
(typ
, dat
, 'ANNOTATION')
458 def getquota(self
, root
):
459 """Get the quota root's resource usage and limits.
461 Part of the IMAP4 QUOTA extension defined in rfc2087.
463 (typ, [data]) = <instance>.getquota(root)
465 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
466 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
469 def getquotaroot(self
, mailbox
):
470 """Get the list of quota roots for the named mailbox.
472 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
474 typ
, dat
= self
._simple
_command
('GETQUOTAROOT', mailbox
)
475 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
476 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
477 return typ
, [quotaroot
, quota
]
480 def list(self
, directory
='""', pattern
='*'):
481 """List mailbox names in directory matching pattern.
483 (typ, [data]) = <instance>.list(directory='""', pattern='*')
485 'data' is list of LIST responses.
488 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
489 return self
._untagged
_response
(typ
, dat
, name
)
492 def login(self
, user
, password
):
493 """Identify client using plaintext password.
495 (typ, [data]) = <instance>.login(user, password)
497 NB: 'password' will be quoted.
499 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
501 raise self
.error(dat
[-1])
506 def login_cram_md5(self
, user
, password
):
507 """ Force use of CRAM-MD5 authentication.
509 (typ, [data]) = <instance>.login_cram_md5(user, password)
511 self
.user
, self
.password
= user
, password
512 return self
.authenticate('CRAM-MD5', self
._CRAM
_MD
5_AUTH
)
515 def _CRAM_MD5_AUTH(self
, challenge
):
516 """ Authobject to use with CRAM-MD5 authentication. """
518 return self
.user
+ " " + hmac
.HMAC(self
.password
, challenge
).hexdigest()
522 """Shutdown connection to server.
524 (typ, [data]) = <instance>.logout()
526 Returns server 'BYE' response.
528 self
.state
= 'LOGOUT'
529 try: typ
, dat
= self
._simple
_command
('LOGOUT')
530 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
532 if 'BYE' in self
.untagged_responses
:
533 return 'BYE', self
.untagged_responses
['BYE']
537 def lsub(self
, directory
='""', pattern
='*'):
538 """List 'subscribed' mailbox names in directory matching pattern.
540 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
542 'data' are tuples of message part envelope and data.
545 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
546 return self
._untagged
_response
(typ
, dat
, name
)
548 def myrights(self
, mailbox
):
549 """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
551 (typ, [data]) = <instance>.myrights(mailbox)
553 typ
,dat
= self
._simple
_command
('MYRIGHTS', mailbox
)
554 return self
._untagged
_response
(typ
, dat
, 'MYRIGHTS')
557 """ Returns IMAP namespaces ala rfc2342
559 (typ, [data, ...]) = <instance>.namespace()
562 typ
, dat
= self
._simple
_command
(name
)
563 return self
._untagged
_response
(typ
, dat
, name
)
567 """Send NOOP command.
569 (typ, [data]) = <instance>.noop()
573 self
._dump
_ur
(self
.untagged_responses
)
574 return self
._simple
_command
('NOOP')
577 def partial(self
, message_num
, message_part
, start
, length
):
578 """Fetch truncated part of a message.
580 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
582 'data' is tuple of message part envelope and data.
585 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
586 return self
._untagged
_response
(typ
, dat
, 'FETCH')
589 def proxyauth(self
, user
):
590 """Assume authentication as "user".
592 Allows an authorised administrator to proxy into any user's
595 (typ, [data]) = <instance>.proxyauth(user)
599 return self
._simple
_command
('PROXYAUTH', user
)
602 def rename(self
, oldmailbox
, newmailbox
):
603 """Rename old mailbox name to new.
605 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
607 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
610 def search(self
, charset
, *criteria
):
611 """Search mailbox for matching messages.
613 (typ, [data]) = <instance>.search(charset, criterion, ...)
615 'data' is space separated list of matching message numbers.
619 typ
, dat
= self
._simple
_command
(name
, 'CHARSET', charset
, *criteria
)
621 typ
, dat
= self
._simple
_command
(name
, *criteria
)
622 return self
._untagged
_response
(typ
, dat
, name
)
625 def select(self
, mailbox
='INBOX', readonly
=False):
628 Flush all untagged responses.
630 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
632 'data' is count of messages in mailbox ('EXISTS' response).
634 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
635 other responses should be obtained via <instance>.response('FLAGS') etc.
637 self
.untagged_responses
= {} # Flush old responses.
638 self
.is_readonly
= readonly
643 typ
, dat
= self
._simple
_command
(name
, mailbox
)
645 self
.state
= 'AUTH' # Might have been 'SELECTED'
647 self
.state
= 'SELECTED'
648 if 'READ-ONLY' in self
.untagged_responses \
652 self
._dump
_ur
(self
.untagged_responses
)
653 raise self
.readonly('%s is not writable' % mailbox
)
654 return typ
, self
.untagged_responses
.get('EXISTS', [None])
657 def setacl(self
, mailbox
, who
, what
):
658 """Set a mailbox acl.
660 (typ, [data]) = <instance>.setacl(mailbox, who, what)
662 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
665 def setannotation(self
, *args
):
666 """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
669 typ
, dat
= self
._simple
_command
('SETANNOTATION', *args
)
670 return self
._untagged
_response
(typ
, dat
, 'ANNOTATION')
673 def setquota(self
, root
, limits
):
674 """Set the quota root's resource limits.
676 (typ, [data]) = <instance>.setquota(root, limits)
678 typ
, dat
= self
._simple
_command
('SETQUOTA', root
, limits
)
679 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
682 def sort(self
, sort_criteria
, charset
, *search_criteria
):
683 """IMAP4rev1 extension SORT command.
685 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
688 #if not name in self.capabilities: # Let the server decide!
689 # raise self.error('unimplemented extension command: %s' % name)
690 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
691 sort_criteria
= '(%s)' % sort_criteria
692 typ
, dat
= self
._simple
_command
(name
, sort_criteria
, charset
, *search_criteria
)
693 return self
._untagged
_response
(typ
, dat
, name
)
696 def status(self
, mailbox
, names
):
697 """Request named status conditions for mailbox.
699 (typ, [data]) = <instance>.status(mailbox, names)
702 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
703 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
704 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
705 return self
._untagged
_response
(typ
, dat
, name
)
708 def store(self
, message_set
, command
, flags
):
709 """Alters flag dispositions for messages in mailbox.
711 (typ, [data]) = <instance>.store(message_set, command, flags)
713 if (flags
[0],flags
[-1]) != ('(',')'):
714 flags
= '(%s)' % flags
# Avoid quoting the flags
715 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
716 return self
._untagged
_response
(typ
, dat
, 'FETCH')
719 def subscribe(self
, mailbox
):
720 """Subscribe to new mailbox.
722 (typ, [data]) = <instance>.subscribe(mailbox)
724 return self
._simple
_command
('SUBSCRIBE', mailbox
)
727 def thread(self
, threading_algorithm
, charset
, *search_criteria
):
728 """IMAPrev1 extension THREAD command.
730 (type, [data]) = <instance>.thread(threading_alogrithm, charset, search_criteria, ...)
733 typ
, dat
= self
._simple
_command
(name
, threading_algorithm
, charset
, *search_criteria
)
734 return self
._untagged
_response
(typ
, dat
, name
)
737 def uid(self
, command
, *args
):
738 """Execute "command arg ..." with messages identified by UID,
739 rather than message number.
741 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
743 Returns response appropriate to 'command'.
745 command
= command
.upper()
746 if not command
in Commands
:
747 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
748 if self
.state
not in Commands
[command
]:
749 raise self
.error("command %s illegal in state %s, "
750 "only allowed in states %s" %
751 (command
, self
.state
,
752 ', '.join(Commands
[command
])))
754 typ
, dat
= self
._simple
_command
(name
, command
, *args
)
755 if command
in ('SEARCH', 'SORT'):
759 return self
._untagged
_response
(typ
, dat
, name
)
762 def unsubscribe(self
, mailbox
):
763 """Unsubscribe from old mailbox.
765 (typ, [data]) = <instance>.unsubscribe(mailbox)
767 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
770 def xatom(self
, name
, *args
):
771 """Allow simple extension commands
772 notified by server in CAPABILITY response.
774 Assumes command is legal in current state.
776 (typ, [data]) = <instance>.xatom(name, arg, ...)
778 Returns response appropriate to extension command `name'.
781 #if not name in self.capabilities: # Let the server decide!
782 # raise self.error('unknown extension command: %s' % name)
783 if not name
in Commands
:
784 Commands
[name
] = (self
.state
,)
785 return self
._simple
_command
(name
, *args
)
792 def _append_untagged(self
, typ
, dat
):
794 if dat
is None: dat
= ''
795 ur
= self
.untagged_responses
798 self
._mesg
('untagged_responses[%s] %s += ["%s"]' %
799 (typ
, len(ur
.get(typ
,'')), dat
))
806 def _check_bye(self
):
807 bye
= self
.untagged_responses
.get('BYE')
809 raise self
.abort(bye
[-1])
812 def _command(self
, name
, *args
):
814 if self
.state
not in Commands
[name
]:
816 raise self
.error("command %s illegal in state %s, "
817 "only allowed in states %s" %
819 ', '.join(Commands
[name
])))
821 for typ
in ('OK', 'NO', 'BAD'):
822 if typ
in self
.untagged_responses
:
823 del self
.untagged_responses
[typ
]
825 if 'READ-ONLY' in self
.untagged_responses \
826 and not self
.is_readonly
:
827 raise self
.readonly('mailbox status changed to READ-ONLY')
829 tag
= self
._new
_tag
()
830 data
= '%s %s' % (tag
, name
)
832 if arg
is None: continue
833 data
= '%s %s' % (data
, self
._checkquote
(arg
))
835 literal
= self
.literal
836 if literal
is not None:
838 if type(literal
) is type(self
._command
):
842 data
= '%s {%s}' % (data
, len(literal
))
846 self
._mesg
('> %s' % data
)
848 self
._log
('> %s' % data
)
851 self
.send('%s%s' % (data
, CRLF
))
852 except (socket
.error
, OSError), val
:
853 raise self
.abort('socket error: %s' % val
)
859 # Wait for continuation response
861 while self
._get
_response
():
862 if self
.tagged_commands
[tag
]: # BAD/NO?
868 literal
= literator(self
.continuation_response
)
872 self
._mesg
('write literal size %s' % len(literal
))
877 except (socket
.error
, OSError), val
:
878 raise self
.abort('socket error: %s' % val
)
886 def _command_complete(self
, name
, tag
):
889 typ
, data
= self
._get
_tagged
_response
(tag
)
890 except self
.abort
, val
:
891 raise self
.abort('command: %s => %s' % (name
, val
))
892 except self
.error
, val
:
893 raise self
.error('command: %s => %s' % (name
, val
))
896 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
900 def _get_response(self
):
902 # Read response and store.
904 # Returns None for continuation responses,
905 # otherwise first response line received.
907 resp
= self
._get
_line
()
909 # Command completion response?
911 if self
._match
(self
.tagre
, resp
):
912 tag
= self
.mo
.group('tag')
913 if not tag
in self
.tagged_commands
:
914 raise self
.abort('unexpected tagged response: %s' % resp
)
916 typ
= self
.mo
.group('type')
917 dat
= self
.mo
.group('data')
918 self
.tagged_commands
[tag
] = (typ
, [dat
])
922 # '*' (untagged) responses?
924 if not self
._match
(Untagged_response
, resp
):
925 if self
._match
(Untagged_status
, resp
):
926 dat2
= self
.mo
.group('data2')
929 # Only other possibility is '+' (continuation) response...
931 if self
._match
(Continuation
, resp
):
932 self
.continuation_response
= self
.mo
.group('data')
933 return None # NB: indicates continuation
935 raise self
.abort("unexpected response: '%s'" % resp
)
937 typ
= self
.mo
.group('type')
938 dat
= self
.mo
.group('data')
939 if dat
is None: dat
= '' # Null untagged response
940 if dat2
: dat
= dat
+ ' ' + dat2
942 # Is there a literal to come?
944 while self
._match
(Literal
, dat
):
946 # Read literal direct from connection.
948 size
= int(self
.mo
.group('size'))
951 self
._mesg
('read literal size %s' % size
)
952 data
= self
.read(size
)
954 # Store response with literal as tuple
956 self
._append
_untagged
(typ
, (dat
, data
))
958 # Read trailer - possibly containing another literal
960 dat
= self
._get
_line
()
962 self
._append
_untagged
(typ
, dat
)
964 # Bracketed response information?
966 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
967 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
970 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
971 self
._mesg
('%s response: %s' % (typ
, dat
))
976 def _get_tagged_response(self
, tag
):
979 result
= self
.tagged_commands
[tag
]
980 if result
is not None:
981 del self
.tagged_commands
[tag
]
984 # Some have reported "unexpected response" exceptions.
985 # Note that ignoring them here causes loops.
986 # Instead, send me details of the unexpected response and
987 # I'll update the code in `_get_response()'.
991 except self
.abort
, val
:
1000 line
= self
.readline()
1002 raise self
.abort('socket error: EOF')
1004 # Protocol mandates all lines terminated by CRLF
1009 self
._mesg
('< %s' % line
)
1011 self
._log
('< %s' % line
)
1015 def _match(self
, cre
, s
):
1017 # Run compiled regular expression match method on 's'.
1018 # Save result, return success.
1020 self
.mo
= cre
.match(s
)
1022 if self
.mo
is not None and self
.debug
>= 5:
1023 self
._mesg
("\tmatched r'%s' => %r" % (cre
.pattern
, self
.mo
.groups()))
1024 return self
.mo
is not None
1029 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
1030 self
.tagnum
= self
.tagnum
+ 1
1031 self
.tagged_commands
[tag
] = None
1035 def _checkquote(self
, arg
):
1037 # Must quote command args if non-alphanumeric chars present,
1038 # and not already quoted.
1040 if type(arg
) is not type(''):
1042 if len(arg
) >= 2 and (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
1044 if arg
and self
.mustquote
.search(arg
) is None:
1046 return self
._quote
(arg
)
1049 def _quote(self
, arg
):
1051 arg
= arg
.replace('\\', '\\\\')
1052 arg
= arg
.replace('"', '\\"')
1057 def _simple_command(self
, name
, *args
):
1059 return self
._command
_complete
(name
, self
._command
(name
, *args
))
1062 def _untagged_response(self
, typ
, dat
, name
):
1066 if not name
in self
.untagged_responses
:
1068 data
= self
.untagged_responses
.pop(name
)
1071 self
._mesg
('untagged_responses[%s] => %s' % (name
, data
))
1077 def _mesg(self
, s
, secs
=None):
1080 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1081 sys
.stderr
.write(' %s.%02d %s\n' % (tm
, (secs
*100)%100, s
))
1084 def _dump_ur(self
, dict):
1085 # Dump untagged responses (in `dict').
1089 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1090 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1092 def _log(self
, line
):
1093 # Keep log of last `_cmd_log_len' interactions for debugging.
1094 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, time
.time())
1095 self
._cmd
_log
_idx
+= 1
1096 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1097 self
._cmd
_log
_idx
= 0
1099 def print_log(self
):
1100 self
._mesg
('last %d IMAP4 interactions:' % len(self
._cmd
_log
))
1101 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1104 self
._mesg
(*self
._cmd
_log
[i
])
1108 if i
>= self
._cmd
_log
_len
:
1119 class IMAP4_SSL(IMAP4
):
1121 """IMAP4 client class over SSL connection
1123 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1125 host - host's name (default: localhost);
1126 port - port number (default: standard IMAP4 SSL port).
1127 keyfile - PEM formatted file that contains your private key (default: None);
1128 certfile - PEM formatted certificate chain file (default: None);
1130 for more documentation see the docstring of the parent class IMAP4.
1134 def __init__(self
, host
= '', port
= IMAP4_SSL_PORT
, keyfile
= None, certfile
= None):
1135 self
.keyfile
= keyfile
1136 self
.certfile
= certfile
1137 IMAP4
.__init__(self
, host
, port
)
1140 def open(self
, host
= '', port
= IMAP4_SSL_PORT
):
1141 """Setup connection to remote server on "host:port".
1142 (default: localhost:standard IMAP4 SSL port).
1143 This connection will be used by the routines:
1144 read, readline, send, shutdown.
1148 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
1149 self
.sock
.connect((host
, port
))
1150 self
.sslobj
= ssl
.wrap_socket(self
.sock
, self
.keyfile
, self
.certfile
)
1153 def read(self
, size
):
1154 """Read 'size' bytes from remote."""
1155 # sslobj.read() sometimes returns < size bytes
1159 data
= self
.sslobj
.read(size
-read
)
1163 return ''.join(chunks
)
1167 """Read line from remote."""
1170 char
= self
.sslobj
.read(1)
1172 if char
== "\n": return ''.join(line
)
1175 def send(self
, data
):
1176 """Send data to remote."""
1179 sent
= self
.sslobj
.write(data
)
1183 bytes
= bytes
- sent
1187 """Close I/O established in "open"."""
1192 """Return socket instance used to connect to IMAP4 server.
1194 socket = <instance>.socket()
1200 """Return SSLObject instance used to communicate with the IMAP4 server.
1202 ssl = ssl.wrap_socket(<instance>.socket)
1206 __all__
.append("IMAP4_SSL")
1209 class IMAP4_stream(IMAP4
):
1211 """IMAP4 client class over a stream
1213 Instantiate with: IMAP4_stream(command)
1215 where "command" is a string that can be passed to os.popen2()
1217 for more documentation see the docstring of the parent class IMAP4.
1221 def __init__(self
, command
):
1222 self
.command
= command
1223 IMAP4
.__init__(self
)
1226 def open(self
, host
= None, port
= None):
1227 """Setup a stream connection.
1228 This connection will be used by the routines:
1229 read, readline, send, shutdown.
1231 self
.host
= None # For compatibility with parent class
1235 self
.writefile
, self
.readfile
= os
.popen2(self
.command
)
1238 def read(self
, size
):
1239 """Read 'size' bytes from remote."""
1240 return self
.readfile
.read(size
)
1244 """Read line from remote."""
1245 return self
.readfile
.readline()
1248 def send(self
, data
):
1249 """Send data to remote."""
1250 self
.writefile
.write(data
)
1251 self
.writefile
.flush()
1255 """Close I/O established in "open"."""
1256 self
.readfile
.close()
1257 self
.writefile
.close()
1261 class _Authenticator
:
1263 """Private class to provide en/decoding
1264 for base64-based authentication conversation.
1267 def __init__(self
, mechinst
):
1268 self
.mech
= mechinst
# Callable object to provide/process data
1270 def process(self
, data
):
1271 ret
= self
.mech(self
.decode(data
))
1273 return '*' # Abort conversation
1274 return self
.encode(ret
)
1276 def encode(self
, inp
):
1278 # Invoke binascii.b2a_base64 iteratively with
1279 # short even length buffers, strip the trailing
1280 # line feed from the result and append. "Even"
1281 # means a number that factors to both 6 and 8,
1282 # so when it gets to the end of the 8-bit input
1283 # there's no partial 6-bit output.
1293 e
= binascii
.b2a_base64(t
)
1298 def decode(self
, inp
):
1301 return binascii
.a2b_base64(inp
)
1305 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1306 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1308 def Internaldate2tuple(resp
):
1309 """Convert IMAP4 INTERNALDATE to UT.
1311 Returns Python time module tuple.
1314 mo
= InternalDate
.match(resp
)
1318 mon
= Mon2num
[mo
.group('mon')]
1319 zonen
= mo
.group('zonen')
1321 day
= int(mo
.group('day'))
1322 year
= int(mo
.group('year'))
1323 hour
= int(mo
.group('hour'))
1324 min = int(mo
.group('min'))
1325 sec
= int(mo
.group('sec'))
1326 zoneh
= int(mo
.group('zoneh'))
1327 zonem
= int(mo
.group('zonem'))
1329 # INTERNALDATE timezone must be subtracted to get UT
1331 zone
= (zoneh
*60 + zonem
)*60
1335 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1337 utc
= time
.mktime(tt
)
1339 # Following is necessary because the time module has no 'mkgmtime'.
1340 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1342 lt
= time
.localtime(utc
)
1343 if time
.daylight
and lt
[-1]:
1344 zone
= zone
+ time
.altzone
1346 zone
= zone
+ time
.timezone
1348 return time
.localtime(utc
- zone
)
1354 """Convert integer to A-P string representation."""
1356 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1359 num
, mod
= divmod(num
, 16)
1365 def ParseFlags(resp
):
1367 """Convert IMAP4 flags response to python tuple."""
1369 mo
= Flags
.match(resp
)
1373 return tuple(mo
.group('flags').split())
1376 def Time2Internaldate(date_time
):
1378 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1380 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1383 if isinstance(date_time
, (int, float)):
1384 tt
= time
.localtime(date_time
)
1385 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1387 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1388 return date_time
# Assume in correct format
1390 raise ValueError("date_time not of a known type")
1392 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1395 if time
.daylight
and tt
[-1]:
1396 zone
= -time
.altzone
1398 zone
= -time
.timezone
1399 return '"' + dt
+ " %+03d%02d" % divmod(zone
//60, 60) + '"'
1403 if __name__
== '__main__':
1405 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1406 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1407 # to test the IMAP4_stream class
1409 import getopt
, getpass
1412 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:s:')
1413 except getopt
.error
, val
:
1414 optlist
, args
= (), ()
1416 stream_command
= None
1417 for opt
,val
in optlist
:
1421 stream_command
= val
1422 if not args
: args
= (stream_command
,)
1424 if not args
: args
= ('',)
1428 USER
= getpass
.getuser()
1429 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1431 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':'\n'}
1433 ('login', (USER
, PASSWD
)),
1434 ('create', ('/tmp/xxx 1',)),
1435 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1436 ('CREATE', ('/tmp/yyz 2',)),
1437 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1438 ('list', ('/tmp', 'yy*')),
1439 ('select', ('/tmp/yyz 2',)),
1440 ('search', (None, 'SUBJECT', 'test')),
1441 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1442 ('store', ('1', 'FLAGS', '(\Deleted)')),
1451 ('response',('UIDVALIDITY',)),
1452 ('uid', ('SEARCH', 'ALL')),
1453 ('response', ('EXISTS',)),
1454 ('append', (None, None, None, test_mesg
)),
1460 M
._mesg
('%s %s' % (cmd
, args
))
1461 typ
, dat
= getattr(M
, cmd
)(*args
)
1462 M
._mesg
('%s => %s %s' % (cmd
, typ
, dat
))
1463 if typ
== 'NO': raise dat
[0]
1468 M
= IMAP4_stream(stream_command
)
1471 if M
.state
== 'AUTH':
1472 test_seq1
= test_seq1
[1:] # Login not needed
1473 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1474 M
._mesg
('CAPABILITIES = %r' % (M
.capabilities
,))
1476 for cmd
,args
in test_seq1
:
1479 for ml
in run('list', ('/tmp/', 'yy%')):
1480 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1481 if mo
: path
= mo
.group(1)
1482 else: path
= ml
.split()[-1]
1483 run('delete', (path
,))
1485 for cmd
,args
in test_seq2
:
1486 dat
= run(cmd
, args
)
1488 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1491 uid
= dat
[-1].split()
1492 if not uid
: continue
1493 run('uid', ('FETCH', '%s' % uid
[-1],
1494 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1496 print '\nAll tests OK.'
1499 print '\nTests failed.'
1503 If you would like to see debugging output,