]>
code.delx.au - offlineimap/blob - offlineimap/imaplib2.py
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 COMPRESS/DEFLATE contributed by Bron Gondwana <brong@brong.net> May 2009."""
33 __author__
= "Piers Lauder <piers@janeelix.com>"
34 __URL__
= "http://janeelix.com/piers/python/imaplib2"
36 import binascii
, os
, Queue
, random
, re
, select
, socket
, sys
, time
, threading
, zlib
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 'COMPRESS': ((AUTH
,), False),
66 'COPY': ((SELECTED
,), True),
67 'CREATE': ((AUTH
, SELECTED
), True),
68 'DELETE': ((AUTH
, SELECTED
), True),
69 'DELETEACL': ((AUTH
, SELECTED
), True),
70 'EXAMINE': ((AUTH
, SELECTED
), False),
71 'EXPUNGE': ((SELECTED
,), True),
72 'FETCH': ((SELECTED
,), True),
73 'GETACL': ((AUTH
, SELECTED
), True),
74 'GETANNOTATION':((AUTH
, SELECTED
), True),
75 'GETQUOTA': ((AUTH
, SELECTED
), True),
76 'GETQUOTAROOT': ((AUTH
, SELECTED
), True),
77 'IDLE': ((SELECTED
,), False),
78 'LIST': ((AUTH
, SELECTED
), True),
79 'LOGIN': ((NONAUTH
,), False),
80 'LOGOUT': ((NONAUTH
, AUTH
, LOGOUT
, SELECTED
), False),
81 'LSUB': ((AUTH
, SELECTED
), True),
82 'MYRIGHTS': ((AUTH
, SELECTED
), True),
83 'NAMESPACE': ((AUTH
, SELECTED
), True),
84 'NOOP': ((NONAUTH
, AUTH
, SELECTED
), True),
85 'PARTIAL': ((SELECTED
,), True),
86 'PROXYAUTH': ((AUTH
,), False),
87 'RENAME': ((AUTH
, SELECTED
), True),
88 'SEARCH': ((SELECTED
,), True),
89 'SELECT': ((AUTH
, SELECTED
), False),
90 'SETACL': ((AUTH
, SELECTED
), False),
91 'SETANNOTATION':((AUTH
, SELECTED
), True),
92 'SETQUOTA': ((AUTH
, SELECTED
), False),
93 'SORT': ((SELECTED
,), True),
94 'STATUS': ((AUTH
, SELECTED
), True),
95 'STORE': ((SELECTED
,), True),
96 'SUBSCRIBE': ((AUTH
, SELECTED
), False),
97 'THREAD': ((SELECTED
,), True),
98 'UID': ((SELECTED
,), True),
99 'UNSUBSCRIBE': ((AUTH
, SELECTED
), False),
102 UID_direct
= ('SEARCH', 'SORT', 'THREAD')
107 """string = Int2AP(num)
108 Return 'num' converted to a string using characters from the set 'A'..'P'
111 val
, a2p
= [], 'ABCDEFGHIJKLMNOP'
114 num
, mod
= divmod(num
, 16)
115 val
.insert(0, a2p
[mod
])
120 class Request(object):
122 """Private class to represent a request awaiting response."""
124 def __init__(self
, parent
, name
=None, callback
=None, cb_arg
=None):
126 self
.callback
= callback
# Function called to process result
127 self
.callback_arg
= cb_arg
# Optional arg passed to "callback"
129 self
.tag
= '%s%s' % (parent
.tagpre
, parent
.tagnum
)
132 self
.ready
= threading
.Event()
138 def abort(self
, typ
, val
):
139 self
.aborted
= (typ
, val
)
143 def get_response(self
, exc_fmt
=None):
147 if self
.aborted
is not None:
148 typ
, val
= self
.aborted
150 exc_fmt
= '%s - %%s' % typ
151 raise typ(exc_fmt
% str(val
))
156 def deliver(self
, response
):
157 if self
.callback
is not None:
158 self
.callback((response
, self
.callback_arg
, self
.aborted
))
161 self
.response
= response
169 """Threaded IMAP4 client class.
172 IMAP4(host=None, port=None, debug=None, debug_file=None)
174 host - host's name (default: localhost);
175 port - port number (default: standard IMAP4 port);
176 debug - debug level (default: 0 - no debug);
177 debug_file - debug stream (default: sys.stderr).
179 All IMAP4rev1 commands are supported by methods of the same name.
181 Each command returns a tuple: (type, [data, ...]) where 'type'
182 is usually 'OK' or 'NO', and 'data' is either the text from the
183 tagged response, or untagged results from command. Each 'data' is
184 either a string, or a tuple. If a tuple, then the first part is the
185 header of the response, and the second part contains the data (ie:
188 Errors raise the exception class <instance>.error("<reason>").
189 IMAP4 server errors raise <instance>.abort("<reason>"), which is
190 a sub-class of 'error'. Mailbox status changes from READ-WRITE to
191 READ-ONLY raise the exception class <instance>.readonly("<reason>"),
192 which is a sub-class of 'abort'.
194 "error" exceptions imply a program error.
195 "abort" exceptions imply the connection should be reset, and
196 the command re-tried.
197 "readonly" exceptions imply the command should be re-tried.
199 All commands take two optional named arguments:
200 'callback' and 'cb_arg'
201 If 'callback' is provided then the command is asynchronous, so after
202 the command is queued for transmission, the call returns immediately
203 with the tuple (None, None).
204 The result will be posted by invoking "callback" with one arg, a tuple:
205 callback((result, cb_arg, None))
206 or, if there was a problem:
207 callback((None, cb_arg, (exception class, reason)))
209 Otherwise the command is synchronous (waits for result). But note
210 that state-changing commands will both block until previous commands
211 have completed, and block subsequent commands until they have finished.
213 All (non-callback) arguments to commands are converted to strings,
214 except for AUTHENTICATE, and the last argument to APPEND which is
215 passed as an IMAP4 literal. If necessary (the string contains any
216 non-printing characters or white-space and isn't enclosed with either
217 parentheses or double quotes) each string is quoted. However, the
218 'password' argument to the LOGIN command is always quoted. If you
219 want to avoid having an argument string quoted (eg: the 'flags'
220 argument to STORE) then enclose the string in parentheses (eg:
223 There is one instance variable, 'state', that is useful for tracking
224 whether the client needs to login to the server. If it has the
225 value "AUTH" after instantiating the class, then the connection
226 is pre-authenticated (otherwise it will be "NONAUTH"). Selecting a
227 mailbox changes the state to be "SELECTED", closing a mailbox changes
228 back to "AUTH", and once the client has logged out, the state changes
229 to "LOGOUT" and no further commands may be issued.
231 Note: to use this module, you must read the RFCs pertaining to the
232 IMAP4 protocol, as the semantics of the arguments to each IMAP4
233 command are left to the invoker, not to mention the results. Also,
234 most IMAP servers implement a sub-set of the commands available here.
236 Note also that you must call logout() to shut down threads before
237 discarding an instance.
240 class error(Exception): pass # Logical errors - debug required
241 class abort(error
): pass # Service errors - close and retry
242 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
245 continuation_cre
= re
.compile(r
'\+( (?P<data>.*))?')
246 literal_cre
= re
.compile(r
'.*{(?P<size>\d+)}$')
247 mapCRLF_cre
= re
.compile(r
'\r\n|\r|\n')
248 mustquote_cre
= re
.compile(r
"[^\w!#$%&'*+,.:;<=>?^`|~-]")
249 response_code_cre
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
250 untagged_response_cre
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
251 untagged_status_cre
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
254 def __init__(self
, host
=None, port
=None, debug
=None, debug_file
=None):
256 self
.state
= NONAUTH
# IMAP4 protocol state
257 self
.literal
= None # A literal argument to a command
258 self
.tagged_commands
= {} # Tagged commands awaiting response
259 self
.untagged_responses
= {} # {typ: [data, ...], ...}
260 self
.is_readonly
= False # READ-ONLY desired state
261 self
.idle_rqb
= None # Server IDLE Request - see _IdleCont
262 self
.idle_timeout
= None # Must prod server occasionally
264 self
._expecting
_data
= 0 # Expecting message data
265 self
._accumulated
_data
= [] # Message data accumulated so far
266 self
._literal
_expected
= None # Message data descriptor
268 self
.compressor
= None # COMPRESS/DEFLATE if not None
269 self
.decompressor
= None
271 # Create unique tag for this session,
272 # and compile tagged response matcher.
275 self
.tagpre
= Int2AP(random
.randint(4096, 65535))
276 self
.tagre
= re
.compile(r
'(?P<tag>'
278 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
280 if __debug__
: self
._init
_debug
(debug
, debug_file
)
282 # Open socket to server.
284 self
.open(host
, port
)
288 self
._mesg
('connected to %s on port %s' % (self
.host
, self
.port
))
292 self
.Terminate
= False
294 self
.state_change_free
= threading
.Event()
295 self
.state_change_pending
= threading
.Lock()
296 self
.commands_lock
= threading
.Lock()
298 self
.ouq
= Queue
.Queue(10)
299 self
.inq
= Queue
.Queue()
301 self
.wrth
= threading
.Thread(target
=self
._writer
)
303 self
.rdth
= threading
.Thread(target
=self
._reader
)
305 self
.inth
= threading
.Thread(target
=self
._handler
)
308 # Get server welcome message,
309 # request and store CAPABILITY response.
312 self
.welcome
= self
._request
_push
(tag
='continuation').get_response('IMAP4 protocol error: %s')[1]
314 if 'PREAUTH' in self
.untagged_responses
:
316 if __debug__
: self
._log
(1, 'state => AUTH')
317 elif 'OK' in self
.untagged_responses
:
318 if __debug__
: self
._log
(1, 'state => NONAUTH')
320 raise self
.error(self
.welcome
)
322 typ
, dat
= self
.capability()
324 raise self
.error('no CAPABILITY response from server')
325 self
.capabilities
= tuple(dat
[-1].upper().split())
326 if __debug__
: self
._log
(3, 'CAPABILITY: %r' % (self
.capabilities
,))
328 for version
in AllowedVersions
:
329 if not version
in self
.capabilities
:
331 self
.PROTOCOL_VERSION
= version
334 raise self
.error('server not IMAP4 compliant')
336 self
._close
_threads
()
340 def __getattr__(self
, attr
):
341 # Allow UPPERCASE variants of IMAP4 command methods.
343 return getattr(self
, attr
.lower())
344 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
348 # Overridable methods
351 def open(self
, host
=None, port
=None):
352 """open(host=None, port=None)
353 Setup connection to remote server on "host:port"
354 (default: localhost:standard IMAP4 port).
355 This connection will be used by the routines:
356 read, send, shutdown, socket."""
358 self
.host
= host
is not None and host
or ''
359 self
.port
= port
is not None and port
or IMAP4_PORT
360 self
.sock
= self
.open_socket()
361 self
.read_fd
= self
.sock
.fileno()
364 def open_socket(self
):
366 Open socket choosing first address family available."""
368 msg
= (-1, 'could not open socket')
369 for res
in socket
.getaddrinfo(self
.host
, self
.port
, socket
.AF_UNSPEC
, socket
.SOCK_STREAM
):
370 af
, socktype
, proto
, canonname
, sa
= res
372 s
= socket
.socket(af
, socktype
, proto
)
373 except socket
.error
, msg
:
377 except socket
.error
, msg
:
382 raise socket
.error(msg
)
387 def start_compressing(self
):
388 """start_compressing()
389 Enable deflate compression on the socket (RFC 4978)."""
391 # rfc 1951 - pure DEFLATE, so use -15 for both windows
392 self
.decompressor
= zlib
.decompressobj(-15)
393 self
.compressor
= zlib
.compressobj(zlib
.Z_DEFAULT_COMPRESSION
, zlib
.DEFLATED
, -15)
396 def read(self
, size
):
398 Read at most 'size' bytes from remote."""
400 if self
.decompressor
is None:
401 return self
.sock
.recv(size
)
403 if self
.decompressor
.unconsumed_tail
:
404 data
= self
.decompressor
.unconsumed_tail
406 data
= self
.sock
.recv(8192)
408 return self
.decompressor
.decompress(data
, size
)
411 def send(self
, data
):
413 Send 'data' to remote."""
415 if self
.compressor
is not None:
416 data
= self
.compressor
.compress(data
)
417 data
+= self
.compressor
.flush(zlib
.Z_SYNC_FLUSH
)
419 self
.sock
.sendall(data
)
424 Close I/O established in "open"."""
431 Return socket instance used to connect to IMAP4 server."""
440 def enable_compression(self
):
441 """enable_compression()
442 Ask the server to start compressing the connection.
443 Should be called from user of this class after instantiation, as in:
444 if 'COMPRESS=DEFLATE' in imapobj.capabilities:
445 imapobj.enable_compression()"""
448 typ
, dat
= self
._simple
_command
('COMPRESS', 'DEFLATE')
450 self
.start_compressing()
451 if __debug__
: self
._log
(1, 'Enabled COMPRESS=DEFLATE')
453 self
.state_change_pending
.release()
456 def recent(self
, **kw
):
457 """(typ, [data]) = recent()
458 Return most recent 'RECENT' responses if any exist,
459 else prompt server for an update using the 'NOOP' command.
460 'data' is None if no new messages,
461 else list of RECENT responses, most recent last."""
464 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
466 return self
._deliver
_dat
(typ
, dat
, kw
)
467 kw
['untagged_response'] = name
468 return self
.noop(**kw
) # Prod server for response
471 def response(self
, code
, **kw
):
472 """(code, [data]) = response(code)
473 Return data for response 'code' if received, or None.
474 Old value for response 'code' is cleared."""
476 typ
, dat
= self
._untagged
_response
(code
, [None], code
.upper())
477 return self
._deliver
_dat
(typ
, dat
, kw
)
485 def append(self
, mailbox
, flags
, date_time
, message
, **kw
):
486 """(typ, [data]) = append(mailbox, flags, date_time, message)
487 Append message to named mailbox.
488 All args except `message' can be None."""
494 if (flags
[0],flags
[-1]) != ('(',')'):
495 flags
= '(%s)' % flags
499 date_time
= Time2Internaldate(date_time
)
502 self
.literal
= self
.mapCRLF_cre
.sub(CRLF
, message
)
504 return self
._simple
_command
(name
, mailbox
, flags
, date_time
, **kw
)
506 self
.state_change_pending
.release()
509 def authenticate(self
, mechanism
, authobject
, **kw
):
510 """(typ, [data]) = authenticate(mechanism, authobject)
511 Authenticate command - requires response processing.
513 'mechanism' specifies which authentication mechanism is to
514 be used - it must appear in <instance>.capabilities in the
515 form AUTH=<mechanism>.
517 'authobject' must be a callable object:
519 data = authobject(response)
521 It will be called to process server continuation responses.
522 It should return data that will be encoded and sent to server.
523 It should return None if the client abort response '*' should
526 self
.literal
= _Authenticator(authobject
).process
528 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mechanism
.upper())
530 self
._deliver
_exc
(self
.error
, dat
[-1])
532 if __debug__
: self
._log
(1, 'state => AUTH')
534 self
.state_change_pending
.release()
535 return self
._deliver
_dat
(typ
, dat
, kw
)
538 def capability(self
, **kw
):
539 """(typ, [data]) = capability()
540 Fetch capabilities list from server."""
543 kw
['untagged_response'] = name
544 return self
._simple
_command
(name
, **kw
)
547 def check(self
, **kw
):
548 """(typ, [data]) = check()
549 Checkpoint mailbox on server."""
551 return self
._simple
_command
('CHECK', **kw
)
554 def close(self
, **kw
):
555 """(typ, [data]) = close()
556 Close currently selected mailbox.
558 Deleted messages are removed from writable mailbox.
559 This is the recommended command before 'LOGOUT'."""
561 if self
.state
!= 'SELECTED':
562 raise self
.error('No mailbox selected.')
564 typ
, dat
= self
._simple
_command
('CLOSE')
567 if __debug__
: self
._log
(1, 'state => AUTH')
568 self
.state_change_pending
.release()
569 return self
._deliver
_dat
(typ
, dat
, kw
)
572 def copy(self
, message_set
, new_mailbox
, **kw
):
573 """(typ, [data]) = copy(message_set, new_mailbox)
574 Copy 'message_set' messages onto end of 'new_mailbox'."""
576 return self
._simple
_command
('COPY', message_set
, new_mailbox
, **kw
)
579 def create(self
, mailbox
, **kw
):
580 """(typ, [data]) = create(mailbox)
581 Create new mailbox."""
583 return self
._simple
_command
('CREATE', mailbox
, **kw
)
586 def delete(self
, mailbox
, **kw
):
587 """(typ, [data]) = delete(mailbox)
588 Delete old mailbox."""
590 return self
._simple
_command
('DELETE', mailbox
, **kw
)
593 def deleteacl(self
, mailbox
, who
, **kw
):
594 """(typ, [data]) = deleteacl(mailbox, who)
595 Delete the ACLs (remove any rights) set for who on mailbox."""
597 return self
._simple
_command
('DELETEACL', mailbox
, who
, **kw
)
600 def examine(self
, mailbox
='INBOX', **kw
):
601 """(typ, [data]) = examine(mailbox='INBOX', readonly=False)
602 Select a mailbox for READ-ONLY access. (Flushes all untagged responses.)
603 'data' is count of messages in mailbox ('EXISTS' response).
604 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
605 other responses should be obtained via "response('FLAGS')" etc."""
607 return self
.select(mailbox
=mailbox
, readonly
=True, **kw
)
610 def expunge(self
, **kw
):
611 """(typ, [data]) = expunge()
612 Permanently remove deleted items from selected mailbox.
613 Generates 'EXPUNGE' response for each deleted message.
614 'data' is list of 'EXPUNGE'd message numbers in order received."""
617 kw
['untagged_response'] = name
618 return self
._simple
_command
(name
, **kw
)
621 def fetch(self
, message_set
, message_parts
, **kw
):
622 """(typ, [data, ...]) = fetch(message_set, message_parts)
623 Fetch (parts of) messages.
624 'message_parts' should be a string of selected parts
625 enclosed in parentheses, eg: "(UID BODY[TEXT])".
626 'data' are tuples of message part envelope and data,
627 followed by a string containing the trailer."""
630 kw
['untagged_response'] = name
631 return self
._simple
_command
(name
, message_set
, message_parts
, **kw
)
634 def getacl(self
, mailbox
, **kw
):
635 """(typ, [data]) = getacl(mailbox)
636 Get the ACLs for a mailbox."""
638 kw
['untagged_response'] = 'ACL'
639 return self
._simple
_command
('GETACL', mailbox
, **kw
)
642 def getannotation(self
, mailbox
, entry
, attribute
, **kw
):
643 """(typ, [data]) = getannotation(mailbox, entry, attribute)
644 Retrieve ANNOTATIONs."""
646 kw
['untagged_response'] = 'ANNOTATION'
647 return self
._simple
_command
('GETANNOTATION', mailbox
, entry
, attribute
, **kw
)
650 def getquota(self
, root
, **kw
):
651 """(typ, [data]) = getquota(root)
652 Get the quota root's resource usage and limits.
653 (Part of the IMAP4 QUOTA extension defined in rfc2087.)"""
655 kw
['untagged_response'] = 'QUOTA'
656 return self
._simple
_command
('GETQUOTA', root
, **kw
)
659 def getquotaroot(self
, mailbox
, **kw
):
660 # Hmmm, this is non-std! Left for backwards-compatibility, sigh.
661 # NB: usage should have been defined as:
662 # (typ, [QUOTAROOT responses...]) = getquotaroot(mailbox)
663 # (typ, [QUOTA responses...]) = response('QUOTA')
664 """(typ, [[QUOTAROOT responses...], [QUOTA responses...]]) = getquotaroot(mailbox)
665 Get the list of quota roots for the named mailbox."""
667 typ
, dat
= self
._simple
_command
('GETQUOTAROOT', mailbox
)
668 typ
, quota
= self
._untagged
_response
(typ
, dat
, 'QUOTA')
669 typ
, quotaroot
= self
._untagged
_response
(typ
, dat
, 'QUOTAROOT')
670 return self
._deliver
_dat
(typ
, [quotaroot
, quota
], kw
)
673 def idle(self
, timeout
=None, **kw
):
674 """"(typ, [data]) = idle(timeout=None)
675 Put server into IDLE mode until server notifies some change,
676 or 'timeout' (secs) occurs (default: 29 minutes),
677 or another IMAP4 command is scheduled."""
680 self
.literal
= _IdleCont(self
, timeout
).process
682 return self
._simple
_command
(name
, **kw
)
684 self
.state_change_pending
.release()
687 def list(self
, directory
='""', pattern
='*', **kw
):
688 """(typ, [data]) = list(directory='""', pattern='*')
689 List mailbox names in directory matching pattern.
690 'data' is list of LIST responses.
693 % matches all except separator ( so LIST "" "%" returns names at root)
694 * matches all (so LIST "" "*" returns whole directory tree from root)"""
697 kw
['untagged_response'] = name
698 return self
._simple
_command
(name
, directory
, pattern
, **kw
)
701 def login(self
, user
, password
, **kw
):
702 """(typ, [data]) = login(user, password)
703 Identify client using plaintext password.
704 NB: 'password' will be quoted."""
707 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
709 self
._deliver
_exc
(self
.error
, dat
[-1], kw
)
711 if __debug__
: self
._log
(1, 'state => AUTH')
713 self
.state_change_pending
.release()
714 return self
._deliver
_dat
(typ
, dat
, kw
)
717 def login_cram_md5(self
, user
, password
, **kw
):
718 """(typ, [data]) = login_cram_md5(user, password)
719 Force use of CRAM-MD5 authentication."""
721 self
.user
, self
.password
= user
, password
722 return self
.authenticate('CRAM-MD5', self
._CRAM
_MD
5_AUTH
, **kw
)
725 def _CRAM_MD5_AUTH(self
, challenge
):
726 """Authobject to use with CRAM-MD5 authentication."""
728 return self
.user
+ " " + hmac
.HMAC(self
.password
, challenge
).hexdigest()
731 def logout(self
, **kw
):
732 """(typ, [data]) = logout()
733 Shutdown connection to server.
734 Returns server 'BYE' response."""
737 if __debug__
: self
._log
(1, 'state => LOGOUT')
740 typ
, dat
= self
._simple
_command
('LOGOUT')
742 typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
743 if __debug__
: self
._log
(1, dat
)
745 self
._close
_threads
()
747 self
.state_change_pending
.release()
749 if __debug__
: self
._log
(1, 'connection closed')
751 bye
= self
.untagged_responses
.get('BYE')
753 typ
, dat
= 'BYE', bye
754 return self
._deliver
_dat
(typ
, dat
, kw
)
757 def lsub(self
, directory
='""', pattern
='*', **kw
):
758 """(typ, [data, ...]) = lsub(directory='""', pattern='*')
759 List 'subscribed' mailbox names in directory matching pattern.
760 'data' are tuples of message part envelope and data."""
763 kw
['untagged_response'] = name
764 return self
._simple
_command
(name
, directory
, pattern
, **kw
)
767 def myrights(self
, mailbox
):
768 """(typ, [data]) = myrights(mailbox)
769 Show my ACLs for a mailbox (i.e. the rights that I have on mailbox)."""
772 kw
['untagged_response'] = name
773 return self
._simple
_command
(name
, mailbox
, **kw
)
776 def namespace(self
, **kw
):
777 """(typ, [data, ...]) = namespace()
778 Returns IMAP namespaces ala rfc2342."""
781 kw
['untagged_response'] = name
782 return self
._simple
_command
(name
, **kw
)
785 def noop(self
, **kw
):
786 """(typ, [data]) = noop()
787 Send NOOP command."""
789 if __debug__
: self
._dump
_ur
(3)
790 return self
._simple
_command
('NOOP', **kw
)
793 def partial(self
, message_num
, message_part
, start
, length
, **kw
):
794 """(typ, [data, ...]) = partial(message_num, message_part, start, length)
795 Fetch truncated part of a message.
796 'data' is tuple of message part envelope and data.
800 kw
['untagged_response'] = 'FETCH'
801 return self
._simple
_command
(name
, message_num
, message_part
, start
, length
, **kw
)
804 def proxyauth(self
, user
, **kw
):
805 """(typ, [data]) = proxyauth(user)
806 Assume authentication as 'user'.
807 (Allows an authorised administrator to proxy into any user's mailbox.)"""
810 return self
._simple
_command
('PROXYAUTH', user
, **kw
)
812 self
.state_change_pending
.release()
815 def rename(self
, oldmailbox
, newmailbox
, **kw
):
816 """(typ, [data]) = rename(oldmailbox, newmailbox)
817 Rename old mailbox name to new."""
819 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
, **kw
)
822 def search(self
, charset
, *criteria
, **kw
):
823 """(typ, [data]) = search(charset, criterion, ...)
824 Search mailbox for matching messages.
825 'data' is space separated list of matching message numbers."""
828 kw
['untagged_response'] = name
830 return self
._simple
_command
(name
, 'CHARSET', charset
, *criteria
, **kw
)
831 return self
._simple
_command
(name
, *criteria
, **kw
)
834 def select(self
, mailbox
='INBOX', readonly
=False, **kw
):
835 """(typ, [data]) = select(mailbox='INBOX', readonly=False)
836 Select a mailbox. (Flushes all untagged responses.)
837 'data' is count of messages in mailbox ('EXISTS' response).
838 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
839 other responses should be obtained via "response('FLAGS')" etc."""
841 self
.commands_lock
.acquire()
842 self
.untagged_responses
= {} # Flush old responses.
843 self
.commands_lock
.release()
845 self
.is_readonly
= readonly
and True or False
851 rqb
= self
._command
(name
, mailbox
)
852 typ
, dat
= rqb
.get_response('command: %s => %%s' % rqb
.name
)
854 if self
.state
== SELECTED
:
856 if __debug__
: self
._log
(1, 'state => AUTH')
858 self
._deliver
_exc
(self
.error
, '%s command error: %s %s. Data: %.100s' % (name
, typ
, dat
, mailbox
), kw
)
859 return self
._deliver
_dat
(typ
, dat
, kw
)
860 self
.state
= SELECTED
861 if __debug__
: self
._log
(1, 'state => SELECTED')
863 self
.state_change_pending
.release()
864 if 'READ-ONLY' in self
.untagged_responses
and not readonly
:
865 if __debug__
: self
._dump
_ur
(1)
866 self
._deliver
_exc
(self
.readonly
, '%s is not writable' % mailbox
, kw
)
867 return self
._deliver
_dat
(typ
, self
.untagged_responses
.get('EXISTS', [None]), kw
)
870 def setacl(self
, mailbox
, who
, what
, **kw
):
871 """(typ, [data]) = setacl(mailbox, who, what)
872 Set a mailbox acl."""
875 return self
._simple
_command
('SETACL', mailbox
, who
, what
, **kw
)
877 self
.state_change_pending
.release()
880 def setannotation(self
, *args
, **kw
):
881 """(typ, [data]) = setannotation(mailbox[, entry, attribute]+)
884 kw
['untagged_response'] = 'ANNOTATION'
885 return self
._simple
_command
('SETANNOTATION', *args
, **kw
)
888 def setquota(self
, root
, limits
, **kw
):
889 """(typ, [data]) = setquota(root, limits)
890 Set the quota root's resource limits."""
892 kw
['untagged_response'] = 'QUOTA'
894 return self
._simple
_command
('SETQUOTA', root
, limits
, **kw
)
896 self
.state_change_pending
.release()
899 def sort(self
, sort_criteria
, charset
, *search_criteria
, **kw
):
900 """(typ, [data]) = sort(sort_criteria, charset, search_criteria, ...)
901 IMAP4rev1 extension SORT command."""
904 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
905 sort_criteria
= '(%s)' % sort_criteria
906 kw
['untagged_response'] = name
907 return self
._simple
_command
(name
, sort_criteria
, charset
, *search_criteria
, **kw
)
910 def status(self
, mailbox
, names
, **kw
):
911 """(typ, [data]) = status(mailbox, names)
912 Request named status conditions for mailbox."""
915 kw
['untagged_response'] = name
916 return self
._simple
_command
(name
, mailbox
, names
, **kw
)
919 def store(self
, message_set
, command
, flags
, **kw
):
920 """(typ, [data]) = store(message_set, command, flags)
921 Alters flag dispositions for messages in mailbox."""
923 if (flags
[0],flags
[-1]) != ('(',')'):
924 flags
= '(%s)' % flags
# Avoid quoting the flags
925 kw
['untagged_response'] = 'FETCH'
926 return self
._simple
_command
('STORE', message_set
, command
, flags
, **kw
)
929 def subscribe(self
, mailbox
, **kw
):
930 """(typ, [data]) = subscribe(mailbox)
931 Subscribe to new mailbox."""
934 return self
._simple
_command
('SUBSCRIBE', mailbox
, **kw
)
936 self
.state_change_pending
.release()
939 def thread(self
, threading_algorithm
, charset
, *search_criteria
, **kw
):
940 """(type, [data]) = thread(threading_alogrithm, charset, search_criteria, ...)
941 IMAPrev1 extension THREAD command."""
944 kw
['untagged_response'] = name
945 return self
._simple
_command
(name
, threading_algorithm
, charset
, *search_criteria
, **kw
)
948 def uid(self
, command
, *args
, **kw
):
949 """(typ, [data]) = uid(command, arg, ...)
950 Execute "command arg ..." with messages identified by UID,
951 rather than message number.
952 Assumes 'command' is legal in current state.
953 Returns response appropriate to 'command'."""
955 command
= command
.upper()
956 if command
in UID_direct
:
960 kw
['untagged_response'] = resp
961 return self
._simple
_command
('UID', command
, *args
, **kw
)
964 def unsubscribe(self
, mailbox
, **kw
):
965 """(typ, [data]) = unsubscribe(mailbox)
966 Unsubscribe from old mailbox."""
969 return self
._simple
_command
('UNSUBSCRIBE', mailbox
, **kw
)
971 self
.state_change_pending
.release()
974 def xatom(self
, name
, *args
, **kw
):
975 """(typ, [data]) = xatom(name, arg, ...)
976 Allow simple extension commands notified by server in CAPABILITY response.
977 Assumes extension command 'name' is legal in current state.
978 Returns response appropriate to extension command 'name'."""
981 if not name
in Commands
:
982 Commands
[name
] = ((self
.state
,), False)
984 return self
._simple
_command
(name
, *args
, **kw
)
986 if self
.state_change_pending
.locked():
987 self
.state_change_pending
.release()
994 def _append_untagged(self
, typ
, dat
):
996 if dat
is None: dat
= ''
998 self
.commands_lock
.acquire()
999 ur
= self
.untagged_responses
.setdefault(typ
, [])
1001 self
.commands_lock
.release()
1003 if __debug__
: self
._log
(5, 'untagged_responses[%s] %s += ["%s"]' % (typ
, len(ur
)-1, dat
))
1006 def _check_bye(self
):
1008 bye
= self
.untagged_responses
.get('BYE')
1010 raise self
.abort(bye
[-1])
1013 def _checkquote(self
, arg
):
1015 # Must quote command args if non-alphanumeric chars present,
1016 # and not already quoted.
1018 if not isinstance(arg
, basestring
):
1020 if len(arg
) >= 2 and (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
1022 if arg
and self
.mustquote_cre
.search(arg
) is None:
1024 return self
._quote
(arg
)
1027 def _command(self
, name
, *args
, **kw
):
1029 if Commands
[name
][CMD_VAL_ASYNC
]:
1034 if __debug__
: self
._log
(1, '[%s] %s %s' % (cmdtyp
, name
, args
))
1036 self
.state_change_pending
.acquire()
1040 if cmdtyp
== 'async':
1041 self
.state_change_pending
.release()
1043 # Need to wait for all async commands to complete
1045 self
.commands_lock
.acquire()
1046 if self
.tagged_commands
:
1047 self
.state_change_free
.clear()
1051 self
.commands_lock
.release()
1053 if __debug__
: self
._log
(4, 'sync command %s waiting for empty commands Q' % name
)
1054 self
.state_change_free
.wait()
1055 if __debug__
: self
._log
(4, 'sync command %s proceeding' % name
)
1057 if self
.state
not in Commands
[name
][CMD_VAL_STATES
]:
1059 raise self
.error('command %s illegal in state %s'
1060 % (name
, self
.state
))
1064 self
.commands_lock
.acquire()
1065 for typ
in ('OK', 'NO', 'BAD'):
1066 if typ
in self
.untagged_responses
:
1067 del self
.untagged_responses
[typ
]
1068 self
.commands_lock
.release()
1070 if 'READ-ONLY' in self
.untagged_responses \
1071 and not self
.is_readonly
:
1073 raise self
.readonly('mailbox status changed to READ-ONLY')
1076 raise self
.abort('connection closed')
1078 rqb
= self
._request
_push
(name
=name
, **kw
)
1080 data
= '%s %s' % (rqb
.tag
, name
)
1082 if arg
is None: continue
1083 data
= '%s %s' % (data
, self
._checkquote
(arg
))
1085 literal
= self
.literal
1086 if literal
is not None:
1088 if isinstance(literal
, basestring
):
1090 data
= '%s {%s}' % (data
, len(literal
))
1094 if __debug__
: self
._log
(4, 'data=%s' % data
)
1096 rqb
.data
= '%s%s' % (data
, CRLF
)
1102 # Must setup continuation expectancy *before* ouq.put
1103 crqb
= self
._request
_push
(tag
='continuation')
1108 # Wait for continuation response
1110 ok
, data
= crqb
.get_response('command: %s => %%s' % name
)
1111 if __debug__
: self
._log
(3, 'continuation => %s, %s' % (ok
, data
))
1120 if literator
is not None:
1121 literal
= literator(data
, rqb
)
1126 if literator
is not None:
1127 # Need new request for next continuation response
1128 crqb
= self
._request
_push
(tag
='continuation')
1130 if __debug__
: self
._log
(4, 'write literal size %s' % len(literal
))
1131 crqb
.data
= '%s%s' % (literal
, CRLF
)
1134 if literator
is None:
1140 def _command_complete(self
, rqb
, kw
):
1142 # Called for non-callback commands
1144 typ
, dat
= rqb
.get_response('command: %s => %%s' % rqb
.name
)
1147 if __debug__
: self
._print
_log
()
1148 raise self
.error('%s command error: %s %s. Data: %.100s' % (rqb
.name
, typ
, dat
, rqb
.data
))
1149 if 'untagged_response' in kw
:
1150 return self
._untagged
_response
(typ
, dat
, kw
['untagged_response'])
1154 def _command_completer(self
, (response
, cb_arg
, error
)):
1156 # Called for callback commands
1158 rqb
.callback
= kw
['callback']
1159 rqb
.callback_arg
= kw
.get('cb_arg')
1160 if error
is not None:
1161 if __debug__
: self
._print
_log
()
1165 bye
= self
.untagged_responses
.get('BYE')
1167 rqb
.abort(self
.abort
, bye
[-1])
1171 if __debug__
: self
._print
_log
()
1172 rqb
.abort(self
.error
, '%s command error: %s %s. Data: %.100s' % (rqb
.name
, typ
, dat
, rqb
.data
))
1174 if 'untagged_response' in kw
:
1175 response
= self
._untagged
_response
(typ
, dat
, kw
['untagged_response'])
1176 rqb
.deliver(response
)
1179 def _deliver_dat(self
, typ
, dat
, kw
):
1181 if 'callback' in kw
:
1182 kw
['callback'](((typ
, dat
), kw
.get('cb_arg'), None))
1186 def _deliver_exc(self
, exc
, dat
, kw
):
1188 if 'callback' in kw
:
1189 kw
['callback']((None, kw
.get('cb_arg'), (exc
, dat
)))
1193 def _end_idle(self
):
1195 irqb
= self
.idle_rqb
1198 self
.idle_rqb
= None
1199 self
.idle_timeout
= None
1200 irqb
.data
= 'DONE%s' % CRLF
1202 if __debug__
: self
._log
(2, 'server IDLE finished')
1205 def _match(self
, cre
, s
):
1207 # Run compiled regular expression 'cre' match method on 's'.
1208 # Save result, return success.
1210 self
.mo
= cre
.match(s
)
1211 return self
.mo
is not None
1214 def _put_response(self
, resp
):
1216 if self
._expecting
_data
> 0:
1218 dlen
= min(self
._expecting
_data
, rlen
)
1219 self
._expecting
_data
-= dlen
1221 self
._accumulated
_data
.append(resp
)
1223 self
._accumulated
_data
.append(resp
[:dlen
])
1226 if self
._accumulated
_data
:
1227 typ
, dat
= self
._literal
_expected
1228 self
._append
_untagged
(typ
, (dat
, ''.join(self
._accumulated
_data
)))
1229 self
._accumulated
_data
= []
1231 # Protocol mandates all lines terminated by CRLF
1234 if 'continuation' in self
.tagged_commands
:
1235 continuation_expected
= True
1237 continuation_expected
= False
1239 if self
._literal
_expected
is not None:
1241 if self
._match
(self
.literal_cre
, dat
):
1242 self
._literal
_expected
[1] = dat
1243 self
._expecting
_data
= int(self
.mo
.group('size'))
1244 if __debug__
: self
._log
(4, 'expecting literal size %s' % self
._expecting
_data
)
1246 typ
= self
._literal
_expected
[0]
1247 self
._literal
_expected
= None
1248 self
._append
_untagged
(typ
, dat
) # Tail
1249 if __debug__
: self
._log
(4, 'literal completed')
1251 # Command completion response?
1252 if self
._match
(self
.tagre
, resp
):
1253 tag
= self
.mo
.group('tag')
1254 typ
= self
.mo
.group('type')
1255 dat
= self
.mo
.group('data')
1256 if not tag
in self
.tagged_commands
:
1257 if __debug__
: self
._log
(1, 'unexpected tagged response: %s' % resp
)
1259 self
._request
_pop
(tag
, (typ
, [dat
]))
1263 # '*' (untagged) responses?
1265 if not self
._match
(self
.untagged_response_cre
, resp
):
1266 if self
._match
(self
.untagged_status_cre
, resp
):
1267 dat2
= self
.mo
.group('data2')
1270 # Only other possibility is '+' (continuation) response...
1272 if self
._match
(self
.continuation_cre
, resp
):
1273 if not continuation_expected
:
1274 if __debug__
: self
._log
(1, "unexpected continuation response: '%s'" % resp
)
1276 self
._request
_pop
('continuation', (True, self
.mo
.group('data')))
1279 if __debug__
: self
._log
(1, "unexpected response: '%s'" % resp
)
1282 typ
= self
.mo
.group('type')
1283 dat
= self
.mo
.group('data')
1284 if dat
is None: dat
= '' # Null untagged response
1285 if dat2
: dat
= dat
+ ' ' + dat2
1287 # Is there a literal to come?
1289 if self
._match
(self
.literal_cre
, dat
):
1290 self
._expecting
_data
= int(self
.mo
.group('size'))
1291 if __debug__
: self
._log
(4, 'read literal size %s' % self
._expecting
_data
)
1292 self
._literal
_expected
= [typ
, dat
]
1295 self
._append
_untagged
(typ
, dat
)
1300 # Bracketed response information?
1302 if typ
in ('OK', 'NO', 'BAD') and self
._match
(self
.response_code_cre
, dat
):
1303 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
1305 # Command waiting for aborted continuation response?
1307 if continuation_expected
:
1308 self
._request
_pop
('continuation', (False, resp
))
1312 if typ
in ('NO', 'BAD', 'BYE'):
1314 self
.Terminate
= True
1315 if __debug__
: self
._log
(1, '%s response: %s' % (typ
, dat
))
1318 def _quote(self
, arg
):
1320 return '"%s"' % arg
.replace('\\', '\\\\').replace('"', '\\"')
1323 def _request_pop(self
, name
, data
):
1325 if __debug__
: self
._log
(4, '_request_pop(%s, %s)' % (name
, data
))
1326 self
.commands_lock
.acquire()
1327 rqb
= self
.tagged_commands
.pop(name
)
1328 if not self
.tagged_commands
:
1329 self
.state_change_free
.set()
1330 self
.commands_lock
.release()
1334 def _request_push(self
, tag
=None, name
=None, **kw
):
1336 self
.commands_lock
.acquire()
1337 rqb
= Request(self
, name
=name
, **kw
)
1340 self
.tagged_commands
[tag
] = rqb
1341 self
.commands_lock
.release()
1342 if __debug__
: self
._log
(4, '_request_push(%s, %s, %s)' % (tag
, name
, `kw`
))
1346 def _simple_command(self
, name
, *args
, **kw
):
1348 if 'callback' in kw
:
1349 rqb
= self
._command
(name
, callback
=self
._command
_completer
, *args
)
1350 rqb
.callback_arg
= (rqb
, kw
)
1352 return self
._command
_complete
(self
._command
(name
, *args
), kw
)
1355 def _untagged_response(self
, typ
, dat
, name
):
1359 if not name
in self
.untagged_responses
:
1361 self
.commands_lock
.acquire()
1362 data
= self
.untagged_responses
.pop(name
)
1363 self
.commands_lock
.release()
1364 if __debug__
: self
._log
(5, 'pop untagged_responses[%s] => %s' % (name
, (typ
, data
)))
1372 def _close_threads(self
):
1385 threading
.currentThread().setName('hdlr')
1387 time
.sleep(0.1) # Don't start handling before main thread ready
1389 if __debug__
: self
._log
(1, 'starting')
1391 typ
, val
= self
.abort
, 'connection terminated'
1393 while not self
.Terminate
:
1395 if self
.idle_timeout
is not None:
1396 timeout
= self
.idle_timeout
- time
.time()
1400 if self
.idle_rqb
is not None:
1401 self
._log
(5, 'server IDLING, timeout=%.2f' % timeout
)
1404 line
= self
.inq
.get(True, timeout
)
1406 if self
.idle_rqb
is None:
1408 if self
.idle_timeout
> time
.time():
1410 if __debug__
: self
._log
(2, 'server IDLE timedout')
1411 line
= IDLE_TIMEOUT_RESPONSE
1416 if not isinstance(line
, basestring
):
1421 self
._put
_response
(line
)
1423 typ
, val
= self
.error
, 'program error: %s - %s' % sys
.exc_info()[:2]
1426 self
.Terminate
= True
1428 while not self
.ouq
.empty():
1430 self
.ouq
.get_nowait().abort(typ
, val
)
1435 self
.commands_lock
.acquire()
1436 for name
in self
.tagged_commands
.keys():
1437 rqb
= self
.tagged_commands
.pop(name
)
1439 self
.state_change_free
.set()
1440 self
.commands_lock
.release()
1442 if __debug__
: self
._log
(1, 'finished')
1445 if hasattr(select_module
, "poll"):
1449 threading
.currentThread().setName('redr')
1451 if __debug__
: self
._log
(1, 'starting using poll')
1453 def poll_error(state
):
1455 select
.POLLERR
: 'Error',
1456 select
.POLLHUP
: 'Hang up',
1457 select
.POLLNVAL
: 'Invalid request: descriptor not open',
1459 return ' '.join([PollErrors
[s
] for s
in PollErrors
.keys() if (s
& state
)])
1463 poll
= select
.poll()
1465 poll
.register(self
.read_fd
, select
.POLLIN
)
1467 while not self
.Terminate
:
1468 if self
.state
== LOGOUT
:
1473 r
= poll
.poll(timeout
)
1474 if __debug__
: self
._log
(5, 'poll => %s' % `r`
)
1480 if state
& select
.POLLIN
:
1481 data
= self
.read(32768) # Drain ssl buffer if present
1484 if __debug__
: self
._log
(5, 'rcvd %s' % dlen
)
1488 stop
= data
.find('\n', start
)
1490 line_part
+= data
[start
:]
1493 line_part
, start
, line
= \
1494 '', stop
, line_part
+ data
[start
:stop
]
1495 if __debug__
: self
._log
(4, '< %s' % line
)
1498 if state
& ~
(select
.POLLIN
):
1499 raise IOError(poll_error(state
))
1501 reason
= 'socket error: %s - %s' % sys
.exc_info()[:2]
1503 if not self
.Terminate
:
1505 if self
.debug
: self
.debug
+= 4 # Output all
1506 self
._log
(1, reason
)
1507 self
.inq
.put((self
.abort
, reason
))
1510 poll
.unregister(self
.read_fd
)
1512 if __debug__
: self
._log
(1, 'finished')
1516 # No "poll" - use select()
1520 threading
.currentThread().setName('redr')
1522 if __debug__
: self
._log
(1, 'starting using select')
1526 while not self
.Terminate
:
1527 if self
.state
== LOGOUT
:
1532 r
,w
,e
= select
.select([self
.read_fd
], [], [], timeout
)
1533 if __debug__
: self
._log
(5, 'select => %s, %s, %s' % (r
,w
,e
))
1537 data
= self
.read(32768) # Drain ssl buffer if present
1540 if __debug__
: self
._log
(5, 'rcvd %s' % dlen
)
1544 stop
= data
.find('\n', start
)
1546 line_part
+= data
[start
:]
1549 line_part
, start
, line
= \
1550 '', stop
, line_part
+ data
[start
:stop
]
1551 if __debug__
: self
._log
(4, '< %s' % line
)
1554 reason
= 'socket error: %s - %s' % sys
.exc_info()[:2]
1556 if not self
.Terminate
:
1558 if self
.debug
: self
.debug
+= 4 # Output all
1559 self
._log
(1, reason
)
1560 self
.inq
.put((self
.abort
, reason
))
1563 if __debug__
: self
._log
(1, 'finished')
1568 threading
.currentThread().setName('wrtr')
1570 if __debug__
: self
._log
(1, 'starting')
1572 reason
= 'Terminated'
1574 while not self
.Terminate
:
1575 rqb
= self
.ouq
.get()
1577 break # Outq flushed
1581 if __debug__
: self
._log
(4, '> %s' % rqb
.data
)
1583 reason
= 'socket error: %s - %s' % sys
.exc_info()[:2]
1585 if not self
.Terminate
:
1587 if self
.debug
: self
.debug
+= 4 # Output all
1588 self
._log
(1, reason
)
1589 rqb
.abort(self
.abort
, reason
)
1592 self
.inq
.put((self
.abort
, reason
))
1594 if __debug__
: self
._log
(1, 'finished')
1603 def _init_debug(self
, debug
=None, debug_file
=None):
1604 self
.debug
= debug
is not None and debug
or Debug
is not None and Debug
or 0
1605 self
.debug_file
= debug_file
is not None and debug_file
or sys
.stderr
1607 self
.debug_lock
= threading
.Lock()
1608 self
._cmd
_log
_len
= 20
1609 self
._cmd
_log
_idx
= 0
1610 self
._cmd
_log
= {} # Last `_cmd_log_len' interactions
1612 self
._mesg
('imaplib2 version %s' % __version__
)
1613 self
._mesg
('imaplib2 debug level %s' % self
.debug
)
1616 def _dump_ur(self
, lvl
):
1617 if lvl
> self
.debug
:
1620 l
= self
.untagged_responses
.items()
1625 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1626 self
.debug_lock
.acquire()
1627 self
._mesg
('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1628 self
.debug_lock
.release()
1631 def _log(self
, lvl
, line
):
1632 if lvl
> self
.debug
:
1635 if line
[-2:] == CRLF
:
1636 line
= line
[:-2] + '\\r\\n'
1638 tn
= threading
.currentThread().getName()
1641 self
.debug_lock
.acquire()
1642 self
._mesg
(line
, tn
)
1643 self
.debug_lock
.release()
1646 # Keep log of last `_cmd_log_len' interactions for debugging.
1647 self
._cmd
_log
[self
._cmd
_log
_idx
] = (line
, tn
, time
.time())
1648 self
._cmd
_log
_idx
+= 1
1649 if self
._cmd
_log
_idx
>= self
._cmd
_log
_len
:
1650 self
._cmd
_log
_idx
= 0
1653 def _mesg(self
, s
, tn
=None, secs
=None):
1657 tn
= threading
.currentThread().getName()
1658 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1659 self
.debug_file
.write(' %s.%02d %s %s\n' % (tm
, (secs
*100)%100, tn
, s
))
1660 self
.debug_file
.flush()
1663 def _print_log(self
):
1664 self
.debug_lock
.acquire()
1665 i
, n
= self
._cmd
_log
_idx
, self
._cmd
_log
_len
1666 if n
: self
._mesg
('last %d imaplib2 reports:' % n
)
1669 self
._mesg
(*self
._cmd
_log
[i
])
1673 if i
>= self
._cmd
_log
_len
:
1676 self
.debug_lock
.release()
1680 class IMAP4_SSL(IMAP4
):
1682 """IMAP4 client class over SSL connection
1685 IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, debug=None, debug_file=None)
1687 host - host's name (default: localhost);
1688 port - port number (default: standard IMAP4 SSL port);
1689 keyfile - PEM formatted file that contains your private key (default: None);
1690 certfile - PEM formatted certificate chain file (default: None);
1691 debug - debug level (default: 0 - no debug);
1692 debug_file - debug stream (default: sys.stderr).
1694 For more documentation see the docstring of the parent class IMAP4.
1698 def __init__(self
, host
=None, port
=None, keyfile
=None, certfile
=None, debug
=None, debug_file
=None):
1699 self
.keyfile
= keyfile
1700 self
.certfile
= certfile
1701 IMAP4
.__init__(self
, host
, port
, debug
, debug_file
)
1704 def open(self
, host
=None, port
=None):
1705 """open(host=None, port=None)
1706 Setup secure connection to remote server on "host:port"
1707 (default: localhost:standard IMAP4 SSL port).
1708 This connection will be used by the routines:
1709 read, send, shutdown, socket, ssl."""
1711 self
.host
= host
is not None and host
or ''
1712 self
.port
= port
is not None and port
or IMAP4_SSL_PORT
1713 self
.sock
= self
.open_socket()
1717 self
.sslobj
= ssl
.wrap_socket(self
.sock
, self
.keyfile
, self
.certfile
)
1719 self
.sslobj
= socket
.ssl(self
.sock
, self
.keyfile
, self
.certfile
)
1721 self
.read_fd
= self
.sock
.fileno()
1724 def read(self
, size
):
1725 """data = read(size)
1726 Read at most 'size' bytes from remote."""
1728 if self
.decompressor
is None:
1729 return self
.sslobj
.read(size
)
1731 if self
.decompressor
.unconsumed_tail
:
1732 data
= self
.decompressor
.unconsumed_tail
1734 data
= self
.sslobj
.read(8192)
1736 return self
.decompressor
.decompress(data
, size
)
1739 def send(self
, data
):
1741 Send 'data' to remote."""
1743 if self
.compressor
is not None:
1744 data
= self
.compressor
.compress(data
)
1745 data
+= self
.compressor
.flush(zlib
.Z_SYNC_FLUSH
)
1747 # NB: socket.ssl needs a "sendall" method to match socket objects.
1750 sent
= self
.sslobj
.write(data
)
1754 bytes
= bytes
- sent
1759 Return socket.ssl instance used to communicate with the IMAP4 server."""
1765 class IMAP4_stream(IMAP4
):
1767 """IMAP4 client class over a stream
1770 IMAP4_stream(command, debug=None, debug_file=None)
1772 command - string that can be passed to os.popen2();
1773 debug - debug level (default: 0 - no debug);
1774 debug_file - debug stream (default: sys.stderr).
1776 For more documentation see the docstring of the parent class IMAP4.
1780 def __init__(self
, command
, debug
=None, debug_file
=None):
1781 self
.command
= command
1785 self
.writefile
, self
.readfile
= None, None
1787 IMAP4
.__init__(self
, debug
=debug
, debug_file
=debug_file
)
1790 def open(self
, host
=None, port
=None):
1791 """open(host=None, port=None)
1792 Setup a stream connection via 'self.command'.
1793 This connection will be used by the routines:
1794 read, send, shutdown, socket."""
1796 self
.writefile
, self
.readfile
= os
.popen2(self
.command
)
1797 self
.read_fd
= self
.readfile
.fileno()
1800 def read(self
, size
):
1801 """Read 'size' bytes from remote."""
1803 if self
.decompressor
is None:
1804 return os
.read(self
.read_fd
, size
)
1806 if self
.decompressor
.unconsumed_tail
:
1807 data
= self
.decompressor
.unconsumed_tail
1809 data
= os
.read(self
.read_fd
, 8192)
1811 return self
.decompressor
.decompress(data
, size
)
1814 def send(self
, data
):
1815 """Send data to remote."""
1817 if self
.compressor
is not None:
1818 data
= self
.compressor
.compress(data
)
1819 data
+= self
.compressor
.flush(zlib
.Z_SYNC_FLUSH
)
1821 self
.writefile
.write(data
)
1822 self
.writefile
.flush()
1826 """Close I/O established in "open"."""
1828 self
.readfile
.close()
1829 self
.writefile
.close()
1833 class _Authenticator(object):
1835 """Private class to provide en/de-coding
1836 for base64 authentication conversation."""
1838 def __init__(self
, mechinst
):
1839 self
.mech
= mechinst
# Callable object to provide/process data
1841 def process(self
, data
, rqb
):
1842 ret
= self
.mech(self
.decode(data
))
1844 return '*' # Abort conversation
1845 return self
.encode(ret
)
1847 def encode(self
, inp
):
1849 # Invoke binascii.b2a_base64 iteratively with
1850 # short even length buffers, strip the trailing
1851 # line feed from the result and append. "Even"
1852 # means a number that factors to both 6 and 8,
1853 # so when it gets to the end of the 8-bit input
1854 # there's no partial 6-bit output.
1864 e
= binascii
.b2a_base64(t
)
1869 def decode(self
, inp
):
1872 return binascii
.a2b_base64(inp
)
1877 class _IdleCont(object):
1879 """When process is called, server is in IDLE state
1880 and will send asynchronous changes."""
1882 def __init__(self
, parent
, timeout
):
1883 self
.parent
= parent
1884 self
.timeout
= timeout
is not None and timeout
or IDLE_TIMEOUT
1885 self
.parent
.idle_timeout
= self
.timeout
+ time
.time()
1887 def process(self
, data
, rqb
):
1888 self
.parent
.idle_rqb
= rqb
1889 self
.parent
.idle_timeout
= self
.timeout
+ time
.time()
1890 if __debug__
: self
.parent
._log
(2, 'server IDLE started, timeout in %.2f secs' % self
.timeout
)
1895 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1896 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1898 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
1899 r
'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
1900 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
1901 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
1905 def Internaldate2Time(resp
):
1907 """time_tuple = Internaldate2Time(resp)
1908 Convert IMAP4 INTERNALDATE to UT."""
1910 mo
= InternalDate
.match(resp
)
1914 mon
= Mon2num
[mo
.group('mon')]
1915 zonen
= mo
.group('zonen')
1917 day
= int(mo
.group('day'))
1918 year
= int(mo
.group('year'))
1919 hour
= int(mo
.group('hour'))
1920 min = int(mo
.group('min'))
1921 sec
= int(mo
.group('sec'))
1922 zoneh
= int(mo
.group('zoneh'))
1923 zonem
= int(mo
.group('zonem'))
1925 # INTERNALDATE timezone must be subtracted to get UT
1927 zone
= (zoneh
*60 + zonem
)*60
1931 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1933 utc
= time
.mktime(tt
)
1935 # Following is necessary because the time module has no 'mkgmtime'.
1936 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1938 lt
= time
.localtime(utc
)
1939 if time
.daylight
and lt
[-1]:
1940 zone
= zone
+ time
.altzone
1942 zone
= zone
+ time
.timezone
1944 return time
.localtime(utc
- zone
)
1946 Internaldate2tuple
= Internaldate2Time
# (Backward compatible)
1950 def Time2Internaldate(date_time
):
1952 """'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time)
1953 Convert 'date_time' to IMAP4 INTERNALDATE representation."""
1955 if isinstance(date_time
, (int, float)):
1956 tt
= time
.localtime(date_time
)
1957 elif isinstance(date_time
, (tuple, time
.struct_time
)):
1959 elif isinstance(date_time
, str) and (date_time
[0],date_time
[-1]) == ('"','"'):
1960 return date_time
# Assume in correct format
1962 raise ValueError("date_time not of a known type")
1964 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1967 if time
.daylight
and tt
[-1]:
1968 zone
= -time
.altzone
1970 zone
= -time
.timezone
1971 return '"' + dt
+ " %+03d%02d" % divmod(zone
//60, 60) + '"'
1975 FLAGS_cre
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
1977 def ParseFlags(resp
):
1979 """('flag', ...) = ParseFlags(line)
1980 Convert IMAP4 flags response to python tuple."""
1982 mo
= FLAGS_cre
.match(resp
)
1986 return tuple(mo
.group('flags').split())
1990 if __name__
== '__main__':
1992 # To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]',
1993 # or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1994 # or as 'python imaplib2.py -l "keyfile[:certfile]" [IMAP4_SSL_server_hostname]'
1996 import getopt
, getpass
1999 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:l:s:p:v')
2000 except getopt
.error
, val
:
2001 optlist
, args
= (), ()
2003 debug
, port
, stream_command
, keyfile
, certfile
= (None,)*5
2004 for opt
,val
in optlist
:
2009 keyfile
,certfile
= val
.split(':')
2011 keyfile
,certfile
= val
,val
2015 stream_command
= val
2016 if not args
: args
= (stream_command
,)
2021 if not args
: args
= ('',)
2022 if not port
: port
= (keyfile
is not None) and IMAP4_SSL_PORT
or IMAP4_PORT
2026 USER
= getpass
.getuser()
2028 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \
2029 % {'user':USER
, 'lf':'\n', 'data':open(__file__
).read()}
2031 ('list', ('""', '%')),
2032 ('create', ('/tmp/imaplib2_test.0',)),
2033 ('rename', ('/tmp/imaplib2_test.0', '/tmp/imaplib2_test.1')),
2034 ('CREATE', ('/tmp/imaplib2_test.2',)),
2035 ('append', ('/tmp/imaplib2_test.2', None, None, test_mesg
)),
2036 ('list', ('/tmp', 'imaplib2_test*')),
2037 ('select', ('/tmp/imaplib2_test.2',)),
2038 ('search', (None, 'SUBJECT', 'IMAP4 test')),
2039 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
2040 ('store', ('1', 'FLAGS', '(\Deleted)')),
2049 ('response',('UIDVALIDITY',)),
2050 ('response', ('EXISTS',)),
2051 ('append', (None, None, None, test_mesg
)),
2052 ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')),
2053 ('uid', ('SEARCH', 'ALL')),
2054 ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')),
2060 def responder((response
, cb_arg
, error
)):
2063 if error
is not None:
2065 M
._mesg
('[cb] ERROR %s %.100s => %s' % (cmd
, args
, error
))
2068 M
._mesg
('[cb] %s %.100s => %s %.100s' % (cmd
, args
, typ
, dat
))
2070 AsyncError
= (Exception, dat
[0])
2072 def run(cmd
, args
, cb
=None):
2075 typ
, val
= AsyncError
2077 M
._mesg
('%s %.100s' % (cmd
, args
))
2080 typ
, dat
= getattr(M
, cmd
)(callback
=responder
, cb_arg
=(cmd
, args
), *args
)
2082 M
._mesg
('%s %.100s => %s %.100s' % (cmd
, args
, typ
, dat
))
2084 typ
, dat
= getattr(M
, cmd
)(*args
)
2085 M
._mesg
('%s %.100s => %s %.100s' % (cmd
, args
, typ
, dat
))
2091 raise Exception(dat
[0])
2095 threading
.currentThread().setName('main')
2097 if keyfile
is not None:
2098 if not keyfile
: keyfile
= None
2099 if not certfile
: certfile
= None
2100 M
= IMAP4_SSL(host
=host
, port
=port
, keyfile
=keyfile
, certfile
=certfile
, debug
=debug
)
2101 elif stream_command
:
2102 M
= IMAP4_stream(stream_command
, debug
=debug
)
2104 M
= IMAP4(host
=host
, port
=port
, debug
=debug
)
2105 if M
.state
!= 'AUTH': # Login needed
2106 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
2107 test_seq1
.insert(0, ('login', (USER
, PASSWD
)))
2108 M
._mesg
('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
2109 M
._mesg
('CAPABILITIES = %r' % (M
.capabilities
,))
2110 if 'COMPRESS=DEFLATE' in M
.capabilities
:
2111 M
.enable_compression()
2113 for cmd
,args
in test_seq1
:
2114 run(cmd
, args
, cb
=1)
2116 for ml
in run('list', ('/tmp/', 'imaplib2_test%')):
2117 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
2118 if mo
: path
= mo
.group(1)
2119 else: path
= ml
.split()[-1]
2120 run('delete', (path
,), cb
=1)
2122 for cmd
,args
in test_seq2
:
2123 if (cmd
,args
) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')):
2124 run(cmd
, args
, cb
=1)
2127 dat
= run(cmd
, args
)
2128 uid
= dat
[-1].split()
2129 if not uid
: continue
2130 run('uid', ('FETCH', uid
[-1],
2131 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'), cb
=1)
2132 run('uid', ('STORE', uid
[-1], 'FLAGS', '(\Deleted)'), cb
=1)
2133 run('expunge', (), cb
=1)
2142 print '\nAll tests OK.'
2145 print '\nTests failed.'
2149 If you would like to see debugging output,