]>
code.delx.au - offlineimap/blob - offlineimap/imaplib2.py
1 #!/usr/bin/env python2.5
3 """Threaded IMAP4 client.
5 Based on RFC 2060 and original imaplib module.
11 Public functions: Internaldate2Time
17 __all__
= ("IMAP4", "IMAP4_SSL", "IMAP4_stream",
18 "Internaldate2Time", "ParseFlags", "Time2Internaldate")
24 Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
25 String method conversion by ESR, February 2001.
26 GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
27 IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
28 GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
29 PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002.
30 IDLE via threads suggested by Philippe Normand <phil@respyre.org> January 2005.
31 GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
32 New socket open code from http://www.python.org/doc/lib/socket-example.html."""
33 __author__
= "Piers Lauder <piers@janeelix.com>"
34 # Source URL: http://www.cs.usyd.edu.au/~piers/python/imaplib2
36 import binascii
, os
, Queue
, random
, re
, select
, socket
, sys
, time
, threading
38 select_module
= select
43 Debug
= None # Backward compatibility
47 IDLE_TIMEOUT_RESPONSE
= '* IDLE TIMEOUT'
48 IDLE_TIMEOUT
= 60*29 # Don't stay in IDLE state longer
50 AllowedVersions
= ('IMAP4REV1', 'IMAP4') # Most recent first
56 NONAUTH
, AUTH
, SELECTED
, LOGOUT
= 'NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'
59 # name valid states asynchronous
60 'APPEND': ((AUTH
, SELECTED
), False),
61 'AUTHENTICATE': ((NONAUTH
,), False),
62 'CAPABILITY': ((NONAUTH
, AUTH
, SELECTED
), True),
63 'CHECK': ((SELECTED
,), True),
64 'CLOSE': ((SELECTED
,), False),
65 'COPY': ((SELECTED
,), True),
66 'CREATE': ((AUTH
, SELECTED
), True),
67 'DELETE': ((AUTH
, SELECTED
), True),
68 'DELETEACL': ((AUTH
, SELECTED
), True),
69 'EXAMINE': ((AUTH
, SELECTED
), False),
70 'EXPUNGE': ((SELECTED
,), True),
71 'FETCH': ((SELECTED
,), True),
72 'GETACL': ((AUTH
, SELECTED
), True),
73 'GETANNOTATION':((AUTH
, SELECTED
), True),
74 'GETQUOTA': ((AUTH
, SELECTED
), True),
75 'GETQUOTAROOT': ((AUTH
, SELECTED
), True),
76 'IDLE': ((SELECTED
,), False),
77 'LIST': ((AUTH
, SELECTED
), True),
78 'LOGIN': ((NONAUTH
,), False),
79 'LOGOUT': ((NONAUTH
, AUTH
, LOGOUT
, SELECTED
), False),
80 'LSUB': ((AUTH
, SELECTED
), True),
81 'MYRIGHTS': ((AUTH
, SELECTED
), True),
82 'NAMESPACE': ((AUTH
, SELECTED
), True),
83 'NOOP': ((NONAUTH
, AUTH
, SELECTED
), True),
84 'PARTIAL': ((SELECTED
,), True),
85 'PROXYAUTH': ((AUTH
,), False),
86 'RENAME': ((AUTH
, SELECTED
), True),
87 'SEARCH': ((SELECTED
,), True),
88 'SELECT': ((AUTH
, SELECTED
), False),
89 'SETACL': ((AUTH
, SELECTED
), False),
90 'SETANNOTATION':((AUTH
, SELECTED
), True),
91 'SETQUOTA': ((AUTH
, SELECTED
), False),
92 'SORT': ((SELECTED
,), True),
93 'STATUS': ((AUTH
, SELECTED
), True),
94 'STORE': ((SELECTED
,), True),
95 'SUBSCRIBE': ((AUTH
, SELECTED
), False),
96 'THREAD': ((SELECTED
,), True),
97 'UID': ((SELECTED
,), True),
98 'UNSUBSCRIBE': ((AUTH
, SELECTED
), False),
101 UID_direct
= ('SEARCH', 'SORT', 'THREAD')
106 """string = Int2AP(num)
107 Return 'num' converted to a string using characters from the set 'A'..'P'
110 val
, a2p
= [], 'ABCDEFGHIJKLMNOP'
113 num
, mod
= divmod(num
, 16)
114 val
.insert(0, a2p
[mod
])
119 class Request(object):
121 """Private class to represent a request awaiting response."""
123 def __init__(self
, parent
, name
=None, callback
=None, cb_arg
=None):
125 self
.callback
= callback
# Function called to process result
126 self
.callback_arg
= cb_arg
# Optional arg passed to "callback"
128 self
.tag
= '%s%s' % (parent
.tagpre
, parent
.tagnum
)
131 self
.ready
= threading
.Event()
137 def abort(self
, typ
, val
):
138 self
.aborted
= (typ
, val
)
142 def get_response(self
, exc_fmt
=None):
146 if self
.aborted
is not None:
147 typ
, val
= self
.aborted
149 exc_fmt
= '%s - %%s' % typ
150 raise typ(exc_fmt
% str(val
))
155 def deliver(self
, response
):
156 if self
.callback
is not None:
157 self
.callback((response
, self
.callback_arg
, self
.aborted
))
160 self
.response
= response
168 """Threaded IMAP4 client class.
171 IMAP4(host=None, port=None, debug=None, debug_file=None)
173 host - host's name (default: localhost);
174 port - port number (default: standard IMAP4 port);
175 debug - debug level (default: 0 - no debug);
176 debug_file - debug stream (default: sys.stderr).
178 All IMAP4rev1 commands are supported by methods of the same name.
180 Each command returns a tuple: (type, [data, ...]) where 'type'
181 is usually 'OK' or 'NO', and 'data' is either the text from the
182 tagged response, or untagged results from command. Each 'data' is
183 either a string, or a tuple. If a tuple, then the first part is the
184 header of the response, and the second part contains the data (ie:
187 Errors raise the exception class <instance>.error("<reason>").
188 IMAP4 server errors raise <instance>.abort("<reason>"), which is
189 a sub-class of 'error'. Mailbox status changes from READ-WRITE to
190 READ-ONLY raise the exception class <instance>.readonly("<reason>"),
191 which is a sub-class of 'abort'.
193 "error" exceptions imply a program error.
194 "abort" exceptions imply the connection should be reset, and
195 the command re-tried.
196 "readonly" exceptions imply the command should be re-tried.
198 All commands take two optional named arguments:
199 'callback' and 'cb_arg'
200 If 'callback' is provided then the command is asynchronous, so after
201 the command is queued for transmission, the call returns immediately
202 with the tuple (None, None).
203 The result will be posted by invoking "callback" with one arg, a tuple:
204 callback((result, cb_arg, None))
205 or, if there was a problem:
206 callback((None, cb_arg, (exception class, reason)))
208 Otherwise the command is synchronous (waits for result). But note
209 that state-changing commands will both block until previous commands
210 have completed, and block subsequent commands until they have finished.
212 All (non-callback) arguments to commands are converted to strings,
213 except for AUTHENTICATE, and the last argument to APPEND which is
214 passed as an IMAP4 literal. If necessary (the string contains any
215 non-printing characters or white-space and isn't enclosed with either
216 parentheses or double quotes) each string is quoted. However, the
217 'password' argument to the LOGIN command is always quoted. If you
218 want to avoid having an argument string quoted (eg: the 'flags'
219 argument to STORE) then enclose the string in parentheses (eg:
222 There is one instance variable, 'state', that is useful for tracking
223 whether the client needs to login to the server. If it has the
224 value "AUTH" after instantiating the class, then the connection
225 is pre-authenticated (otherwise it will be "NONAUTH"). Selecting a
226 mailbox changes the state to be "SELECTED", closing a mailbox changes
227 back to "AUTH", and once the client has logged out, the state changes
228 to "LOGOUT" and no further commands may be issued.
230 Note: to use this module, you must read the RFCs pertaining to the
231 IMAP4 protocol, as the semantics of the arguments to each IMAP4
232 command are left to the invoker, not to mention the results. Also,
233 most IMAP servers implement a sub-set of the commands available here.
235 Note also that you must call logout() to shut down threads before
236 discarding an instance.
239 class error(Exception): pass # Logical errors - debug required
240 class abort(error
): pass # Service errors - close and retry
241 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
244 continuation_cre
= re
.compile(r
'\+( (?P<data>.*))?')
245 literal_cre
= re
.compile(r
'.*{(?P<size>\d+)}$')
246 mapCRLF_cre
= re
.compile(r
'\r\n|\r|\n')
247 mustquote_cre
= re
.compile(r
"[^\w!#$%&'*+,.:;<=>?^`|~-]")
248 response_code_cre
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
249 untagged_response_cre
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
250 untagged_status_cre
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
253 def __init__(self
, host
=None, port
=None, debug
=None, debug_file
=None):
255 self
.state
= NONAUTH
# IMAP4 protocol state
256 self
.literal
= None # A literal argument to a command
257 self
.tagged_commands
= {} # Tagged commands awaiting response
258 self
.untagged_responses
= {} # {typ: [data, ...], ...}
259 self
.is_readonly
= False # READ-ONLY desired state
260 self
.idle_rqb
= None # Server IDLE Request - see _IdleCont
261 self
.idle_timeout
= None # Must prod server occasionally
263 self
._expecting
_data
= 0 # Expecting message data
264 self
._accumulated
_data
= [] # Message data accumulated so far
265 self
._literal
_expected
= None # Message data descriptor
267 # Create unique tag for this session,
268 # and compile tagged response matcher.
271 self
.tagpre
= Int2AP(random
.randint(4096, 65535))
272 self
.tagre
= re
.compile(r
'(?P<tag>'
274 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
276 if __debug__
: self
._init
_debug
(debug
, debug_file
)
278 # Open socket to server.
280 self
.open(host
, port
)
284 self
._mesg
('connected to %s on port %s' % (self
.host
, self
.port
))
288 self
.Terminate
= False
290 self
.state_change_free
= threading
.Event()
291 self
.state_change_pending
= threading
.Lock()
292 self
.commands_lock
= threading
.Lock()
294 self
.ouq
= Queue
.Queue(10)
295 self
.inq
= Queue
.Queue()
297 self
.wrth
= threading
.Thread(target
=self
._writer
)
299 self
.rdth
= threading
.Thread(target
=self
._reader
)
301 self
.inth
= threading
.Thread(target
=self
._handler
)
304 # Get server welcome message,
305 # request and store CAPABILITY response.
308 self
.welcome
= self
._request
_push
(tag
='continuation').get_response('IMAP4 protocol error: %s')[1]
310 if 'PREAUTH' in self
.untagged_responses
:
312 if __debug__
: self
._log
(1, 'state => AUTH')
313 elif 'OK' in self
.untagged_responses
:
314 if __debug__
: self
._log
(1, 'state => NONAUTH')
316 raise self
.error(self
.welcome
)
318 typ
, dat
= self
.capability()
320 raise self
.error('no CAPABILITY response from server')
321 self
.capabilities
= tuple(dat
[-1].upper().split())
322 if __debug__
: self
._log
(3, 'CAPABILITY: %r' % (self
.capabilities
,))
324 for version
in AllowedVersions
:
325 if not version
in self
.capabilities
:
327 self
.PROTOCOL_VERSION
= version
330 raise self
.error('server not IMAP4 compliant')
332 self
._close
_threads
()
336 def __getattr__(self
, attr
):
337 # Allow UPPERCASE variants of IMAP4 command methods.
339 return getattr(self
, attr
.lower())
340 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
344 # Overridable methods
347 def open(self
, host
=None, port
=None):
348 """open(host=None, port=None)
349 Setup connection to remote server on "host:port"
350 (default: localhost:standard IMAP4 port).
351 This connection will be used by the routines:
352 read, send, shutdown, socket."""
354 self
.host
= host
is not None and host
or ''
355 self
.port
= port
is not None and port
or IMAP4_PORT
356 self
.sock
= self
.open_socket()
357 self
.read_fd
= self
.sock
.fileno()
360 def open_socket(self
):
361 """Open socket choosing first address family available."""
363 msg
= (-1, 'could not open socket')
364 for res
in socket
.getaddrinfo(self
.host
, self
.port
, socket
.AF_UNSPEC
, socket
.SOCK_STREAM
):
365 af
, socktype
, proto
, canonname
, sa
= res
367 s
= socket
.socket(af
, socktype
, proto
)
368 except socket
.error
, msg
:
372 except socket
.error
, msg
:
377 raise socket
.error(msg
)
382 def read(self
, size
):
384 Read at most 'size' bytes from remote."""
386 return self
.sock
.recv(size
)
389 def send(self
, data
):
391 Send 'data' to remote."""
393 self
.sock
.sendall(data
)
398 Close I/O established in "open"."""
405 Return socket instance used to connect to IMAP4 server."""
414 def recent(self
, **kw
):
415 """(typ, [data]) = recent()
416 Return most recent 'RECENT' responses if any exist,
417 else prompt server for an update using the 'NOOP' command.
418 'data' is None if no new messages,
419 else list of RECENT responses, most recent last."""
422 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
424 return self
._deliver
_dat
(typ
, dat
, kw
)
425 kw
['untagged_response'] = name
426 return self
.noop(**kw
) # Prod server for response
429 def response(self
, code
, **kw
):
430 """(code, [data]) = response(code)
431 Return data for response 'code' if received, or None.
432 Old value for response 'code' is cleared."""
434 typ
, dat
= self
._untagged
_response
(code
, [None], code
.upper())
435 return self
._deliver
_dat
(typ
, dat
, kw
)
443 def append(self
, mailbox
, flags
, date_time
, message
, **kw
):
444 """(typ, [data]) = append(mailbox, flags, date_time, message)
445 Append message to named mailbox.
446 All args except `message' can be None."""
452 if (flags
[0],flags
[-1]) != ('(',')'):
453 flags
= '(%s)' % flags
457 date_time
= Time2Internaldate(date_time
)
460 self
.literal
= self
.mapCRLF_cre
.sub(CRLF
, message
)
462 return self
._simple
_command
(name
, mailbox
, flags
, date_time
, **kw
)
464 self
.state_change_pending
.release()
467 def authenticate(self
, mechanism
, authobject
, **kw
):
468 """(typ, [data]) = authenticate(mechanism, authobject)
469 Authenticate command - requires response processing.
471 'mechanism' specifies which authentication mechanism is to
472 be used - it must appear in <instance>.capabilities in the
473 form AUTH=<mechanism>.
475 'authobject' must be a callable object:
477 data = authobject(response)
479 It will be called to process server continuation responses.
480 It should return data that will be encoded and sent to server.
481 It should return None if the client abort response '*' should
484 self
.literal
= _Authenticator(authobject
).process
486 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mechanism
.upper())
488 self
._deliver
_exc
(self
.error
, dat
[-1])
490 if __debug__
: self
._log
(1, 'state => AUTH')
492 self
.state_change_pending
.release()
493 return self
._deliver
_dat
(typ
, dat
, kw
)
496 def capability(self
, **kw
):
497 """(typ, [data]) = capability()
498 Fetch capabilities list from server."""
501 kw
['untagged_response'] = name
502 return self
._simple
_command
(name
, **kw
)
505 def check(self
, **kw
):
506 """(typ, [data]) = check()
507 Checkpoint mailbox on server."""
509 return self
._simple
_command
('CHECK', **kw
)
512 def close(self
, **kw
):
513 """(typ, [data]) = close()
514 Close currently selected mailbox.
516 Deleted messages are removed from writable mailbox.
517 This is the recommended command before 'LOGOUT'."""
519 if self
.state
!= 'SELECTED':
520 raise self
.error('No mailbox selected.')
522 typ
, dat
= self
._simple
_command
('CLOSE')
525 if __debug__
: self
._log
(1, 'state => AUTH')
526 self
.state_change_pending
.release()
527 return self
._deliver
_dat
(typ
, dat
, kw
)
530 def copy(self
, message_set
, new_mailbox
, **kw
):
531 """(typ, [data]) = copy(message_set, new_mailbox)
532 Copy 'message_set' messages onto end of 'new_mailbox'."""
534 return self
._simple
_command
('COPY', message_set
, new_mailbox
, **kw
)
537 def create(self
, mailbox
, **kw
):
538 """(typ, [data]) = create(mailbox)
539 Create new mailbox."""
541 return self
._simple
_command
('CREATE', mailbox
, **kw
)
544 def delete(self
, mailbox
, **kw
):
545 """(typ, [data]) = delete(mailbox)
546 Delete old mailbox."""
548 return self
._simple
_command
('DELETE', mailbox
, **kw
)
551 def deleteacl(self
, mailbox
, who
, **kw
):
552 """(typ, [data]) = deleteacl(mailbox, who)
553 Delete the ACLs (remove any rights) set for who on mailbox."""
555 return self
._simple
_command
('DELETEACL', mailbox
, who
, **kw
)
558 def examine(self
, mailbox
='INBOX', **kw
):
559 """(typ, [data]) = examine(mailbox='INBOX', readonly=False)
560 Select a mailbox for READ-ONLY access. (Flushes all untagged responses.)
561 'data' is count of messages in mailbox ('EXISTS' response).
562 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
563 other responses should be obtained via "response('FLAGS')" etc."""
565 return self
.select(mailbox
=mailbox
, readonly
=True, **kw
)
568 def expunge(self
, **kw
):
569 """(typ, [data]) = expunge()
570 Permanently remove deleted items from selected mailbox.
571 Generates 'EXPUNGE' response for each deleted message.
572 'data' is list of 'EXPUNGE'd message numbers in order received."""
575 kw
['untagged_response'] = name
576 return self
._simple
_command
(name
, **kw
)
579 def fetch(self
, message_set
, message_parts
, **kw
):
580 """(typ, [data, ...]) = fetch(message_set, message_parts)
581 Fetch (parts of) messages.
582 'message_parts' should be a string of selected parts
583 enclosed in parentheses, eg: "(UID BODY[TEXT])".
584 'data' are tuples of message part envelope and data,
585 followed by a string containing the trailer."""
588 kw
['untagged_response'] = name
589 return self
._simple
_command
(name
, message_set
, message_parts
, **kw
)
592 def getacl(self
, mailbox
, **kw
):
593 """(typ, [data]) = getacl(mailbox)
594 Get the ACLs for a mailbox."""
596 kw
['untagged_response'] = 'ACL'
597 return self
._simple
_command
('GETACL', mailbox
, **kw
)
600 def getannotation(self
, mailbox
, entry
, attribute
, **kw
):
601 """(typ, [data]) = getannotation(mailbox, entry, attribute)
602 Retrieve ANNOTATIONs."""
604 kw
['untagged_response'] = 'ANNOTATION'
605 return self
._simple
_command
('GETANNOTATION', mailbox
, entry
, attribute
, **kw
)
608 def getquota(self
, root
, **kw
):
609 """(typ, [data]) = getquota(root)
610 Get the quota root's resource usage and limits.
611 (Part of the IMAP4 QUOTA extension defined in rfc2087.)"""
613 kw
['untagged_response'] = 'QUOTA'
614 return self
._simple
_command
('GETQUOTA', root
, **kw
)
617 def getquotaroot(self
, mailbox
, **kw
):
618 # Hmmm, this is non-std! Left for backwards-compatibility, sigh.
619 # NB: usage should have been defined as:
620 # (typ, [QUOTAROOT responses...]) = getquotaroot(mailbox)
621 # (typ, [QUOTA responses...]) = response('QUOTA')
622 """(typ, [[QUOTAROOT responses...], [QUOTA responses...]]) = getquotaroot(mailbox)
623 Get the list of quota roots for the named mailbox."""
625 typ
, dat
= self
._simple
_command
('GETQUOTAROOT', mailbox
)
626 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
627 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
628 return self
._deliver
_dat
(typ
, [quotaroot
, quota
], kw
)
631 def idle(self
, timeout
=None, **kw
):
632 """"(typ, [data]) = idle(timeout=None)
633 Put server into IDLE mode until server notifies some change,
634 or 'timeout' (secs) occurs (default: 29 minutes),
635 or another IMAP4 command is scheduled."""
638 self
.literal
= _IdleCont(self
, timeout
).process
640 return self
._simple
_command
(name
, **kw
)
642 self
.state_change_pending
.release()
645 def list(self
, directory
='""', pattern
='*', **kw
):
646 """(typ, [data]) = list(directory='""', pattern='*')
647 List mailbox names in directory matching pattern.
648 'data' is list of LIST responses.
651 % matches all except separator ( so LIST "" "%" returns names at root)
652 * matches all (so LIST "" "*" returns whole directory tree from root)"""
655 kw
['untagged_response'] = name
656 return self
._simple
_command
(name
, directory
, pattern
, **kw
)
659 def login(self
, user
, password
, **kw
):
660 """(typ, [data]) = login(user, password)
661 Identify client using plaintext password.
662 NB: 'password' will be quoted."""
665 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
667 self
._deliver
_exc
(self
.error
, dat
[-1], kw
)
669 if __debug__
: self
._log
(1, 'state => AUTH')
671 self
.state_change_pending
.release()
672 return self
._deliver
_dat
(typ
, dat
, kw
)
675 def login_cram_md5(self
, user
, password
, **kw
):
676 """(typ, [data]) = login_cram_md5(user, password)
677 Force use of CRAM-MD5 authentication."""
679 self
.user
, self
.password
= user
, password
680 return self
.authenticate('CRAM-MD5', self
._CRAM
_MD
5_AUTH
, **kw
)
683 def _CRAM_MD5_AUTH(self
, challenge
):
684 """Authobject to use with CRAM-MD5 authentication."""
686 return self
.user
+ " " + hmac
.HMAC(self
.password
, challenge
).hexdigest()
689 def logout(self
, **kw
):
690 """(typ, [data]) = logout()
691 Shutdown connection to server.
692 Returns server 'BYE' response."""
695 if __debug__
: self
._log
(1, 'state => LOGOUT')
698 typ
, dat
= self
._simple
_command
('LOGOUT')
700 typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
701 if __debug__
: self
._log
(1, dat
)
703 self
._close
_threads
()
705 self
.state_change_pending
.release()
707 if __debug__
: self
._log
(1, 'connection closed')
709 bye
= self
.untagged_responses
.get('BYE')
711 typ
, dat
= 'BYE', bye
712 return self
._deliver
_dat
(typ
, dat
, kw
)
715 def lsub(self
, directory
='""', pattern
='*', **kw
):
716 """(typ, [data, ...]) = lsub(directory='""', pattern='*')
717 List 'subscribed' mailbox names in directory matching pattern.
718 'data' are tuples of message part envelope and data."""
721 kw
['untagged_response'] = name
722 return self
._simple
_command
(name
, directory
, pattern
, **kw
)
725 def myrights(self
, mailbox
):
726 """(typ, [data]) = myrights(mailbox)
727 Show my ACLs for a mailbox (i.e. the rights that I have on mailbox)."""
730 kw
['untagged_response'] = name
731 return self
._simple
_command
(name
, mailbox
, **kw
)
734 def namespace(self
, **kw
):
735 """(typ, [data, ...]) = namespace()
736 Returns IMAP namespaces ala rfc2342."""
739 kw
['untagged_response'] = name
740 return self
._simple
_command
(name
, **kw
)
743 def noop(self
, **kw
):
744 """(typ, [data]) = noop()
745 Send NOOP command."""
747 if __debug__
: self
._dump
_ur
(3)
748 return self
._simple
_command
('NOOP', **kw
)
751 def partial(self
, message_num
, message_part
, start
, length
, **kw
):
752 """(typ, [data, ...]) = partial(message_num, message_part, start, length)
753 Fetch truncated part of a message.
754 'data' is tuple of message part envelope and data.
758 kw
['untagged_response'] = 'FETCH'
759 return self
._simple
_command
(name
, message_num
, message_part
, start
, length
, **kw
)
762 def proxyauth(self
, user
, **kw
):
763 """(typ, [data]) = proxyauth(user)
764 Assume authentication as 'user'.
765 (Allows an authorised administrator to proxy into any user's mailbox.)"""
768 return self
._simple
_command
('PROXYAUTH', user
, **kw
)
770 self
.state_change_pending
.release()
773 def rename(self
, oldmailbox
, newmailbox
, **kw
):
774 """(typ, [data]) = rename(oldmailbox, newmailbox)
775 Rename old mailbox name to new."""
777 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
, **kw
)
780 def search(self
, charset
, *criteria
, **kw
):
781 """(typ, [data]) = search(charset, criterion, ...)
782 Search mailbox for matching messages.
783 'data' is space separated list of matching message numbers."""
786 kw
['untagged_response'] = name
788 return self
._simple
_command
(name
, 'CHARSET', charset
, *criteria
, **kw
)
789 return self
._simple
_command
(name
, *criteria
, **kw
)
792 def select(self
, mailbox
='INBOX', readonly
=False, **kw
):
793 """(typ, [data]) = select(mailbox='INBOX', readonly=False)
794 Select a mailbox. (Flushes all untagged responses.)
795 'data' is count of messages in mailbox ('EXISTS' response).
796 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
797 other responses should be obtained via "response('FLAGS')" etc."""
799 self
.commands_lock
.acquire()
800 self
.untagged_responses
= {} # Flush old responses.
801 self
.commands_lock
.release()
803 self
.is_readonly
= readonly
and True or False
809 rqb
= self
._command
(name
, mailbox
)
810 typ
, dat
= rqb
.get_response('command: %s => %%s' % rqb
.name
)
812 if self
.state
== SELECTED
:
814 if __debug__
: self
._log
(1, 'state => AUTH')
816 self
._deliver
_exc
(self
.error
, '%s command error: %s %s' % (name
, typ
, dat
), kw
)
817 return self
._deliver
_dat
(typ
, dat
, kw
)
818 self
.state
= SELECTED
819 if __debug__
: self
._log
(1, 'state => SELECTED')
821 self
.state_change_pending
.release()
822 if 'READ-ONLY' in self
.untagged_responses
and not readonly
:
823 if __debug__
: self
._dump
_ur
(1)
824 self
._deliver
_exc
(self
.readonly
, '%s is not writable' % mailbox
, kw
)
825 return self
._deliver
_dat
(typ
, self
.untagged_responses
.get('EXISTS', [None]), kw
)
828 def setacl(self
, mailbox
, who
, what
, **kw
):
829 """(typ, [data]) = setacl(mailbox, who, what)
830 Set a mailbox acl."""
833 return self
._simple
_command
('SETACL', mailbox
, who
, what
, **kw
)
835 self
.state_change_pending
.release()
838 def setannotation(self
, *args
, **kw
):
839 """(typ, [data]) = setannotation(mailbox[, entry, attribute]+)
842 kw
['untagged_response'] = 'ANNOTATION'
843 return self
._simple
_command
('SETANNOTATION', *args
, **kw
)
846 def setquota(self
, root
, limits
, **kw
):
847 """(typ, [data]) = setquota(root, limits)
848 Set the quota root's resource limits."""
850 kw
['untagged_response'] = 'QUOTA'
852 return self
._simple
_command
('SETQUOTA', root
, limits
, **kw
)
854 self
.state_change_pending
.release()
857 def sort(self
, sort_criteria
, charset
, *search_criteria
, **kw
):
858 """(typ, [data]) = sort(sort_criteria, charset, search_criteria, ...)
859 IMAP4rev1 extension SORT command."""
862 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
863 sort_criteria
= '(%s)' % sort_criteria
864 kw
['untagged_response'] = name
865 return self
._simple
_command
(name
, sort_criteria
, charset
, *search_criteria
, **kw
)
868 def status(self
, mailbox
, names
, **kw
):
869 """(typ, [data]) = status(mailbox, names)
870 Request named status conditions for mailbox."""
873 kw
['untagged_response'] = name
874 return self
._simple
_command
(name
, mailbox
, names
, **kw
)
877 def store(self
, message_set
, command
, flags
, **kw
):
878 """(typ, [data]) = store(message_set, command, flags)
879 Alters flag dispositions for messages in mailbox."""
881 if (flags
[0],flags
[-1]) != ('(',')'):
882 flags
= '(%s)' % flags
# Avoid quoting the flags
883 kw
['untagged_response'] = 'FETCH'
884 return self
._simple
_command
('STORE', message_set
, command
, flags
, **kw
)
887 def subscribe(self
, mailbox
, **kw
):
888 """(typ, [data]) = subscribe(mailbox)
889 Subscribe to new mailbox."""
892 return self
._simple
_command
('SUBSCRIBE', mailbox
, **kw
)
894 self
.state_change_pending
.release()
897 def thread(self
, threading_algorithm
, charset
, *search_criteria
, **kw
):
898 """(type, [data]) = thread(threading_alogrithm, charset, search_criteria, ...)
899 IMAPrev1 extension THREAD command."""
902 kw
['untagged_response'] = name
903 return self
._simple
_command
(name
, threading_algorithm
, charset
, *search_criteria
, **kw
)
906 def uid(self
, command
, *args
, **kw
):
907 """(typ, [data]) = uid(command, arg, ...)
908 Execute "command arg ..." with messages identified by UID,
909 rather than message number.
910 Assumes 'command' is legal in current state.
911 Returns response appropriate to 'command'."""
913 command
= command
.upper()
914 if command
in UID_direct
:
918 kw
['untagged_response'] = resp
919 return self
._simple
_command
('UID', command
, *args
, **kw
)
922 def unsubscribe(self
, mailbox
, **kw
):
923 """(typ, [data]) = unsubscribe(mailbox)
924 Unsubscribe from old mailbox."""
927 return self
._simple
_command
('UNSUBSCRIBE', mailbox
, **kw
)
929 self
.state_change_pending
.release()
932 def xatom(self
, name
, *args
, **kw
):
933 """(typ, [data]) = xatom(name, arg, ...)
934 Allow simple extension commands notified by server in CAPABILITY response.
935 Assumes extension command 'name' is legal in current state.
936 Returns response appropriate to extension command 'name'."""
939 if not name
in Commands
:
940 Commands
[name
] = ((self
.state
,), False)
942 return self
._simple
_command
(name
, *args
, **kw
)
944 if self
.state_change_pending
.locked():
945 self
.state_change_pending
.release()
952 def _append_untagged(self
, typ
, dat
):
954 if dat
is None: dat
= ''
956 self
.commands_lock
.acquire()
957 ur
= self
.untagged_responses
.setdefault(typ
, [])
959 self
.commands_lock
.release()
961 if __debug__
: self
._log
(5, 'untagged_responses[%s] %s += ["%s"]' % (typ
, len(ur
)-1, dat
))
964 def _check_bye(self
):
966 bye
= self
.untagged_responses
.get('BYE')
968 raise self
.abort(bye
[-1])
971 def _checkquote(self
, arg
):
973 # Must quote command args if non-alphanumeric chars present,
974 # and not already quoted.
976 if not isinstance(arg
, basestring
):
978 if len(arg
) >= 2 and (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
980 if arg
and self
.mustquote_cre
.search(arg
) is None:
982 return self
._quote
(arg
)
985 def _command(self
, name
, *args
, **kw
):
987 if Commands
[name
][CMD_VAL_ASYNC
]:
992 if __debug__
: self
._log
(1, '[%s] %s %s' % (cmdtyp
, name
, args
))
994 self
.state_change_pending
.acquire()
998 if cmdtyp
== 'async':
999 self
.state_change_pending
.release()
1001 # Need to wait for all async commands to complete
1003 self
.commands_lock
.acquire()
1004 if self
.tagged_commands
:
1005 self
.state_change_free
.clear()
1009 self
.commands_lock
.release()
1011 if __debug__
: self
._log
(4, 'sync command %s waiting for empty commands Q' % name
)
1012 self
.state_change_free
.wait()
1013 if __debug__
: self
._log
(4, 'sync command %s proceeding' % name
)
1015 if self
.state
not in Commands
[name
][CMD_VAL_STATES
]:
1017 raise self
.error('command %s illegal in state %s'
1018 % (name
, self
.state
))
1022 self
.commands_lock
.acquire()
1023 for typ
in ('OK', 'NO', 'BAD'):
1024 if typ
in self
.untagged_responses
:
1025 del self
.untagged_responses
[typ
]
1026 self
.commands_lock
.release()
1028 if 'READ-ONLY' in self
.untagged_responses \
1029 and not self
.is_readonly
:
1031 raise self
.readonly('mailbox status changed to READ-ONLY')
1034 raise self
.abort('connection closed')
1036 rqb
= self
._request
_push
(name
=name
, **kw
)
1038 data
= '%s %s' % (rqb
.tag
, name
)
1040 if arg
is None: continue
1041 data
= '%s %s' % (data
, self
._checkquote
(arg
))
1043 literal
= self
.literal
1044 if literal
is not None:
1046 if isinstance(literal
, str):
1048 data
= '%s {%s}' % (data
, len(literal
))
1052 rqb
.data
= '%s%s' % (data
, CRLF
)
1058 crqb
= self
._request
_push
(tag
='continuation')
1061 # Wait for continuation response
1063 ok
, data
= crqb
.get_response('command: %s => %%s' % name
)
1064 if __debug__
: self
._log
(3, 'continuation => %s, %s' % (ok
, data
))
1073 if literator
is not None:
1074 literal
= literator(data
, rqb
)
1079 if __debug__
: self
._log
(4, 'write literal size %s' % len(literal
))
1080 crqb
.data
= '%s%s' % (literal
, CRLF
)
1083 if literator
is None:
1086 self
.commands_lock
.acquire()
1087 self
.tagged_commands
['continuation'] = crqb
1088 self
.commands_lock
.release()
1093 def _command_complete(self
, rqb
, kw
):
1095 # Called for non-callback commands
1097 typ
, dat
= rqb
.get_response('command: %s => %%s' % rqb
.name
)
1100 if __debug__
: self
._print
_log
()
1101 raise self
.error('%s command error: %s %s' % (rqb
.name
, typ
, dat
))
1102 if 'untagged_response' in kw
:
1103 return self
._untagged
_response
(typ
, dat
, kw
['untagged_response'])
1107 def _command_completer(self
, (response
, cb_arg
, error
)):
1109 # Called for callback commands
1111 rqb
.callback
= kw
['callback']
1112 rqb
.callback_arg
= kw
.get('cb_arg')
1113 if error
is not None:
1114 if __debug__
: self
._print
_log
()
1118 bye
= self
.untagged_responses
.get('BYE')
1120 rqb
.abort(self
.abort
, bye
[-1])
1124 if __debug__
: self
._print
_log
()
1125 rqb
.abort(self
.error
, '%s command error: %s %s' % (rqb
.name
, typ
, dat
))
1127 if 'untagged_response' in kw
:
1128 rqb
.deliver(self
._untagged
_response
(typ
, dat
, kw
['untagged_response']))
1130 rqb
.deliver(response
)
1133 def _deliver_dat(self
, typ
, dat
, kw
):
1135 if 'callback' in kw
:
1136 kw
['callback'](((typ
, dat
), kw
.get('cb_arg'), None))
1140 def _deliver_exc(self
, exc
, dat
, kw
):
1142 if 'callback' in kw
:
1143 kw
['callback']((None, kw
.get('cb_arg'), (exc
, dat
)))
1147 def _end_idle(self
):
1149 irqb
= self
.idle_rqb
1150 if irqb
is not None:
1151 self
.idle_rqb
= None
1152 self
.idle_timeout
= None
1153 irqb
.data
= 'DONE%s' % CRLF
1155 if __debug__
: self
._log
(2, 'server IDLE finished')
1158 def _match(self
, cre
, s
):
1160 # Run compiled regular expression 'cre' match method on 's'.
1161 # Save result, return success.
1163 self
.mo
= cre
.match(s
)
1164 return self
.mo
is not None
1167 def _put_response(self
, resp
):
1169 if self
._expecting
_data
> 0:
1171 dlen
= min(self
._expecting
_data
, rlen
)
1172 self
._expecting
_data
-= dlen
1174 self
._accumulated
_data
.append(resp
)
1176 self
._accumulated
_data
.append(resp
[:dlen
])
1179 if self
._accumulated
_data
:
1180 typ
, dat
= self
._literal
_expected
1181 self
._append
_untagged
(typ
, (dat
, ''.join(self
._accumulated
_data
)))
1182 self
._accumulated
_data
= []
1184 # Protocol mandates all lines terminated by CRLF
1187 if 'continuation' in self
.tagged_commands
:
1188 continuation_expected
= True
1190 continuation_expected
= False
1192 if self
._literal
_expected
is not None:
1194 if self
._match
(self
.literal_cre
, dat
):
1195 self
._literal
_expected
[1] = dat
1196 self
._expecting
_data
= int(self
.mo
.group('size'))
1197 if __debug__
: self
._log
(4, 'expecting literal size %s' % self
._expecting
_data
)
1199 typ
= self
._literal
_expected
[0]
1200 self
._literal
_expected
= None
1201 self
._append
_untagged
(typ
, dat
) # Tail
1202 if __debug__
: self
._log
(4, 'literal completed')
1204 # Command completion response?
1205 if self
._match
(self
.tagre
, resp
):
1206 tag
= self
.mo
.group('tag')
1207 typ
= self
.mo
.group('type')
1208 dat
= self
.mo
.group('data')
1209 if not tag
in self
.tagged_commands
:
1210 if __debug__
: self
._log
(1, 'unexpected tagged response: %s' % resp
)
1212 self
._request
_pop
(tag
, (typ
, [dat
]))
1216 # '*' (untagged) responses?
1218 if not self
._match
(self
.untagged_response_cre
, resp
):
1219 if self
._match
(self
.untagged_status_cre
, resp
):
1220 dat2
= self
.mo
.group('data2')
1223 # Only other possibility is '+' (continuation) response...
1225 if self
._match
(self
.continuation_cre
, resp
):
1226 if not continuation_expected
:
1227 if __debug__
: self
._log
(1, "unexpected continuation response: '%s'" % resp
)
1229 self
._request
_pop
('continuation', (True, self
.mo
.group('data')))
1232 if __debug__
: self
._log
(1, "unexpected response: '%s'" % resp
)
1235 typ
= self
.mo
.group('type')
1236 dat
= self
.mo
.group('data')
1237 if dat
is None: dat
= '' # Null untagged response
1238 if dat2
: dat
= dat
+ ' ' + dat2
1240 # Is there a literal to come?
1242 if self
._match
(self
.literal_cre
, dat
):
1243 self
._expecting
_data
= int(self
.mo
.group('size'))
1244 if __debug__
: self
._log
(4, 'read literal size %s' % self
._expecting
_data
)
1245 self
._literal
_expected
= [typ
, dat
]
1248 self
._append
_untagged
(typ
, dat
)
1253 # Bracketed response information?
1255 if typ
in ('OK', 'NO', 'BAD') and self
._match
(self
.response_code_cre
, dat
):
1256 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
1258 # Command waiting for aborted continuation response?
1260 if continuation_expected
:
1261 self
._request
_pop
('continuation', (False, resp
))
1265 if typ
in ('NO', 'BAD', 'BYE'):
1267 self
.Terminate
= True
1268 if __debug__
: self
._log
(1, '%s response: %s' % (typ
, dat
))
1271 def _quote(self
, arg
):
1273 return '"%s"' % arg
.replace('\\', '\\\\').replace('"', '\\"')
1276 def _request_pop(self
, name
, data
):
1278 if __debug__
: self
._log
(4, '_request_pop(%s, %s)' % (name
, data
))
1279 self
.commands_lock
.acquire()
1280 rqb
= self
.tagged_commands
.pop(name
)
1281 if not self
.tagged_commands
:
1282 self
.state_change_free
.set()
1283 self
.commands_lock
.release()
1287 def _request_push(self
, tag
=None, name
=None, **kw
):
1289 self
.commands_lock
.acquire()
1290 rqb
= Request(self
, name
=name
, **kw
)
1293 self
.tagged_commands
[tag
] = rqb
1294 self
.commands_lock
.release()
1295 if __debug__
: self
._log
(4, '_request_push(%s, %s, %s)' % (tag
, name
, `kw`
))
1299 def _simple_command(self
, name
, *args
, **kw
):
1301 if 'callback' in kw
:
1302 rqb
= self
._command
(name
, callback
=self
._command
_completer
, *args
)
1303 rqb
.callback_arg
= (rqb
, kw
)
1305 return self
._command
_complete
(self
._command
(name
, *args
), kw
)
1308 def _untagged_response(self
, typ
, dat
, name
):
1312 if not name
in self
.untagged_responses
:
1314 self
.commands_lock
.acquire()
1315 data
= self
.untagged_responses
.pop(name
)
1316 self
.commands_lock
.release()
1317 if __debug__
: self
._log
(5, 'pop untagged_responses[%s] => %s' % (name
, (typ
, data
)))
1325 def _close_threads(self
):
1338 threading
.currentThread().setName('hdlr')
1340 time
.sleep(0.1) # Don't start handling before main thread ready
1342 if __debug__
: self
._log
(1, 'starting')
1344 typ
, val
= self
.abort
, 'connection terminated'
1346 while not self
.Terminate
:
1348 if self
.idle_timeout
is not None:
1349 timeout
= self
.idle_timeout
- time
.time()
1353 if self
.idle_rqb
is not None:
1354 self
._log
(5, 'server IDLING, timeout=%.2f' % timeout
)
1357 line
= self
.inq
.get(True, timeout
)
1359 if self
.idle_rqb
is None:
1361 if self
.idle_timeout
> time
.time():
1363 if __debug__
: self
._log
(2, 'server IDLE timedout')
1364 line
= IDLE_TIMEOUT_RESPONSE
1369 if not isinstance(line
, str):
1374 self
._put
_response
(line
)
1376 typ
, val
= self
.error
, 'program error: %s - %s' % sys
.exc_info()[:2]
1379 self
.Terminate
= True
1381 while not self
.ouq
.empty():
1383 self
.ouq
.get_nowait().abort(typ
, val
)
1388 self
.commands_lock
.acquire()
1389 for name
in self
.tagged_commands
.keys():
1390 rqb
= self
.tagged_commands
.pop(name
)
1392 self
.state_change_free
.set()
1393 self
.commands_lock
.release()
1395 if __debug__
: self
._log
(1, 'finished')
1398 if hasattr(select_module
, "poll"):
1402 threading
.currentThread().setName('redr')
1404 if __debug__
: self
._log
(1, 'starting using poll')
1406 def poll_error(state
):
1408 select
.POLLERR
: 'Error',
1409 select
.POLLHUP
: 'Hang up',
1410 select
.POLLNVAL
: 'Invalid request: descriptor not open',
1412 return ' '.join([PollErrors
[s
] for s
in PollErrors
.keys() if (s
& state
)])
1416 poll
= select
.poll()
1418 poll
.register(self
.read_fd
, select
.POLLIN
)
1420 while not self
.Terminate
:
1421 if self
.state
== LOGOUT
:
1426 r
= poll
.poll(timeout
)
1427 if __debug__
: self
._log
(5, 'poll => %s' % `r`
)
1433 if state
& select
.POLLIN
:
1434 data
= self
.read(32768) # Drain ssl buffer if present
1437 if __debug__
: self
._log
(5, 'rcvd %s' % dlen
)
1441 stop
= data
.find('\n', start
)
1443 line_part
+= data
[start
:]
1446 line_part
, start
, line
= \
1447 '', stop
, line_part
+ data
[start
:stop
]
1448 if __debug__
: self
._log
(4, '< %s' % line
)
1451 if state
& ~
(select
.POLLIN
):
1452 raise IOError(poll_error(state
))
1454 reason
= 'socket error: %s - %s' % sys
.exc_info()[:2]
1456 if not self
.Terminate
:
1458 if self
.debug
: self
.debug
+= 4 # Output all
1459 self
._log
(1, reason
)
1460 self
.inq
.put((self
.abort
, reason
))
1463 poll
.unregister(self
.read_fd
)
1465 if __debug__
: self
._log
(1, 'finished')
1469 # No "poll" - use select()
1473 threading
.currentThread().setName('redr')
1475 if __debug__
: self
._log
(1, 'starting using select')
1479 while not self
.Terminate
:
1480 if self
.state
== LOGOUT
:
1485 r
,w
,e
= select
.select([self
.read_fd
], [], [], timeout
)
1486 if __debug__
: self
._log
(5, 'select => %s, %s, %s' % (r
,w
,e
))
1490 data
= self
.read(32768) # Drain ssl buffer if present
1493 if __debug__
: self
._log
(5, 'rcvd %s' % dlen
)
1497 stop
= data
.find('\n', start
)
1499 line_part
+= data
[start
:]
1502 line_part
, start
, line
= \
1503 '', stop
, line_part
+ data
[start
:stop
]
1504 if __debug__
: self
._log
(4, '< %s' % line
)
1507 reason
= 'socket error: %s - %s' % sys
.exc_info()[:2]
1509 if not self
.Terminate
:
1511 if self
.debug
: self
.debug
+= 4 # Output all
1512 self
._log
(1, reason
)
1513 self
.inq
.put((self
.abort
, reason
))
1516 if __debug__
: self
._log
(1, 'finished')
1521 threading
.currentThread().setName('wrtr')
1523 if __debug__
: self
._log
(1, 'starting')
1525 reason
= 'Terminated'
1527 while not self
.Terminate
:
1528 rqb
= self
.ouq
.get()
1530 break # Outq flushed
1534 if __debug__
: self
._log
(4, '> %s' % rqb
.data
)
1536 reason
= 'socket error: %s - %s' % sys
.exc_info()[:2]
1538 if not self
.Terminate
:
1540 if self
.debug
: self
.debug
+= 4 # Output all
1541 self
._log
(1, reason
)
1542 rqb
.abort(self
.abort
, reason
)
1545 self
.inq
.put((self
.abort
, reason
))
1547 if __debug__
: self
._log
(1, 'finished')
1556 def _init_debug(self
, debug
=None, debug_file
=None):
1557 self
.debug
= debug
is not None and debug
or Debug
is not None and Debug
or 0
1558 self
.debug_file
= debug_file
is not None and debug_file
or sys
.stderr
1560 self
.debug_lock
= threading
.Lock()
1561 self
._cmd
_log
_len
= 20
1562 self
._cmd
_log
_idx
= 0
1563 self
._cmd
_log
= {} # Last `_cmd_log_len' interactions
1565 self
._mesg
('imaplib2 version %s' % __version__
)
1566 self
._mesg
('imaplib2 debug level %s' % self
.debug
)
1569 def _dump_ur(self
, lvl
):
1570 if lvl
> self
.debug
:
1573 l
= self
.untagged_responses
.items()
1578 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1579 self
.debug_lock
.acquire()
1580 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1581 self
.debug_lock
.release()
1584 def _log(self
, lvl
, line
):
1585 if lvl
> self
.debug
:
1588 if line
[-2:] == CRLF
:
1589 line
= line
[:-2] + '\\r\\n'
1591 tn
= threading
.currentThread().getName()
1594 self
.debug_lock
.acquire()
1595 self
._mesg
(line
, tn
)
1596 self
.debug_lock
.release()
1599 # Keep log of last `_cmd_log_len' interactions for debugging.
1600 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, tn
, time
.time())
1601 self
._cmd
_log
_idx
+= 1
1602 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1603 self
._cmd
_log
_idx
= 0
1606 def _mesg(self
, s
, tn
=None, secs
=None):
1610 tn
= threading
.currentThread().getName()
1611 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1612 self
.debug_file
.write(' %s.%02d %s %s\n' % (tm
, (secs
*100)%100, tn
, s
))
1613 self
.debug_file
.flush()
1616 def _print_log(self
):
1617 self
.debug_lock
.acquire()
1618 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1619 if n
: self
._mesg
('last %d imaplib2 reports:' % n
)
1622 self
._mesg
(*self
._cmd
_log
[i
])
1626 if i
>= self
._cmd
_log
_len
:
1629 self
.debug_lock
.release()
1633 class IMAP4_SSL(IMAP4
):
1635 """IMAP4 client class over SSL connection
1638 IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, debug=None, debug_file=None)
1640 host - host's name (default: localhost);
1641 port - port number (default: standard IMAP4 SSL port);
1642 keyfile - PEM formatted file that contains your private key (default: None);
1643 certfile - PEM formatted certificate chain file (default: None);
1644 debug - debug level (default: 0 - no debug);
1645 debug_file - debug stream (default: sys.stderr).
1647 For more documentation see the docstring of the parent class IMAP4.
1651 def __init__(self
, host
=None, port
=None, keyfile
=None, certfile
=None, debug
=None, debug_file
=None):
1652 self
.keyfile
= keyfile
1653 self
.certfile
= certfile
1654 IMAP4
.__init__(self
, host
, port
, debug
, debug_file
)
1657 def open(self
, host
=None, port
=None):
1658 """open(host=None, port=None)
1659 Setup secure connection to remote server on "host:port"
1660 (default: localhost:standard IMAP4 SSL port).
1661 This connection will be used by the routines:
1662 read, send, shutdown, socket, ssl."""
1664 self
.host
= host
is not None and host
or ''
1665 self
.port
= port
is not None and port
or IMAP4_SSL_PORT
1666 self
.sock
= self
.open_socket()
1667 self
.sslobj
= socket
.ssl(self
.sock
, self
.keyfile
, self
.certfile
)
1669 self
.read_fd
= self
.sock
.fileno()
1672 def read(self
, size
):
1673 """data = read(size)
1674 Read at most 'size' bytes from remote."""
1676 return self
.sslobj
.read(size
)
1679 def send(self
, data
):
1681 Send 'data' to remote."""
1683 # NB: socket.ssl needs a "sendall" method to match socket objects.
1686 sent
= self
.sslobj
.write(data
)
1690 bytes
= bytes
- sent
1695 Return socket.ssl instance used to communicate with the IMAP4 server."""
1701 class IMAP4_stream(IMAP4
):
1703 """IMAP4 client class over a stream
1706 IMAP4_stream(command, debug=None, debug_file=None)
1708 command - string that can be passed to os.popen2();
1709 debug - debug level (default: 0 - no debug);
1710 debug_file - debug stream (default: sys.stderr).
1712 For more documentation see the docstring of the parent class IMAP4.
1716 def __init__(self
, command
, debug
=None, debug_file
=None):
1717 self
.command
= command
1721 self
.writefile
, self
.readfile
= None, None
1723 IMAP4
.__init__(self
, debug
=debug
, debug_file
=debug_file
)
1726 def open(self
, host
=None, port
=None):
1727 """open(host=None, port=None)
1728 Setup a stream connection via 'self.command'.
1729 This connection will be used by the routines:
1730 read, send, shutdown, socket."""
1732 self
.writefile
, self
.readfile
= os
.popen2(self
.command
)
1733 self
.read_fd
= self
.readfile
.fileno()
1736 def read(self
, size
):
1737 """Read 'size' bytes from remote."""
1739 return os
.read(self
.read_fd
, size
)
1742 def send(self
, data
):
1743 """Send data to remote."""
1745 self
.writefile
.write(data
)
1746 self
.writefile
.flush()
1750 """Close I/O established in "open"."""
1752 self
.readfile
.close()
1753 self
.writefile
.close()
1757 class _Authenticator(object):
1759 """Private class to provide en/de-coding
1760 for base64 authentication conversation."""
1762 def __init__(self
, mechinst
):
1763 self
.mech
= mechinst
# Callable object to provide/process data
1765 def process(self
, data
, rqb
):
1766 ret
= self
.mech(self
.decode(data
))
1768 return '*' # Abort conversation
1769 return self
.encode(ret
)
1771 def encode(self
, inp
):
1773 # Invoke binascii.b2a_base64 iteratively with
1774 # short even length buffers, strip the trailing
1775 # line feed from the result and append. "Even"
1776 # means a number that factors to both 6 and 8,
1777 # so when it gets to the end of the 8-bit input
1778 # there's no partial 6-bit output.
1788 e
= binascii
.b2a_base64(t
)
1793 def decode(self
, inp
):
1796 return binascii
.a2b_base64(inp
)
1801 class _IdleCont(object):
1803 """When process is called, server is in IDLE state
1804 and will send asynchronous changes."""
1806 def __init__(self
, parent
, timeout
):
1807 self
.parent
= parent
1808 self
.timeout
= timeout
is not None and timeout
or IDLE_TIMEOUT
1809 self
.parent
.idle_timeout
= self
.timeout
+ time
.time()
1811 def process(self
, data
, rqb
):
1812 self
.parent
.idle_rqb
= rqb
1813 self
.parent
.idle_timeout
= self
.timeout
+ time
.time()
1814 if __debug__
: self
.parent
._log
(2, 'server IDLE started, timeout in %.2f secs' % self
.timeout
)
1819 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1820 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1822 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
1823 r
'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
1824 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
1825 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
1829 def Internaldate2Time(resp
):
1831 """time_tuple = Internaldate2Time(resp)
1832 Convert IMAP4 INTERNALDATE to UT."""
1834 mo
= InternalDate
.match(resp
)
1838 mon
= Mon2num
[mo
.group('mon')]
1839 zonen
= mo
.group('zonen')
1841 day
= int(mo
.group('day'))
1842 year
= int(mo
.group('year'))
1843 hour
= int(mo
.group('hour'))
1844 min = int(mo
.group('min'))
1845 sec
= int(mo
.group('sec'))
1846 zoneh
= int(mo
.group('zoneh'))
1847 zonem
= int(mo
.group('zonem'))
1849 # INTERNALDATE timezone must be subtracted to get UT
1851 zone
= (zoneh
*60 + zonem
)*60
1855 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1857 utc
= time
.mktime(tt
)
1859 # Following is necessary because the time module has no 'mkgmtime'.
1860 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1862 lt
= time
.localtime(utc
)
1863 if time
.daylight
and lt
[-1]:
1864 zone
= zone
+ time
.altzone
1866 zone
= zone
+ time
.timezone
1868 return time
.localtime(utc
- zone
)
1870 Internaldate2tuple
= Internaldate2Time
# (Backward compatible)
1874 def Time2Internaldate(date_time
):
1876 """'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time)
1877 Convert 'date_time' to IMAP4 INTERNALDATE representation."""
1879 if isinstance(date_time
, (int, float)):
1880 tt
= time
.localtime(date_time
)
1881 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1883 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1884 return date_time
# Assume in correct format
1886 raise ValueError("date_time not of a known type")
1888 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1891 if time
.daylight
and tt
[-1]:
1892 zone
= -time
.altzone
1894 zone
= -time
.timezone
1895 return '"' + dt
+ " %+03d%02d" % divmod(zone
//60, 60) + '"'
1899 FLAGS_cre
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
1901 def ParseFlags(resp
):
1903 """('flag', ...) = ParseFlags(line)
1904 Convert IMAP4 flags response to python tuple."""
1906 mo
= FLAGS_cre
.match(resp
)
1910 return tuple(mo
.group('flags').split())
1914 if __name__
== '__main__':
1916 # To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]',
1917 # or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1918 # or as 'python imaplib2.py -l "keyfile[:certfile]" [IMAP4_SSL_server_hostname]'
1920 import getopt
, getpass
1923 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:l:s:p:')
1924 except getopt
.error
, val
:
1925 optlist
, args
= (), ()
1927 debug
, port
, stream_command
, keyfile
, certfile
= (None,)*5
1928 for opt
,val
in optlist
:
1933 keyfile
,certfile
= val
.split(':')
1935 keyfile
,certfile
= val
,val
1939 stream_command
= val
1940 if not args
: args
= (stream_command
,)
1942 if not args
: args
= ('',)
1943 if not port
: port
= (keyfile
is not None) and IMAP4_SSL_PORT
or IMAP4_PORT
1947 USER
= getpass
.getuser()
1949 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \
1950 % {'user':USER
, 'lf':'\n', 'data':open(__file__
).read()}
1952 ('list', ('""', '%')),
1953 ('create', ('/tmp/imaplib2_test.0',)),
1954 ('rename', ('/tmp/imaplib2_test.0', '/tmp/imaplib2_test.1')),
1955 ('CREATE', ('/tmp/imaplib2_test.2',)),
1956 ('append', ('/tmp/imaplib2_test.2', None, None, test_mesg
)),
1957 ('list', ('/tmp', 'imaplib2_test*')),
1958 ('select', ('/tmp/imaplib2_test.2',)),
1959 ('search', (None, 'SUBJECT', 'IMAP4 test')),
1960 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1961 ('store', ('1', 'FLAGS', '(\Deleted)')),
1970 ('response',('UIDVALIDITY',)),
1971 ('response', ('EXISTS',)),
1972 ('append', (None, None, None, test_mesg
)),
1973 ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')),
1974 ('uid', ('SEARCH', 'ALL')),
1975 ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')),
1981 def responder((response
, cb_arg
, error
)):
1984 if error
is not None:
1986 M
._mesg
('[cb] ERROR %s %.100s => %s' % (cmd
, args
, error
))
1989 M
._mesg
('[cb] %s %.100s => %s %.100s' % (cmd
, args
, typ
, dat
))
1991 AsyncError
= (Exception, dat
[0])
1993 def run(cmd
, args
, cb
=None):
1996 typ
, val
= AsyncError
1998 M
._mesg
('%s %.100s' % (cmd
, args
))
2001 typ
, dat
= getattr(M
, cmd
)(callback
=responder
, cb_arg
=(cmd
, args
), *args
)
2003 M
._mesg
('%s %.100s => %s %.100s' % (cmd
, args
, typ
, dat
))
2005 typ
, dat
= getattr(M
, cmd
)(*args
)
2006 M
._mesg
('%s %.100s => %s %.100s' % (cmd
, args
, typ
, dat
))
2012 raise Exception(dat
[0])
2016 threading
.currentThread().setName('main')
2018 if keyfile
is not None:
2019 if not keyfile
: keyfile
= None
2020 if not certfile
: certfile
= None
2021 M
= IMAP4_SSL(host
=host
, port
=port
, keyfile
=keyfile
, certfile
=certfile
, debug
=debug
)
2022 elif stream_command
:
2023 M
= IMAP4_stream(stream_command
, debug
=debug
)
2025 M
= IMAP4(host
=host
, port
=port
, debug
=debug
)
2026 if M
.state
!= 'AUTH': # Login needed
2027 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
2028 test_seq1
.insert(0, ('login', (USER
, PASSWD
)))
2029 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
2030 M
._mesg
('CAPABILITIES = %r' % (M
.capabilities
,))
2032 for cmd
,args
in test_seq1
:
2033 run(cmd
, args
, cb
=1)
2035 for ml
in run('list', ('/tmp/', 'imaplib2_test%')):
2036 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
2037 if mo
: path
= mo
.group(1)
2038 else: path
= ml
.split()[-1]
2039 run('delete', (path
,), cb
=1)
2041 for cmd
,args
in test_seq2
:
2042 if (cmd
,args
) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')):
2043 run(cmd
, args
, cb
=1)
2046 dat
= run(cmd
, args
)
2047 uid
= dat
[-1].split()
2048 if not uid
: continue
2049 run('uid', ('FETCH', uid
[-1],
2050 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'), cb
=1)
2051 run('uid', ('STORE', uid
[-1], 'FLAGS', '(\Deleted)'), cb
=1)
2052 run('expunge', (), cb
=1)
2061 print '\nAll tests OK.'
2064 print '\nTests failed.'
2068 If you would like to see debugging output,