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
, random
, re
, socket
, subprocess
, 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
.create_connection((host
, port
))
230 self
.file = self
.sock
.makefile('rb')
233 def read(self
, size
):
234 """Read 'size' bytes from remote."""
235 return self
.file.read(size
)
239 """Read line from remote."""
240 return self
.file.readline()
243 def send(self
, data
):
244 """Send data to remote."""
245 self
.sock
.sendall(data
)
249 """Close I/O established in "open"."""
255 """Return socket instance used to connect to IMAP4 server.
257 socket = <instance>.socket()
267 """Return most recent 'RECENT' responses if any exist,
268 else prompt server for an update using the 'NOOP' command.
270 (typ, [data]) = <instance>.recent()
272 'data' is None if no new messages,
273 else list of RECENT responses, most recent last.
276 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
279 typ
, dat
= self
.noop() # Prod server for response
280 return self
._untagged
_response
(typ
, dat
, name
)
283 def response(self
, code
):
284 """Return data for response 'code' if received, or None.
286 Old value for response 'code' is cleared.
288 (code, [data]) = <instance>.response(code)
290 return self
._untagged
_response
(code
, [None], code
.upper())
297 def append(self
, mailbox
, flags
, date_time
, message
):
298 """Append message to named mailbox.
300 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
302 All args except `message' can be None.
308 if (flags
[0],flags
[-1]) != ('(',')'):
309 flags
= '(%s)' % flags
313 date_time
= Time2Internaldate(date_time
)
316 self
.literal
= MapCRLF
.sub(CRLF
, message
)
317 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
320 def authenticate(self
, mechanism
, authobject
):
321 """Authenticate command - requires response processing.
323 'mechanism' specifies which authentication mechanism is to
324 be used - it must appear in <instance>.capabilities in the
325 form AUTH=<mechanism>.
327 'authobject' must be a callable object:
329 data = authobject(response)
331 It will be called to process server continuation responses.
332 It should return data that will be encoded and sent to server.
333 It should return None if the client abort response '*' should
336 mech
= mechanism
.upper()
337 # XXX: shouldn't this code be removed, not commented out?
338 #cap = 'AUTH=%s' % mech
339 #if not cap in self.capabilities: # Let the server decide!
340 # raise self.error("Server doesn't allow %s authentication." % mech)
341 self
.literal
= _Authenticator(authobject
).process
342 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
344 raise self
.error(dat
[-1])
349 def capability(self
):
350 """(typ, [data]) = <instance>.capability()
351 Fetch capabilities list from server."""
354 typ
, dat
= self
._simple
_command
(name
)
355 return self
._untagged
_response
(typ
, dat
, name
)
359 """Checkpoint mailbox on server.
361 (typ, [data]) = <instance>.check()
363 return self
._simple
_command
('CHECK')
367 """Close currently selected mailbox.
369 Deleted messages are removed from writable mailbox.
370 This is the recommended command before 'LOGOUT'.
372 (typ, [data]) = <instance>.close()
375 typ
, dat
= self
._simple
_command
('CLOSE')
381 def copy(self
, message_set
, new_mailbox
):
382 """Copy 'message_set' messages onto end of 'new_mailbox'.
384 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
386 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
389 def create(self
, mailbox
):
390 """Create new mailbox.
392 (typ, [data]) = <instance>.create(mailbox)
394 return self
._simple
_command
('CREATE', mailbox
)
397 def delete(self
, mailbox
):
398 """Delete old mailbox.
400 (typ, [data]) = <instance>.delete(mailbox)
402 return self
._simple
_command
('DELETE', mailbox
)
404 def deleteacl(self
, mailbox
, who
):
405 """Delete the ACLs (remove any rights) set for who on mailbox.
407 (typ, [data]) = <instance>.deleteacl(mailbox, who)
409 return self
._simple
_command
('DELETEACL', mailbox
, who
)
412 """Permanently remove deleted items from selected mailbox.
414 Generates 'EXPUNGE' response for each deleted message.
416 (typ, [data]) = <instance>.expunge()
418 'data' is list of 'EXPUNGE'd message numbers in order received.
421 typ
, dat
= self
._simple
_command
(name
)
422 return self
._untagged
_response
(typ
, dat
, name
)
425 def fetch(self
, message_set
, message_parts
):
426 """Fetch (parts of) messages.
428 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
430 'message_parts' should be a string of selected parts
431 enclosed in parentheses, eg: "(UID BODY[TEXT])".
433 'data' are tuples of message part envelope and data.
436 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
437 return self
._untagged
_response
(typ
, dat
, name
)
440 def getacl(self
, mailbox
):
441 """Get the ACLs for a mailbox.
443 (typ, [data]) = <instance>.getacl(mailbox)
445 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
446 return self
._untagged
_response
(typ
, dat
, 'ACL')
449 def getannotation(self
, mailbox
, entry
, attribute
):
450 """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
451 Retrieve ANNOTATIONs."""
453 typ
, dat
= self
._simple
_command
('GETANNOTATION', mailbox
, entry
, attribute
)
454 return self
._untagged
_response
(typ
, dat
, 'ANNOTATION')
457 def getquota(self
, root
):
458 """Get the quota root's resource usage and limits.
460 Part of the IMAP4 QUOTA extension defined in rfc2087.
462 (typ, [data]) = <instance>.getquota(root)
464 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
465 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
468 def getquotaroot(self
, mailbox
):
469 """Get the list of quota roots for the named mailbox.
471 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
473 typ
, dat
= self
._simple
_command
('GETQUOTAROOT', mailbox
)
474 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
475 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
476 return typ
, [quotaroot
, quota
]
479 def list(self
, directory
='""', pattern
='*'):
480 """List mailbox names in directory matching pattern.
482 (typ, [data]) = <instance>.list(directory='""', pattern='*')
484 'data' is list of LIST responses.
487 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
488 return self
._untagged
_response
(typ
, dat
, name
)
491 def login(self
, user
, password
):
492 """Identify client using plaintext password.
494 (typ, [data]) = <instance>.login(user, password)
496 NB: 'password' will be quoted.
498 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
500 raise self
.error(dat
[-1])
505 def login_cram_md5(self
, user
, password
):
506 """ Force use of CRAM-MD5 authentication.
508 (typ, [data]) = <instance>.login_cram_md5(user, password)
510 self
.user
, self
.password
= user
, password
511 return self
.authenticate('CRAM-MD5', self
._CRAM
_MD
5_AUTH
)
514 def _CRAM_MD5_AUTH(self
, challenge
):
515 """ Authobject to use with CRAM-MD5 authentication. """
517 return self
.user
+ " " + hmac
.HMAC(self
.password
, challenge
).hexdigest()
521 """Shutdown connection to server.
523 (typ, [data]) = <instance>.logout()
525 Returns server 'BYE' response.
527 self
.state
= 'LOGOUT'
528 try: typ
, dat
= self
._simple
_command
('LOGOUT')
529 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
531 if 'BYE' in self
.untagged_responses
:
532 return 'BYE', self
.untagged_responses
['BYE']
536 def lsub(self
, directory
='""', pattern
='*'):
537 """List 'subscribed' mailbox names in directory matching pattern.
539 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
541 'data' are tuples of message part envelope and data.
544 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
545 return self
._untagged
_response
(typ
, dat
, name
)
547 def myrights(self
, mailbox
):
548 """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
550 (typ, [data]) = <instance>.myrights(mailbox)
552 typ
,dat
= self
._simple
_command
('MYRIGHTS', mailbox
)
553 return self
._untagged
_response
(typ
, dat
, 'MYRIGHTS')
556 """ Returns IMAP namespaces ala rfc2342
558 (typ, [data, ...]) = <instance>.namespace()
561 typ
, dat
= self
._simple
_command
(name
)
562 return self
._untagged
_response
(typ
, dat
, name
)
566 """Send NOOP command.
568 (typ, [data]) = <instance>.noop()
572 self
._dump
_ur
(self
.untagged_responses
)
573 return self
._simple
_command
('NOOP')
576 def partial(self
, message_num
, message_part
, start
, length
):
577 """Fetch truncated part of a message.
579 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
581 'data' is tuple of message part envelope and data.
584 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
585 return self
._untagged
_response
(typ
, dat
, 'FETCH')
588 def proxyauth(self
, user
):
589 """Assume authentication as "user".
591 Allows an authorised administrator to proxy into any user's
594 (typ, [data]) = <instance>.proxyauth(user)
598 return self
._simple
_command
('PROXYAUTH', user
)
601 def rename(self
, oldmailbox
, newmailbox
):
602 """Rename old mailbox name to new.
604 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
606 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
609 def search(self
, charset
, *criteria
):
610 """Search mailbox for matching messages.
612 (typ, [data]) = <instance>.search(charset, criterion, ...)
614 'data' is space separated list of matching message numbers.
618 typ
, dat
= self
._simple
_command
(name
, 'CHARSET', charset
, *criteria
)
620 typ
, dat
= self
._simple
_command
(name
, *criteria
)
621 return self
._untagged
_response
(typ
, dat
, name
)
624 def select(self
, mailbox
='INBOX', readonly
=False):
627 Flush all untagged responses.
629 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
631 'data' is count of messages in mailbox ('EXISTS' response).
633 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
634 other responses should be obtained via <instance>.response('FLAGS') etc.
636 self
.untagged_responses
= {} # Flush old responses.
637 self
.is_readonly
= readonly
642 typ
, dat
= self
._simple
_command
(name
, mailbox
)
644 self
.state
= 'AUTH' # Might have been 'SELECTED'
646 self
.state
= 'SELECTED'
647 if 'READ-ONLY' in self
.untagged_responses \
651 self
._dump
_ur
(self
.untagged_responses
)
652 raise self
.readonly('%s is not writable' % mailbox
)
653 return typ
, self
.untagged_responses
.get('EXISTS', [None])
656 def setacl(self
, mailbox
, who
, what
):
657 """Set a mailbox acl.
659 (typ, [data]) = <instance>.setacl(mailbox, who, what)
661 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
664 def setannotation(self
, *args
):
665 """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
668 typ
, dat
= self
._simple
_command
('SETANNOTATION', *args
)
669 return self
._untagged
_response
(typ
, dat
, 'ANNOTATION')
672 def setquota(self
, root
, limits
):
673 """Set the quota root's resource limits.
675 (typ, [data]) = <instance>.setquota(root, limits)
677 typ
, dat
= self
._simple
_command
('SETQUOTA', root
, limits
)
678 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
681 def sort(self
, sort_criteria
, charset
, *search_criteria
):
682 """IMAP4rev1 extension SORT command.
684 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
687 #if not name in self.capabilities: # Let the server decide!
688 # raise self.error('unimplemented extension command: %s' % name)
689 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
690 sort_criteria
= '(%s)' % sort_criteria
691 typ
, dat
= self
._simple
_command
(name
, sort_criteria
, charset
, *search_criteria
)
692 return self
._untagged
_response
(typ
, dat
, name
)
695 def status(self
, mailbox
, names
):
696 """Request named status conditions for mailbox.
698 (typ, [data]) = <instance>.status(mailbox, names)
701 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
702 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
703 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
704 return self
._untagged
_response
(typ
, dat
, name
)
707 def store(self
, message_set
, command
, flags
):
708 """Alters flag dispositions for messages in mailbox.
710 (typ, [data]) = <instance>.store(message_set, command, flags)
712 if (flags
[0],flags
[-1]) != ('(',')'):
713 flags
= '(%s)' % flags
# Avoid quoting the flags
714 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
715 return self
._untagged
_response
(typ
, dat
, 'FETCH')
718 def subscribe(self
, mailbox
):
719 """Subscribe to new mailbox.
721 (typ, [data]) = <instance>.subscribe(mailbox)
723 return self
._simple
_command
('SUBSCRIBE', mailbox
)
726 def thread(self
, threading_algorithm
, charset
, *search_criteria
):
727 """IMAPrev1 extension THREAD command.
729 (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
732 typ
, dat
= self
._simple
_command
(name
, threading_algorithm
, charset
, *search_criteria
)
733 return self
._untagged
_response
(typ
, dat
, name
)
736 def uid(self
, command
, *args
):
737 """Execute "command arg ..." with messages identified by UID,
738 rather than message number.
740 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
742 Returns response appropriate to 'command'.
744 command
= command
.upper()
745 if not command
in Commands
:
746 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
747 if self
.state
not in Commands
[command
]:
748 raise self
.error("command %s illegal in state %s, "
749 "only allowed in states %s" %
750 (command
, self
.state
,
751 ', '.join(Commands
[command
])))
753 typ
, dat
= self
._simple
_command
(name
, command
, *args
)
754 if command
in ('SEARCH', 'SORT'):
758 return self
._untagged
_response
(typ
, dat
, name
)
761 def unsubscribe(self
, mailbox
):
762 """Unsubscribe from old mailbox.
764 (typ, [data]) = <instance>.unsubscribe(mailbox)
766 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
769 def xatom(self
, name
, *args
):
770 """Allow simple extension commands
771 notified by server in CAPABILITY response.
773 Assumes command is legal in current state.
775 (typ, [data]) = <instance>.xatom(name, arg, ...)
777 Returns response appropriate to extension command `name'.
780 #if not name in self.capabilities: # Let the server decide!
781 # raise self.error('unknown extension command: %s' % name)
782 if not name
in Commands
:
783 Commands
[name
] = (self
.state
,)
784 return self
._simple
_command
(name
, *args
)
791 def _append_untagged(self
, typ
, dat
):
793 if dat
is None: dat
= ''
794 ur
= self
.untagged_responses
797 self
._mesg
('untagged_responses[%s] %s += ["%s"]' %
798 (typ
, len(ur
.get(typ
,'')), dat
))
805 def _check_bye(self
):
806 bye
= self
.untagged_responses
.get('BYE')
808 raise self
.abort(bye
[-1])
811 def _command(self
, name
, *args
):
813 if self
.state
not in Commands
[name
]:
815 raise self
.error("command %s illegal in state %s, "
816 "only allowed in states %s" %
818 ', '.join(Commands
[name
])))
820 for typ
in ('OK', 'NO', 'BAD'):
821 if typ
in self
.untagged_responses
:
822 del self
.untagged_responses
[typ
]
824 if 'READ-ONLY' in self
.untagged_responses \
825 and not self
.is_readonly
:
826 raise self
.readonly('mailbox status changed to READ-ONLY')
828 tag
= self
._new
_tag
()
829 data
= '%s %s' % (tag
, name
)
831 if arg
is None: continue
832 data
= '%s %s' % (data
, self
._checkquote
(arg
))
834 literal
= self
.literal
835 if literal
is not None:
837 if type(literal
) is type(self
._command
):
841 data
= '%s {%s}' % (data
, len(literal
))
845 self
._mesg
('> %s' % data
)
847 self
._log
('> %s' % data
)
850 self
.send('%s%s' % (data
, CRLF
))
851 except (socket
.error
, OSError), val
:
852 raise self
.abort('socket error: %s' % val
)
858 # Wait for continuation response
860 while self
._get
_response
():
861 if self
.tagged_commands
[tag
]: # BAD/NO?
867 literal
= literator(self
.continuation_response
)
871 self
._mesg
('write literal size %s' % len(literal
))
876 except (socket
.error
, OSError), val
:
877 raise self
.abort('socket error: %s' % val
)
885 def _command_complete(self
, name
, tag
):
888 typ
, data
= self
._get
_tagged
_response
(tag
)
889 except self
.abort
, val
:
890 raise self
.abort('command: %s => %s' % (name
, val
))
891 except self
.error
, val
:
892 raise self
.error('command: %s => %s' % (name
, val
))
895 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
899 def _get_response(self
):
901 # Read response and store.
903 # Returns None for continuation responses,
904 # otherwise first response line received.
906 resp
= self
._get
_line
()
908 # Command completion response?
910 if self
._match
(self
.tagre
, resp
):
911 tag
= self
.mo
.group('tag')
912 if not tag
in self
.tagged_commands
:
913 raise self
.abort('unexpected tagged response: %s' % resp
)
915 typ
= self
.mo
.group('type')
916 dat
= self
.mo
.group('data')
917 self
.tagged_commands
[tag
] = (typ
, [dat
])
921 # '*' (untagged) responses?
923 if not self
._match
(Untagged_response
, resp
):
924 if self
._match
(Untagged_status
, resp
):
925 dat2
= self
.mo
.group('data2')
928 # Only other possibility is '+' (continuation) response...
930 if self
._match
(Continuation
, resp
):
931 self
.continuation_response
= self
.mo
.group('data')
932 return None # NB: indicates continuation
934 raise self
.abort("unexpected response: '%s'" % resp
)
936 typ
= self
.mo
.group('type')
937 dat
= self
.mo
.group('data')
938 if dat
is None: dat
= '' # Null untagged response
939 if dat2
: dat
= dat
+ ' ' + dat2
941 # Is there a literal to come?
943 while self
._match
(Literal
, dat
):
945 # Read literal direct from connection.
947 size
= int(self
.mo
.group('size'))
950 self
._mesg
('read literal size %s' % size
)
951 data
= self
.read(size
)
953 # Store response with literal as tuple
955 self
._append
_untagged
(typ
, (dat
, data
))
957 # Read trailer - possibly containing another literal
959 dat
= self
._get
_line
()
961 self
._append
_untagged
(typ
, dat
)
963 # Bracketed response information?
965 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
966 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
969 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
970 self
._mesg
('%s response: %s' % (typ
, dat
))
975 def _get_tagged_response(self
, tag
):
978 result
= self
.tagged_commands
[tag
]
979 if result
is not None:
980 del self
.tagged_commands
[tag
]
983 # Some have reported "unexpected response" exceptions.
984 # Note that ignoring them here causes loops.
985 # Instead, send me details of the unexpected response and
986 # I'll update the code in `_get_response()'.
990 except self
.abort
, val
:
999 line
= self
.readline()
1001 raise self
.abort('socket error: EOF')
1003 # Protocol mandates all lines terminated by CRLF
1004 if not line
.endswith('\r\n'):
1005 raise self
.abort('socket error: unterminated line')
1010 self
._mesg
('< %s' % line
)
1012 self
._log
('< %s' % line
)
1016 def _match(self
, cre
, s
):
1018 # Run compiled regular expression match method on 's'.
1019 # Save result, return success.
1021 self
.mo
= cre
.match(s
)
1023 if self
.mo
is not None and self
.debug
>= 5:
1024 self
._mesg
("\tmatched r'%s' => %r" % (cre
.pattern
, self
.mo
.groups()))
1025 return self
.mo
is not None
1030 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
1031 self
.tagnum
= self
.tagnum
+ 1
1032 self
.tagged_commands
[tag
] = None
1036 def _checkquote(self
, arg
):
1038 # Must quote command args if non-alphanumeric chars present,
1039 # and not already quoted.
1041 if type(arg
) is not type(''):
1043 if len(arg
) >= 2 and (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
1045 if arg
and self
.mustquote
.search(arg
) is None:
1047 return self
._quote
(arg
)
1050 def _quote(self
, arg
):
1052 arg
= arg
.replace('\\', '\\\\')
1053 arg
= arg
.replace('"', '\\"')
1058 def _simple_command(self
, name
, *args
):
1060 return self
._command
_complete
(name
, self
._command
(name
, *args
))
1063 def _untagged_response(self
, typ
, dat
, name
):
1067 if not name
in self
.untagged_responses
:
1069 data
= self
.untagged_responses
.pop(name
)
1072 self
._mesg
('untagged_responses[%s] => %s' % (name
, data
))
1078 def _mesg(self
, s
, secs
=None):
1081 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1082 sys
.stderr
.write(' %s.%02d %s\n' % (tm
, (secs
*100)%100, s
))
1085 def _dump_ur(self
, dict):
1086 # Dump untagged responses (in `dict').
1090 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1091 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1093 def _log(self
, line
):
1094 # Keep log of last `_cmd_log_len' interactions for debugging.
1095 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, time
.time())
1096 self
._cmd
_log
_idx
+= 1
1097 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1098 self
._cmd
_log
_idx
= 0
1100 def print_log(self
):
1101 self
._mesg
('last %d IMAP4 interactions:' % len(self
._cmd
_log
))
1102 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1105 self
._mesg
(*self
._cmd
_log
[i
])
1109 if i
>= self
._cmd
_log
_len
:
1120 class IMAP4_SSL(IMAP4
):
1122 """IMAP4 client class over SSL connection
1124 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1126 host - host's name (default: localhost);
1127 port - port number (default: standard IMAP4 SSL port).
1128 keyfile - PEM formatted file that contains your private key (default: None);
1129 certfile - PEM formatted certificate chain file (default: None);
1131 for more documentation see the docstring of the parent class IMAP4.
1135 def __init__(self
, host
= '', port
= IMAP4_SSL_PORT
, keyfile
= None, certfile
= None):
1136 self
.keyfile
= keyfile
1137 self
.certfile
= certfile
1138 IMAP4
.__init__(self
, host
, port
)
1141 def open(self
, host
= '', port
= IMAP4_SSL_PORT
):
1142 """Setup connection to remote server on "host:port".
1143 (default: localhost:standard IMAP4 SSL port).
1144 This connection will be used by the routines:
1145 read, readline, send, shutdown.
1149 self
.sock
= socket
.create_connection((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(min(size
-read
, 16384))
1163 return ''.join(chunks
)
1167 """Read line from remote."""
1170 char
= self
.sslobj
.read(1)
1172 if char
in ("\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 subprocess.Popen()
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
.process
= subprocess
.Popen(self
.command
,
1236 stdin
=subprocess
.PIPE
, stdout
=subprocess
.PIPE
,
1237 shell
=True, close_fds
=True)
1238 self
.writefile
= self
.process
.stdin
1239 self
.readfile
= self
.process
.stdout
1242 def read(self
, size
):
1243 """Read 'size' bytes from remote."""
1244 return self
.readfile
.read(size
)
1248 """Read line from remote."""
1249 return self
.readfile
.readline()
1252 def send(self
, data
):
1253 """Send data to remote."""
1254 self
.writefile
.write(data
)
1255 self
.writefile
.flush()
1259 """Close I/O established in "open"."""
1260 self
.readfile
.close()
1261 self
.writefile
.close()
1266 class _Authenticator
:
1268 """Private class to provide en/decoding
1269 for base64-based authentication conversation.
1272 def __init__(self
, mechinst
):
1273 self
.mech
= mechinst
# Callable object to provide/process data
1275 def process(self
, data
):
1276 ret
= self
.mech(self
.decode(data
))
1278 return '*' # Abort conversation
1279 return self
.encode(ret
)
1281 def encode(self
, inp
):
1283 # Invoke binascii.b2a_base64 iteratively with
1284 # short even length buffers, strip the trailing
1285 # line feed from the result and append. "Even"
1286 # means a number that factors to both 6 and 8,
1287 # so when it gets to the end of the 8-bit input
1288 # there's no partial 6-bit output.
1298 e
= binascii
.b2a_base64(t
)
1303 def decode(self
, inp
):
1306 return binascii
.a2b_base64(inp
)
1310 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1311 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1313 def Internaldate2tuple(resp
):
1314 """Convert IMAP4 INTERNALDATE to UT.
1316 Returns Python time module tuple.
1319 mo
= InternalDate
.match(resp
)
1323 mon
= Mon2num
[mo
.group('mon')]
1324 zonen
= mo
.group('zonen')
1326 day
= int(mo
.group('day'))
1327 year
= int(mo
.group('year'))
1328 hour
= int(mo
.group('hour'))
1329 min = int(mo
.group('min'))
1330 sec
= int(mo
.group('sec'))
1331 zoneh
= int(mo
.group('zoneh'))
1332 zonem
= int(mo
.group('zonem'))
1334 # INTERNALDATE timezone must be subtracted to get UT
1336 zone
= (zoneh
*60 + zonem
)*60
1340 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1342 utc
= time
.mktime(tt
)
1344 # Following is necessary because the time module has no 'mkgmtime'.
1345 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1347 lt
= time
.localtime(utc
)
1348 if time
.daylight
and lt
[-1]:
1349 zone
= zone
+ time
.altzone
1351 zone
= zone
+ time
.timezone
1353 return time
.localtime(utc
- zone
)
1359 """Convert integer to A-P string representation."""
1361 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1364 num
, mod
= divmod(num
, 16)
1370 def ParseFlags(resp
):
1372 """Convert IMAP4 flags response to python tuple."""
1374 mo
= Flags
.match(resp
)
1378 return tuple(mo
.group('flags').split())
1381 def Time2Internaldate(date_time
):
1383 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1385 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1388 if isinstance(date_time
, (int, float)):
1389 tt
= time
.localtime(date_time
)
1390 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1392 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1393 return date_time
# Assume in correct format
1395 raise ValueError("date_time not of a known type")
1397 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1400 if time
.daylight
and tt
[-1]:
1401 zone
= -time
.altzone
1403 zone
= -time
.timezone
1404 return '"' + dt
+ " %+03d%02d" % divmod(zone
//60, 60) + '"'
1408 if __name__
== '__main__':
1410 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1411 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1412 # to test the IMAP4_stream class
1414 import getopt
, getpass
1417 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:s:')
1418 except getopt
.error
, val
:
1419 optlist
, args
= (), ()
1421 stream_command
= None
1422 for opt
,val
in optlist
:
1426 stream_command
= val
1427 if not args
: args
= (stream_command
,)
1429 if not args
: args
= ('',)
1433 USER
= getpass
.getuser()
1434 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1436 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':'\n'}
1438 ('login', (USER
, PASSWD
)),
1439 ('create', ('/tmp/xxx 1',)),
1440 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1441 ('CREATE', ('/tmp/yyz 2',)),
1442 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1443 ('list', ('/tmp', 'yy*')),
1444 ('select', ('/tmp/yyz 2',)),
1445 ('search', (None, 'SUBJECT', 'test')),
1446 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1447 ('store', ('1', 'FLAGS', '(\Deleted)')),
1456 ('response',('UIDVALIDITY',)),
1457 ('uid', ('SEARCH', 'ALL')),
1458 ('response', ('EXISTS',)),
1459 ('append', (None, None, None, test_mesg
)),
1465 M
._mesg
('%s %s' % (cmd
, args
))
1466 typ
, dat
= getattr(M
, cmd
)(*args
)
1467 M
._mesg
('%s => %s %s' % (cmd
, typ
, dat
))
1468 if typ
== 'NO': raise dat
[0]
1473 M
= IMAP4_stream(stream_command
)
1476 if M
.state
== 'AUTH':
1477 test_seq1
= test_seq1
[1:] # Login not needed
1478 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1479 M
._mesg
('CAPABILITIES = %r' % (M
.capabilities
,))
1481 for cmd
,args
in test_seq1
:
1484 for ml
in run('list', ('/tmp/', 'yy%')):
1485 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1486 if mo
: path
= mo
.group(1)
1487 else: path
= ml
.split()[-1]
1488 run('delete', (path
,))
1490 for cmd
,args
in test_seq2
:
1491 dat
= run(cmd
, args
)
1493 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1496 uid
= dat
[-1].split()
1497 if not uid
: continue
1498 run('uid', ('FETCH', '%s' % uid
[-1],
1499 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1501 print '\nAll tests OK.'
1504 print '\nTests failed.'
1508 If you would like to see debugging output,