]>
git.proxmox.com Git - mirror_edk2.git/blob - AppPkg/Applications/Python/Python-2.7.2/Lib/imaplib.py
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
, errno
, 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"."""
252 self
.sock
.shutdown(socket
.SHUT_RDWR
)
253 except socket
.error
as e
:
254 # The server might already have closed the connection
255 if e
.errno
!= errno
.ENOTCONN
:
262 """Return socket instance used to connect to IMAP4 server.
264 socket = <instance>.socket()
274 """Return most recent 'RECENT' responses if any exist,
275 else prompt server for an update using the 'NOOP' command.
277 (typ, [data]) = <instance>.recent()
279 'data' is None if no new messages,
280 else list of RECENT responses, most recent last.
283 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
286 typ
, dat
= self
.noop() # Prod server for response
287 return self
._untagged
_response
(typ
, dat
, name
)
290 def response(self
, code
):
291 """Return data for response 'code' if received, or None.
293 Old value for response 'code' is cleared.
295 (code, [data]) = <instance>.response(code)
297 return self
._untagged
_response
(code
, [None], code
.upper())
304 def append(self
, mailbox
, flags
, date_time
, message
):
305 """Append message to named mailbox.
307 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
309 All args except `message' can be None.
315 if (flags
[0],flags
[-1]) != ('(',')'):
316 flags
= '(%s)' % flags
320 date_time
= Time2Internaldate(date_time
)
323 self
.literal
= MapCRLF
.sub(CRLF
, message
)
324 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
327 def authenticate(self
, mechanism
, authobject
):
328 """Authenticate command - requires response processing.
330 'mechanism' specifies which authentication mechanism is to
331 be used - it must appear in <instance>.capabilities in the
332 form AUTH=<mechanism>.
334 'authobject' must be a callable object:
336 data = authobject(response)
338 It will be called to process server continuation responses.
339 It should return data that will be encoded and sent to server.
340 It should return None if the client abort response '*' should
343 mech
= mechanism
.upper()
344 # XXX: shouldn't this code be removed, not commented out?
345 #cap = 'AUTH=%s' % mech
346 #if not cap in self.capabilities: # Let the server decide!
347 # raise self.error("Server doesn't allow %s authentication." % mech)
348 self
.literal
= _Authenticator(authobject
).process
349 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
351 raise self
.error(dat
[-1])
356 def capability(self
):
357 """(typ, [data]) = <instance>.capability()
358 Fetch capabilities list from server."""
361 typ
, dat
= self
._simple
_command
(name
)
362 return self
._untagged
_response
(typ
, dat
, name
)
366 """Checkpoint mailbox on server.
368 (typ, [data]) = <instance>.check()
370 return self
._simple
_command
('CHECK')
374 """Close currently selected mailbox.
376 Deleted messages are removed from writable mailbox.
377 This is the recommended command before 'LOGOUT'.
379 (typ, [data]) = <instance>.close()
382 typ
, dat
= self
._simple
_command
('CLOSE')
388 def copy(self
, message_set
, new_mailbox
):
389 """Copy 'message_set' messages onto end of 'new_mailbox'.
391 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
393 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
396 def create(self
, mailbox
):
397 """Create new mailbox.
399 (typ, [data]) = <instance>.create(mailbox)
401 return self
._simple
_command
('CREATE', mailbox
)
404 def delete(self
, mailbox
):
405 """Delete old mailbox.
407 (typ, [data]) = <instance>.delete(mailbox)
409 return self
._simple
_command
('DELETE', mailbox
)
411 def deleteacl(self
, mailbox
, who
):
412 """Delete the ACLs (remove any rights) set for who on mailbox.
414 (typ, [data]) = <instance>.deleteacl(mailbox, who)
416 return self
._simple
_command
('DELETEACL', mailbox
, who
)
419 """Permanently remove deleted items from selected mailbox.
421 Generates 'EXPUNGE' response for each deleted message.
423 (typ, [data]) = <instance>.expunge()
425 'data' is list of 'EXPUNGE'd message numbers in order received.
428 typ
, dat
= self
._simple
_command
(name
)
429 return self
._untagged
_response
(typ
, dat
, name
)
432 def fetch(self
, message_set
, message_parts
):
433 """Fetch (parts of) messages.
435 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
437 'message_parts' should be a string of selected parts
438 enclosed in parentheses, eg: "(UID BODY[TEXT])".
440 'data' are tuples of message part envelope and data.
443 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
444 return self
._untagged
_response
(typ
, dat
, name
)
447 def getacl(self
, mailbox
):
448 """Get the ACLs for a mailbox.
450 (typ, [data]) = <instance>.getacl(mailbox)
452 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
453 return self
._untagged
_response
(typ
, dat
, 'ACL')
456 def getannotation(self
, mailbox
, entry
, attribute
):
457 """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
458 Retrieve ANNOTATIONs."""
460 typ
, dat
= self
._simple
_command
('GETANNOTATION', mailbox
, entry
, attribute
)
461 return self
._untagged
_response
(typ
, dat
, 'ANNOTATION')
464 def getquota(self
, root
):
465 """Get the quota root's resource usage and limits.
467 Part of the IMAP4 QUOTA extension defined in rfc2087.
469 (typ, [data]) = <instance>.getquota(root)
471 typ
, dat
= self
._simple
_command
('GETQUOTA', root
)
472 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
475 def getquotaroot(self
, mailbox
):
476 """Get the list of quota roots for the named mailbox.
478 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
480 typ
, dat
= self
._simple
_command
('GETQUOTAROOT', mailbox
)
481 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
482 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
483 return typ
, [quotaroot
, quota
]
486 def list(self
, directory
='""', pattern
='*'):
487 """List mailbox names in directory matching pattern.
489 (typ, [data]) = <instance>.list(directory='""', pattern='*')
491 'data' is list of LIST responses.
494 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
495 return self
._untagged
_response
(typ
, dat
, name
)
498 def login(self
, user
, password
):
499 """Identify client using plaintext password.
501 (typ, [data]) = <instance>.login(user, password)
503 NB: 'password' will be quoted.
505 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
507 raise self
.error(dat
[-1])
512 def login_cram_md5(self
, user
, password
):
513 """ Force use of CRAM-MD5 authentication.
515 (typ, [data]) = <instance>.login_cram_md5(user, password)
517 self
.user
, self
.password
= user
, password
518 return self
.authenticate('CRAM-MD5', self
._CRAM
_MD
5_AUTH
)
521 def _CRAM_MD5_AUTH(self
, challenge
):
522 """ Authobject to use with CRAM-MD5 authentication. """
524 return self
.user
+ " " + hmac
.HMAC(self
.password
, challenge
).hexdigest()
528 """Shutdown connection to server.
530 (typ, [data]) = <instance>.logout()
532 Returns server 'BYE' response.
534 self
.state
= 'LOGOUT'
535 try: typ
, dat
= self
._simple
_command
('LOGOUT')
536 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
538 if 'BYE' in self
.untagged_responses
:
539 return 'BYE', self
.untagged_responses
['BYE']
543 def lsub(self
, directory
='""', pattern
='*'):
544 """List 'subscribed' mailbox names in directory matching pattern.
546 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
548 'data' are tuples of message part envelope and data.
551 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
552 return self
._untagged
_response
(typ
, dat
, name
)
554 def myrights(self
, mailbox
):
555 """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
557 (typ, [data]) = <instance>.myrights(mailbox)
559 typ
,dat
= self
._simple
_command
('MYRIGHTS', mailbox
)
560 return self
._untagged
_response
(typ
, dat
, 'MYRIGHTS')
563 """ Returns IMAP namespaces ala rfc2342
565 (typ, [data, ...]) = <instance>.namespace()
568 typ
, dat
= self
._simple
_command
(name
)
569 return self
._untagged
_response
(typ
, dat
, name
)
573 """Send NOOP command.
575 (typ, [data]) = <instance>.noop()
579 self
._dump
_ur
(self
.untagged_responses
)
580 return self
._simple
_command
('NOOP')
583 def partial(self
, message_num
, message_part
, start
, length
):
584 """Fetch truncated part of a message.
586 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
588 'data' is tuple of message part envelope and data.
591 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
592 return self
._untagged
_response
(typ
, dat
, 'FETCH')
595 def proxyauth(self
, user
):
596 """Assume authentication as "user".
598 Allows an authorised administrator to proxy into any user's
601 (typ, [data]) = <instance>.proxyauth(user)
605 return self
._simple
_command
('PROXYAUTH', user
)
608 def rename(self
, oldmailbox
, newmailbox
):
609 """Rename old mailbox name to new.
611 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
613 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
616 def search(self
, charset
, *criteria
):
617 """Search mailbox for matching messages.
619 (typ, [data]) = <instance>.search(charset, criterion, ...)
621 'data' is space separated list of matching message numbers.
625 typ
, dat
= self
._simple
_command
(name
, 'CHARSET', charset
, *criteria
)
627 typ
, dat
= self
._simple
_command
(name
, *criteria
)
628 return self
._untagged
_response
(typ
, dat
, name
)
631 def select(self
, mailbox
='INBOX', readonly
=False):
634 Flush all untagged responses.
636 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
638 'data' is count of messages in mailbox ('EXISTS' response).
640 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
641 other responses should be obtained via <instance>.response('FLAGS') etc.
643 self
.untagged_responses
= {} # Flush old responses.
644 self
.is_readonly
= readonly
649 typ
, dat
= self
._simple
_command
(name
, mailbox
)
651 self
.state
= 'AUTH' # Might have been 'SELECTED'
653 self
.state
= 'SELECTED'
654 if 'READ-ONLY' in self
.untagged_responses \
658 self
._dump
_ur
(self
.untagged_responses
)
659 raise self
.readonly('%s is not writable' % mailbox
)
660 return typ
, self
.untagged_responses
.get('EXISTS', [None])
663 def setacl(self
, mailbox
, who
, what
):
664 """Set a mailbox acl.
666 (typ, [data]) = <instance>.setacl(mailbox, who, what)
668 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
671 def setannotation(self
, *args
):
672 """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
675 typ
, dat
= self
._simple
_command
('SETANNOTATION', *args
)
676 return self
._untagged
_response
(typ
, dat
, 'ANNOTATION')
679 def setquota(self
, root
, limits
):
680 """Set the quota root's resource limits.
682 (typ, [data]) = <instance>.setquota(root, limits)
684 typ
, dat
= self
._simple
_command
('SETQUOTA', root
, limits
)
685 return self
._untagged
_response
(typ
, dat
, 'QUOTA')
688 def sort(self
, sort_criteria
, charset
, *search_criteria
):
689 """IMAP4rev1 extension SORT command.
691 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
694 #if not name in self.capabilities: # Let the server decide!
695 # raise self.error('unimplemented extension command: %s' % name)
696 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
697 sort_criteria
= '(%s)' % sort_criteria
698 typ
, dat
= self
._simple
_command
(name
, sort_criteria
, charset
, *search_criteria
)
699 return self
._untagged
_response
(typ
, dat
, name
)
702 def status(self
, mailbox
, names
):
703 """Request named status conditions for mailbox.
705 (typ, [data]) = <instance>.status(mailbox, names)
708 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
709 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
710 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
711 return self
._untagged
_response
(typ
, dat
, name
)
714 def store(self
, message_set
, command
, flags
):
715 """Alters flag dispositions for messages in mailbox.
717 (typ, [data]) = <instance>.store(message_set, command, flags)
719 if (flags
[0],flags
[-1]) != ('(',')'):
720 flags
= '(%s)' % flags
# Avoid quoting the flags
721 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
722 return self
._untagged
_response
(typ
, dat
, 'FETCH')
725 def subscribe(self
, mailbox
):
726 """Subscribe to new mailbox.
728 (typ, [data]) = <instance>.subscribe(mailbox)
730 return self
._simple
_command
('SUBSCRIBE', mailbox
)
733 def thread(self
, threading_algorithm
, charset
, *search_criteria
):
734 """IMAPrev1 extension THREAD command.
736 (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
739 typ
, dat
= self
._simple
_command
(name
, threading_algorithm
, charset
, *search_criteria
)
740 return self
._untagged
_response
(typ
, dat
, name
)
743 def uid(self
, command
, *args
):
744 """Execute "command arg ..." with messages identified by UID,
745 rather than message number.
747 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
749 Returns response appropriate to 'command'.
751 command
= command
.upper()
752 if not command
in Commands
:
753 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
754 if self
.state
not in Commands
[command
]:
755 raise self
.error("command %s illegal in state %s, "
756 "only allowed in states %s" %
757 (command
, self
.state
,
758 ', '.join(Commands
[command
])))
760 typ
, dat
= self
._simple
_command
(name
, command
, *args
)
761 if command
in ('SEARCH', 'SORT', 'THREAD'):
765 return self
._untagged
_response
(typ
, dat
, name
)
768 def unsubscribe(self
, mailbox
):
769 """Unsubscribe from old mailbox.
771 (typ, [data]) = <instance>.unsubscribe(mailbox)
773 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
776 def xatom(self
, name
, *args
):
777 """Allow simple extension commands
778 notified by server in CAPABILITY response.
780 Assumes command is legal in current state.
782 (typ, [data]) = <instance>.xatom(name, arg, ...)
784 Returns response appropriate to extension command `name'.
787 #if not name in self.capabilities: # Let the server decide!
788 # raise self.error('unknown extension command: %s' % name)
789 if not name
in Commands
:
790 Commands
[name
] = (self
.state
,)
791 return self
._simple
_command
(name
, *args
)
798 def _append_untagged(self
, typ
, dat
):
800 if dat
is None: dat
= ''
801 ur
= self
.untagged_responses
804 self
._mesg
('untagged_responses[%s] %s += ["%s"]' %
805 (typ
, len(ur
.get(typ
,'')), dat
))
812 def _check_bye(self
):
813 bye
= self
.untagged_responses
.get('BYE')
815 raise self
.abort(bye
[-1])
818 def _command(self
, name
, *args
):
820 if self
.state
not in Commands
[name
]:
822 raise self
.error("command %s illegal in state %s, "
823 "only allowed in states %s" %
825 ', '.join(Commands
[name
])))
827 for typ
in ('OK', 'NO', 'BAD'):
828 if typ
in self
.untagged_responses
:
829 del self
.untagged_responses
[typ
]
831 if 'READ-ONLY' in self
.untagged_responses \
832 and not self
.is_readonly
:
833 raise self
.readonly('mailbox status changed to READ-ONLY')
835 tag
= self
._new
_tag
()
836 data
= '%s %s' % (tag
, name
)
838 if arg
is None: continue
839 data
= '%s %s' % (data
, self
._checkquote
(arg
))
841 literal
= self
.literal
842 if literal
is not None:
844 if type(literal
) is type(self
._command
):
848 data
= '%s {%s}' % (data
, len(literal
))
852 self
._mesg
('> %s' % data
)
854 self
._log
('> %s' % data
)
857 self
.send('%s%s' % (data
, CRLF
))
858 except (socket
.error
, OSError), val
:
859 raise self
.abort('socket error: %s' % val
)
865 # Wait for continuation response
867 while self
._get
_response
():
868 if self
.tagged_commands
[tag
]: # BAD/NO?
874 literal
= literator(self
.continuation_response
)
878 self
._mesg
('write literal size %s' % len(literal
))
883 except (socket
.error
, OSError), val
:
884 raise self
.abort('socket error: %s' % val
)
892 def _command_complete(self
, name
, tag
):
893 # BYE is expected after LOGOUT
897 typ
, data
= self
._get
_tagged
_response
(tag
)
898 except self
.abort
, val
:
899 raise self
.abort('command: %s => %s' % (name
, val
))
900 except self
.error
, val
:
901 raise self
.error('command: %s => %s' % (name
, val
))
905 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
909 def _get_response(self
):
911 # Read response and store.
913 # Returns None for continuation responses,
914 # otherwise first response line received.
916 resp
= self
._get
_line
()
918 # Command completion response?
920 if self
._match
(self
.tagre
, resp
):
921 tag
= self
.mo
.group('tag')
922 if not tag
in self
.tagged_commands
:
923 raise self
.abort('unexpected tagged response: %s' % resp
)
925 typ
= self
.mo
.group('type')
926 dat
= self
.mo
.group('data')
927 self
.tagged_commands
[tag
] = (typ
, [dat
])
931 # '*' (untagged) responses?
933 if not self
._match
(Untagged_response
, resp
):
934 if self
._match
(Untagged_status
, resp
):
935 dat2
= self
.mo
.group('data2')
938 # Only other possibility is '+' (continuation) response...
940 if self
._match
(Continuation
, resp
):
941 self
.continuation_response
= self
.mo
.group('data')
942 return None # NB: indicates continuation
944 raise self
.abort("unexpected response: '%s'" % resp
)
946 typ
= self
.mo
.group('type')
947 dat
= self
.mo
.group('data')
948 if dat
is None: dat
= '' # Null untagged response
949 if dat2
: dat
= dat
+ ' ' + dat2
951 # Is there a literal to come?
953 while self
._match
(Literal
, dat
):
955 # Read literal direct from connection.
957 size
= int(self
.mo
.group('size'))
960 self
._mesg
('read literal size %s' % size
)
961 data
= self
.read(size
)
963 # Store response with literal as tuple
965 self
._append
_untagged
(typ
, (dat
, data
))
967 # Read trailer - possibly containing another literal
969 dat
= self
._get
_line
()
971 self
._append
_untagged
(typ
, dat
)
973 # Bracketed response information?
975 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
976 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
979 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
980 self
._mesg
('%s response: %s' % (typ
, dat
))
985 def _get_tagged_response(self
, tag
):
988 result
= self
.tagged_commands
[tag
]
989 if result
is not None:
990 del self
.tagged_commands
[tag
]
993 # Some have reported "unexpected response" exceptions.
994 # Note that ignoring them here causes loops.
995 # Instead, send me details of the unexpected response and
996 # I'll update the code in `_get_response()'.
1000 except self
.abort
, val
:
1007 def _get_line(self
):
1009 line
= self
.readline()
1011 raise self
.abort('socket error: EOF')
1013 # Protocol mandates all lines terminated by CRLF
1014 if not line
.endswith('\r\n'):
1015 raise self
.abort('socket error: unterminated line')
1020 self
._mesg
('< %s' % line
)
1022 self
._log
('< %s' % line
)
1026 def _match(self
, cre
, s
):
1028 # Run compiled regular expression match method on 's'.
1029 # Save result, return success.
1031 self
.mo
= cre
.match(s
)
1033 if self
.mo
is not None and self
.debug
>= 5:
1034 self
._mesg
("\tmatched r'%s' => %r" % (cre
.pattern
, self
.mo
.groups()))
1035 return self
.mo
is not None
1040 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
1041 self
.tagnum
= self
.tagnum
+ 1
1042 self
.tagged_commands
[tag
] = None
1046 def _checkquote(self
, arg
):
1048 # Must quote command args if non-alphanumeric chars present,
1049 # and not already quoted.
1051 if type(arg
) is not type(''):
1053 if len(arg
) >= 2 and (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
1055 if arg
and self
.mustquote
.search(arg
) is None:
1057 return self
._quote
(arg
)
1060 def _quote(self
, arg
):
1062 arg
= arg
.replace('\\', '\\\\')
1063 arg
= arg
.replace('"', '\\"')
1068 def _simple_command(self
, name
, *args
):
1070 return self
._command
_complete
(name
, self
._command
(name
, *args
))
1073 def _untagged_response(self
, typ
, dat
, name
):
1077 if not name
in self
.untagged_responses
:
1079 data
= self
.untagged_responses
.pop(name
)
1082 self
._mesg
('untagged_responses[%s] => %s' % (name
, data
))
1088 def _mesg(self
, s
, secs
=None):
1091 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1092 sys
.stderr
.write(' %s.%02d %s\n' % (tm
, (secs
*100)%100, s
))
1095 def _dump_ur(self
, dict):
1096 # Dump untagged responses (in `dict').
1100 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1101 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1103 def _log(self
, line
):
1104 # Keep log of last `_cmd_log_len' interactions for debugging.
1105 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, time
.time())
1106 self
._cmd
_log
_idx
+= 1
1107 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1108 self
._cmd
_log
_idx
= 0
1110 def print_log(self
):
1111 self
._mesg
('last %d IMAP4 interactions:' % len(self
._cmd
_log
))
1112 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1115 self
._mesg
(*self
._cmd
_log
[i
])
1119 if i
>= self
._cmd
_log
_len
:
1130 class IMAP4_SSL(IMAP4
):
1132 """IMAP4 client class over SSL connection
1134 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1136 host - host's name (default: localhost);
1137 port - port number (default: standard IMAP4 SSL port).
1138 keyfile - PEM formatted file that contains your private key (default: None);
1139 certfile - PEM formatted certificate chain file (default: None);
1141 for more documentation see the docstring of the parent class IMAP4.
1145 def __init__(self
, host
= '', port
= IMAP4_SSL_PORT
, keyfile
= None, certfile
= None):
1146 self
.keyfile
= keyfile
1147 self
.certfile
= certfile
1148 IMAP4
.__init__(self
, host
, port
)
1151 def open(self
, host
= '', port
= IMAP4_SSL_PORT
):
1152 """Setup connection to remote server on "host:port".
1153 (default: localhost:standard IMAP4 SSL port).
1154 This connection will be used by the routines:
1155 read, readline, send, shutdown.
1159 self
.sock
= socket
.create_connection((host
, port
))
1160 self
.sslobj
= ssl
.wrap_socket(self
.sock
, self
.keyfile
, self
.certfile
)
1161 self
.file = self
.sslobj
.makefile('rb')
1164 def read(self
, size
):
1165 """Read 'size' bytes from remote."""
1166 return self
.file.read(size
)
1170 """Read line from remote."""
1171 return self
.file.readline()
1174 def send(self
, data
):
1175 """Send data to remote."""
1178 sent
= self
.sslobj
.write(data
)
1182 bytes
= bytes
- sent
1186 """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 """Parse an IMAP4 INTERNALDATE string.
1316 Return corresponding local time. The return value is a
1317 time.struct_time instance or None if the string has wrong format.
1320 mo
= InternalDate
.match(resp
)
1324 mon
= Mon2num
[mo
.group('mon')]
1325 zonen
= mo
.group('zonen')
1327 day
= int(mo
.group('day'))
1328 year
= int(mo
.group('year'))
1329 hour
= int(mo
.group('hour'))
1330 min = int(mo
.group('min'))
1331 sec
= int(mo
.group('sec'))
1332 zoneh
= int(mo
.group('zoneh'))
1333 zonem
= int(mo
.group('zonem'))
1335 # INTERNALDATE timezone must be subtracted to get UT
1337 zone
= (zoneh
*60 + zonem
)*60
1341 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1343 utc
= time
.mktime(tt
)
1345 # Following is necessary because the time module has no 'mkgmtime'.
1346 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1348 lt
= time
.localtime(utc
)
1349 if time
.daylight
and lt
[-1]:
1350 zone
= zone
+ time
.altzone
1352 zone
= zone
+ time
.timezone
1354 return time
.localtime(utc
- zone
)
1360 """Convert integer to A-P string representation."""
1362 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1365 num
, mod
= divmod(num
, 16)
1371 def ParseFlags(resp
):
1373 """Convert IMAP4 flags response to python tuple."""
1375 mo
= Flags
.match(resp
)
1379 return tuple(mo
.group('flags').split())
1382 def Time2Internaldate(date_time
):
1384 """Convert date_time to IMAP4 INTERNALDATE representation.
1386 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'. The
1387 date_time argument can be a number (int or float) representing
1388 seconds since epoch (as returned by time.time()), a 9-tuple
1389 representing local time (as returned by time.localtime()), or a
1390 double-quoted string. In the last case, it is assumed to already
1391 be in the correct format.
1394 if isinstance(date_time
, (int, float)):
1395 tt
= time
.localtime(date_time
)
1396 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1398 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1399 return date_time
# Assume in correct format
1401 raise ValueError("date_time not of a known type")
1403 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1406 if time
.daylight
and tt
[-1]:
1407 zone
= -time
.altzone
1409 zone
= -time
.timezone
1410 return '"' + dt
+ " %+03d%02d" % divmod(zone
//60, 60) + '"'
1414 if __name__
== '__main__':
1416 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1417 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1418 # to test the IMAP4_stream class
1420 import getopt
, getpass
1423 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:s:')
1424 except getopt
.error
, val
:
1425 optlist
, args
= (), ()
1427 stream_command
= None
1428 for opt
,val
in optlist
:
1432 stream_command
= val
1433 if not args
: args
= (stream_command
,)
1435 if not args
: args
= ('',)
1439 USER
= getpass
.getuser()
1440 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1442 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':'\n'}
1444 ('login', (USER
, PASSWD
)),
1445 ('create', ('/tmp/xxx 1',)),
1446 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1447 ('CREATE', ('/tmp/yyz 2',)),
1448 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1449 ('list', ('/tmp', 'yy*')),
1450 ('select', ('/tmp/yyz 2',)),
1451 ('search', (None, 'SUBJECT', 'test')),
1452 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1453 ('store', ('1', 'FLAGS', '(\Deleted)')),
1462 ('response',('UIDVALIDITY',)),
1463 ('uid', ('SEARCH', 'ALL')),
1464 ('response', ('EXISTS',)),
1465 ('append', (None, None, None, test_mesg
)),
1471 M
._mesg
('%s %s' % (cmd
, args
))
1472 typ
, dat
= getattr(M
, cmd
)(*args
)
1473 M
._mesg
('%s => %s %s' % (cmd
, typ
, dat
))
1474 if typ
== 'NO': raise dat
[0]
1479 M
= IMAP4_stream(stream_command
)
1482 if M
.state
== 'AUTH':
1483 test_seq1
= test_seq1
[1:] # Login not needed
1484 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1485 M
._mesg
('CAPABILITIES = %r' % (M
.capabilities
,))
1487 for cmd
,args
in test_seq1
:
1490 for ml
in run('list', ('/tmp/', 'yy%')):
1491 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1492 if mo
: path
= mo
.group(1)
1493 else: path
= ml
.split()[-1]
1494 run('delete', (path
,))
1496 for cmd
,args
in test_seq2
:
1497 dat
= run(cmd
, args
)
1499 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1502 uid
= dat
[-1].split()
1503 if not uid
: continue
1504 run('uid', ('FETCH', '%s' % uid
[-1],
1505 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1507 print '\nAll tests OK.'
1510 print '\nTests failed.'
1514 If you would like to see debugging output,