Using standard imaplib2.py

This is the standard imaplib2 file.
This commit is contained in:
Rodolfo García Peñas (kix) 2020-08-28 11:48:16 +02:00
parent 75ce6e71f3
commit 476c485a52

View File

@ -57,26 +57,18 @@ __author__ = "Piers Lauder <piers@janeelix.com>"
__URL__ = "http://imaplib2.sourceforge.net" __URL__ = "http://imaplib2.sourceforge.net"
__license__ = "Python License" __license__ = "Python License"
import binascii, calendar, errno, os, random, re, select, socket, sys, time, threading, zlib import binascii, calendar, errno, os, queue, random, re, select, socket, sys, time, threading, zlib
if bytes != str:
# Python 3, but NB assumes strings in all I/O
# for backwards compatibility with python 2 usage.
import queue
string_types = str
else:
import queue as queue
string_types = str
select_module = select select_module = select
# Globals # Globals
CRLF = '\r\n' CRLF = b'\r\n'
IMAP4_PORT = 143 IMAP4_PORT = 143
IMAP4_SSL_PORT = 993 IMAP4_SSL_PORT = 993
IDLE_TIMEOUT_RESPONSE = '* IDLE TIMEOUT\r\n' IDLE_TIMEOUT_RESPONSE = b'* IDLE TIMEOUT\r\n'
IDLE_TIMEOUT = 60*29 # Don't stay in IDLE state longer IDLE_TIMEOUT = 60*29 # Don't stay in IDLE state longer
READ_POLL_TIMEOUT = 30 # Without this timeout interrupted network connections can hang reader READ_POLL_TIMEOUT = 30 # Without this timeout interrupted network connections can hang reader
READ_SIZE = 32768 # Consume all available in socket READ_SIZE = 32768 # Consume all available in socket
@ -148,15 +140,15 @@ UID_direct = ('SEARCH', 'SORT', 'THREAD')
def Int2AP(num): def Int2AP(num):
"""string = Int2AP(num) """string = Int2AP(num)
Return 'num' converted to a string using characters from the set 'A'..'P' Return 'num' converted to bytes using characters from the set 'A'..'P'
""" """
val, a2p = [], 'ABCDEFGHIJKLMNOP' val = b''; AP = b'ABCDEFGHIJKLMNOP'
num = int(abs(num)) num = int(abs(num))
while num: while num:
num, mod = divmod(num, 16) num, mod = divmod(num, 16)
val.insert(0, a2p[mod]) val = AP[mod:mod+1] + val
return ''.join(val) return val
@ -173,7 +165,7 @@ class Request(object):
else: else:
self.callback_arg = (self, cb_arg) # Self reference required in callback arg self.callback_arg = (self, cb_arg) # Self reference required in callback arg
self.tag = '%s%s' % (parent.tagpre, parent.tagnum) self.tag = parent.tagpre + bytes(str(parent.tagnum), 'ASCII')
parent.tagnum += 1 parent.tagnum += 1
self.ready = threading.Event() self.ready = threading.Event()
@ -262,20 +254,10 @@ class IMAP4(object):
that state-changing commands will both block until previous commands that state-changing commands will both block until previous commands
have completed, and block subsequent commands until they have finished. have completed, and block subsequent commands until they have finished.
All (non-callback) arguments to commands are converted to strings, All (non-callback) string arguments to commands are converted to bytes,
except for AUTHENTICATE, and the last argument to APPEND which is except for AUTHENTICATE, and the last argument to APPEND which is
passed as an IMAP4 literal. If necessary (the string contains any passed as an IMAP4 literal. NB: the 'password' argument to the LOGIN
non-printing characters or white-space and isn't enclosed with command is always quoted.
either parentheses or double or single quotes) each string is
quoted. However, the 'password' argument to the LOGIN command is
always quoted. If you want to avoid having an argument string
quoted (eg: the 'flags' argument to STORE) then enclose the string
in parentheses (eg: "(\Deleted)"). If you are using "sequence sets"
containing the wildcard character '*', then enclose the argument
in single quotes: the quotes will be removed and the resulting
string passed unquoted. Note also that you can pass in an argument
with a type that doesn't evaluate to 'string_types' (eg: 'bytearray')
and it will be converted to a string without quoting.
There is one instance variable, 'state', that is useful for tracking There is one instance variable, 'state', that is useful for tracking
whether the client needs to login to the server. If it has the whether the client needs to login to the server. If it has the
@ -302,11 +284,10 @@ class IMAP4(object):
_literal = br'.*{(?P<size>\d+)}$' _literal = br'.*{(?P<size>\d+)}$'
_untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?' _untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'
continuation_cre = re.compile(r'\+( (?P<data>.*))?') continuation_cre = re.compile(br'\+( (?P<data>.*))?')
mapCRLF_cre = re.compile(r'\r\n|\r|\n') mapCRLF_cre = re.compile(br'\r\n|\r|\n')
mustquote_cre = re.compile(r"[^!#$&'+,./0-9:;<=>?@A-Z\[^_`a-z|}~-]") response_code_cre = re.compile(br'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
response_code_cre = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]') untagged_response_cre = re.compile(br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
untagged_response_cre = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
def __init__(self, host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None): def __init__(self, host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None):
@ -334,9 +315,9 @@ class IMAP4(object):
self.tagnum = 0 self.tagnum = 0
self.tagpre = Int2AP(random.randint(4096, 65535)) self.tagpre = Int2AP(random.randint(4096, 65535))
self.tagre = re.compile(r'(?P<tag>' self.tagre = re.compile(br'(?P<tag>'
+ self.tagpre + self.tagpre
+ r'\d+) (?P<type>[A-Z]+) ?(?P<data>.*)') + br'\d+) (?P<type>[A-Z]+) (?P<data>.*)', re.ASCII)
self._mode_ascii() self._mode_ascii()
@ -401,10 +382,7 @@ class IMAP4(object):
else: else:
raise self.error('unrecognised server welcome message: %s' % repr(self.welcome)) raise self.error('unrecognised server welcome message: %s' % repr(self.welcome))
typ, dat = self.capability() self._get_capabilities()
if dat == [None]:
raise self.error('no CAPABILITY response from server')
self.capabilities = tuple(dat[-1].upper().split())
if __debug__: self._log(1, 'CAPABILITY: %r' % (self.capabilities,)) if __debug__: self._log(1, 'CAPABILITY: %r' % (self.capabilities,))
for version in AllowedVersions: for version in AllowedVersions:
@ -426,26 +404,28 @@ class IMAP4(object):
raise AttributeError("Unknown IMAP4 command: '%s'" % attr) raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
def __enter__(self):
return self
def __exit__(self, *args):
try:
self.logout()
except OSError:
pass
def _mode_ascii(self): def _mode_ascii(self):
self.utf8_enabled = False self.utf8_enabled = False
self._encoding = 'ascii' self._encoding = 'ascii'
if bytes != str:
self.literal_cre = re.compile(self._literal, re.ASCII) self.literal_cre = re.compile(self._literal, re.ASCII)
self.untagged_status_cre = re.compile(self._untagged_status, re.ASCII) self.untagged_status_cre = re.compile(self._untagged_status, re.ASCII)
else:
self.literal_cre = re.compile(self._literal)
self.untagged_status_cre = re.compile(self._untagged_status)
def _mode_utf8(self): def _mode_utf8(self):
self.utf8_enabled = True self.utf8_enabled = True
self._encoding = 'utf-8' self._encoding = 'utf-8'
if bytes != str:
self.literal_cre = re.compile(self._literal) self.literal_cre = re.compile(self._literal)
self.untagged_status_cre = re.compile(self._untagged_status) self.untagged_status_cre = re.compile(self._untagged_status)
else:
self.literal_cre = re.compile(self._literal, re.UNICODE)
self.untagged_status_cre = re.compile(self._untagged_status, re.UNICODE)
@ -469,34 +449,7 @@ class IMAP4(object):
"""open_socket() """open_socket()
Open socket choosing first address family available.""" Open socket choosing first address family available."""
msg = (-1, 'could not open socket') return socket.create_connection((self.host, self.port))
for res in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
try:
s = socket.socket(af, socktype, proto)
except socket.error as m:
msg = m
continue
try:
for i in (0, 1):
try:
s.connect(sa)
break
except socket.error as m:
msg = m
if len(msg.args) < 2 or msg.args[0] != errno.EINTR:
raise
else:
raise socket.error(msg)
except socket.error as m:
msg = m
s.close()
continue
break
else:
raise socket.error(msg)
return s
def ssl_wrap_socket(self): def ssl_wrap_socket(self):
@ -599,9 +552,6 @@ class IMAP4(object):
data = self.compressor.compress(data) data = self.compressor.compress(data)
data += self.compressor.flush(zlib.Z_SYNC_FLUSH) data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
if bytes != str:
data = bytes(data, 'ASCII')
self.sock.sendall(data) self.sock.sendall(data)
@ -706,6 +656,8 @@ class IMAP4(object):
date_time = Time2Internaldate(date_time) date_time = Time2Internaldate(date_time)
else: else:
date_time = None date_time = None
if isinstance(message, str):
message = bytes(message, 'ASCII')
literal = self.mapCRLF_cre.sub(CRLF, message) literal = self.mapCRLF_cre.sub(CRLF, message)
if self.utf8_enabled: if self.utf8_enabled:
literal = b'UTF8 (' + literal + b')' literal = b'UTF8 (' + literal + b')'
@ -728,10 +680,11 @@ class IMAP4(object):
data = authobject(response) data = authobject(response)
It will be called to process server continuation responses. It will be called to process server continuation responses,
It should return data that will be encoded and sent to server. the 'response' argument will be a 'bytes'. It should return
It should return None if the client abort response '*' should bytes that will be encoded and sent to server. It should
be sent instead.""" return None if the client abort response '*' should be sent
instead."""
self.literal = _Authenticator(authobject).process self.literal = _Authenticator(authobject).process
try: try:
@ -970,7 +923,7 @@ class IMAP4(object):
def _CRAM_MD5_AUTH(self, challenge): def _CRAM_MD5_AUTH(self, challenge):
"""Authobject to use with CRAM-MD5 authentication.""" """Authobject to use with CRAM-MD5 authentication."""
import hmac import hmac
pwd = (self.password.encode('ASCII') if isinstance(self.password, str) pwd = (self.password.encode('utf-8') if isinstance(self.password, str)
else self.password) else self.password)
return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest() return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest()
@ -1206,10 +1159,7 @@ class IMAP4(object):
self.rdth.setDaemon(True) self.rdth.setDaemon(True)
self.rdth.start() self.rdth.start()
typ, dat = self.capability() self._get_capabilities()
if dat == [None]:
raise self.error('no CAPABILITY response from server')
self.capabilities = tuple(dat[-1].upper().split())
self._tls_established = True self._tls_established = True
@ -1305,7 +1255,7 @@ class IMAP4(object):
# Append new 'dat' to end of last untagged response if same 'typ', # Append new 'dat' to end of last untagged response if same 'typ',
# else append new response. # else append new response.
if dat is None: dat = '' if dat is None: dat = b''
self.commands_lock.acquire() self.commands_lock.acquire()
@ -1324,38 +1274,19 @@ class IMAP4(object):
self.commands_lock.release() self.commands_lock.release()
if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%.80s"]' % (typ, len(urd)-1, dat)) if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%.80r"]' % (typ, len(urd)-1, dat))
def _check_bye(self): def _check_bye(self):
bye = self._get_untagged_response('BYE', leave=True) bye = self._get_untagged_response('BYE', leave=True)
if bye: if bye:
if str != bytes:
raise self.abort(bye[-1].decode('ASCII', 'replace')) raise self.abort(bye[-1].decode('ASCII', 'replace'))
else:
raise self.abort(bye[-1])
def _checkquote(self, arg):
# Must quote command args if "atom-specials" present,
# and not already quoted. NB: single quotes are removed.
if not isinstance(arg, string_types):
return arg
if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
return arg
if len(arg) >= 2 and (arg[0],arg[-1]) in (("'","'"),):
return arg[1:-1]
if arg and self.mustquote_cre.search(arg) is None:
return arg
return self._quote(arg)
def _choose_nonull_or_dflt(self, dflt, *args): def _choose_nonull_or_dflt(self, dflt, *args):
if isinstance(dflt, string_types): if isinstance(dflt, str):
dflttyp = string_types # Allow any string type dflttyp = str # Allow any string type
else: else:
dflttyp = type(dflt) dflttyp = type(dflt)
for arg in args: for arg in args:
@ -1400,8 +1331,8 @@ class IMAP4(object):
if self.state not in Commands[name][CMD_VAL_STATES]: if self.state not in Commands[name][CMD_VAL_STATES]:
self.literal = None self.literal = None
raise self.error('command %s illegal in state %s, only allowed in states %s' raise self.error('command %s illegal in state %s'
% (name, self.state, ', '.join(Commands[name][CMD_VAL_STATES]))) % (name, self.state))
self._check_bye() self._check_bye()
@ -1423,23 +1354,26 @@ class IMAP4(object):
rqb = self._request_push(name=name, **kw) rqb = self._request_push(name=name, **kw)
data = '%s %s' % (rqb.tag, name) name = bytes(name, self._encoding)
data = rqb.tag + b' ' + name
for arg in args: for arg in args:
if arg is None: continue if arg is None: continue
data = '%s %s' % (data, self._checkquote(arg)) if isinstance(arg, str):
arg = bytes(arg, self._encoding)
data = data + b' ' + arg
literal = self.literal literal = self.literal
if literal is not None: if literal is not None:
self.literal = None self.literal = None
if isinstance(literal, string_types): if type(literal) is type(self._command):
literator = None
data = '%s {%s}' % (data, len(literal))
else:
literator = literal literator = literal
else:
literator = None
data = data + bytes(' {%s}' % len(literal), self._encoding)
if __debug__: self._log(4, 'data=%s' % data) if __debug__: self._log(4, 'data=%r' % data)
rqb.data = '%s%s' % (data, CRLF) rqb.data = data + CRLF
if literal is None: if literal is None:
self.ouq.put(rqb) self.ouq.put(rqb)
@ -1454,7 +1388,7 @@ class IMAP4(object):
# Wait for continuation response # Wait for continuation response
ok, data = crqb.get_response('command: %s => %%s' % name) ok, data = crqb.get_response('command: %s => %%s' % name)
if __debug__: self._log(4, 'continuation => %s, %s' % (ok, data)) if __debug__: self._log(4, 'continuation => %s, %r' % (ok, data))
# NO/BAD response? # NO/BAD response?
@ -1477,7 +1411,7 @@ class IMAP4(object):
crqb = self._request_push(name=name, tag='continuation') crqb = self._request_push(name=name, tag='continuation')
if __debug__: self._log(4, 'write literal size %s' % len(literal)) if __debug__: self._log(4, 'write literal size %s' % len(literal))
crqb.data = '%s%s' % (literal, CRLF) crqb.data = literal + CRLF
self.ouq.put(crqb) self.ouq.put(crqb)
if literator is None: if literator is None:
@ -1514,10 +1448,7 @@ class IMAP4(object):
return return
bye = self._get_untagged_response('BYE', leave=True) bye = self._get_untagged_response('BYE', leave=True)
if bye: if bye:
if str != bytes:
rqb.abort(self.abort, bye[-1].decode('ASCII', 'replace')) rqb.abort(self.abort, bye[-1].decode('ASCII', 'replace'))
else:
rqb.abort(self.abort, bye[-1])
return return
typ, dat = response typ, dat = response
if typ == 'BAD': if typ == 'BAD':
@ -1554,11 +1485,20 @@ class IMAP4(object):
self.idle_rqb = None self.idle_rqb = None
self.idle_timeout = None self.idle_timeout = None
self.idle_lock.release() self.idle_lock.release()
irqb.data = 'DONE%s' % CRLF irqb.data = bytes('DONE', 'ASCII') + CRLF
self.ouq.put(irqb) self.ouq.put(irqb)
if __debug__: self._log(2, 'server IDLE finished') if __debug__: self._log(2, 'server IDLE finished')
def _get_capabilities(self):
typ, dat = self.capability()
if dat == [None]:
raise self.error('no CAPABILITY response from server')
dat = str(dat[-1], "ASCII")
dat = dat.upper()
self.capabilities = tuple(dat.split())
def _get_untagged_response(self, name, leave=False): def _get_untagged_response(self, name, leave=False):
self.commands_lock.acquire() self.commands_lock.acquire()
@ -1568,7 +1508,7 @@ class IMAP4(object):
if not leave: if not leave:
del self.untagged_responses[i] del self.untagged_responses[i]
self.commands_lock.release() self.commands_lock.release()
if __debug__: self._log(5, '_get_untagged_response(%s) => %.80s' % (name, dat)) if __debug__: self._log(5, '_get_untagged_response(%s) => %.80r' % (name, dat))
return dat return dat
self.commands_lock.release() self.commands_lock.release()
@ -1600,12 +1540,12 @@ class IMAP4(object):
if self._accumulated_data: if self._accumulated_data:
typ, dat = self._literal_expected typ, dat = self._literal_expected
self._append_untagged(typ, (dat, ''.join(self._accumulated_data))) self._append_untagged(typ, (dat, b''.join(self._accumulated_data)))
self._accumulated_data = [] self._accumulated_data = []
# Protocol mandates all lines terminated by CRLF # Protocol mandates all lines terminated by CRLF
resp = resp[:-2] resp = resp[:-2]
if __debug__: self._log(5, '_put_response(%s)' % resp) if __debug__: self._log(5, '_put_response(%r)' % resp)
if 'continuation' in self.tagged_commands: if 'continuation' in self.tagged_commands:
continuation_expected = True continuation_expected = True
@ -1629,12 +1569,12 @@ class IMAP4(object):
# Command completion response? # Command completion response?
if self._match(self.tagre, resp): if self._match(self.tagre, resp):
tag = self.mo.group('tag') tag = self.mo.group('tag')
typ = self.mo.group('type') typ = str(self.mo.group('type'), 'ASCII')
dat = self.mo.group('data') dat = self.mo.group('data')
if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat): if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat):
self._append_untagged(self.mo.group('type'), self.mo.group('data')) self._append_untagged(str(self.mo.group('type'), 'ASCII'), self.mo.group('data'))
if not tag in self.tagged_commands: if not tag in self.tagged_commands:
if __debug__: self._log(1, 'unexpected tagged response: %s' % resp) if __debug__: self._log(1, 'unexpected tagged response: %r' % resp)
else: else:
self._request_pop(tag, (typ, [dat])) self._request_pop(tag, (typ, [dat]))
else: else:
@ -1651,18 +1591,18 @@ class IMAP4(object):
if self._match(self.continuation_cre, resp): if self._match(self.continuation_cre, resp):
if not continuation_expected: if not continuation_expected:
if __debug__: self._log(1, "unexpected continuation response: '%s'" % resp) if __debug__: self._log(1, "unexpected continuation response: '%r'" % resp)
return return
self._request_pop('continuation', (True, self.mo.group('data'))) self._request_pop('continuation', (True, self.mo.group('data')))
return return
if __debug__: self._log(1, "unexpected response: '%s'" % resp) if __debug__: self._log(1, "unexpected response: '%r'" % resp)
return return
typ = self.mo.group('type') typ = str(self.mo.group('type'), 'ASCII')
dat = self.mo.group('data') dat = self.mo.group('data')
if dat is None: dat = '' # Null untagged response if dat is None: dat = b'' # Null untagged response
if dat2: dat = dat + ' ' + dat2 if dat2: dat = dat + b' ' + dat2
# Is there a literal to come? # Is there a literal to come?
@ -1675,7 +1615,7 @@ class IMAP4(object):
self._append_untagged(typ, dat) self._append_untagged(typ, dat)
if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat): if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat):
self._append_untagged(self.mo.group('type'), self.mo.group('data')) self._append_untagged(str(self.mo.group('type'), 'ASCII'), self.mo.group('data'))
if typ != 'OK': # NO, BYE, IDLE if typ != 'OK': # NO, BYE, IDLE
self._end_idle() self._end_idle()
@ -1690,7 +1630,7 @@ class IMAP4(object):
if typ in ('NO', 'BAD', 'BYE'): if typ in ('NO', 'BAD', 'BYE'):
if typ == 'BYE': if typ == 'BYE':
self.Terminate = True self.Terminate = True
if __debug__: self._log(1, '%s response: %s' % (typ, dat)) if __debug__: self._log(1, '%s response: %r' % (typ, dat))
def _quote(self, arg): def _quote(self, arg):
@ -1715,7 +1655,7 @@ class IMAP4(object):
need_event = False need_event = False
self.commands_lock.release() self.commands_lock.release()
if __debug__: self._log(4, '_request_pop(%s, %s) [%d] = %s' % (name, data, len(self.tagged_commands), rqb.tag)) if __debug__: self._log(4, '_request_pop(%s, %r) [%d] = %s' % (name, data, len(self.tagged_commands), rqb.tag))
rqb.deliver(data) rqb.deliver(data)
if need_event: if need_event:
@ -1756,7 +1696,7 @@ class IMAP4(object):
if not dat: if not dat:
break break
data += dat data += dat
if __debug__: self._log(4, '_untagged_response(%s, ?, %s) => %.80s' % (typ, name, data)) if __debug__: self._log(4, '_untagged_response(%s, ?, %s) => %.80r' % (typ, name, data))
return typ, data return typ, data
@ -1823,7 +1763,7 @@ class IMAP4(object):
if __debug__: self._log(1, 'inq None - terminating') if __debug__: self._log(1, 'inq None - terminating')
break break
if not isinstance(line, string_types): if not isinstance(line, bytes):
typ, val = line typ, val = line
break break
@ -1873,10 +1813,7 @@ class IMAP4(object):
} }
return ' '.join([PollErrors[s] for s in list(PollErrors.keys()) if (s & state)]) return ' '.join([PollErrors[s] for s in list(PollErrors.keys()) if (s & state)])
if bytes != str:
line_part = b'' line_part = b''
else:
line_part = ''
poll = select.poll() poll = select.poll()
@ -1913,23 +1850,14 @@ class IMAP4(object):
rxzero = 0 rxzero = 0
while True: while True:
if bytes != str:
stop = data.find(b'\n', start) stop = data.find(b'\n', start)
if stop < 0: if stop < 0:
line_part += data[start:] line_part += data[start:]
break break
stop += 1 stop += 1
line_part, start, line = \ line_part, start, line = \
b'', stop, (line_part + data[start:stop]).decode(errors='ignore') b'', stop, line_part + data[start:stop]
else: if __debug__: self._log(4, '< %r' % line)
stop = data.find('\n', start)
if stop < 0:
line_part += data[start:]
break
stop += 1
line_part, start, line = \
'', stop, line_part + data[start:stop]
if __debug__: self._log(4, '< %s' % line)
self.inq.put(line) self.inq.put(line)
if self.TerminateReader: if self.TerminateReader:
terminate = True terminate = True
@ -1960,10 +1888,7 @@ class IMAP4(object):
if __debug__: self._log(1, 'starting using select') if __debug__: self._log(1, 'starting using select')
if bytes != str:
line_part = b'' line_part = b''
else:
line_part = ''
rxzero = 0 rxzero = 0
terminate = False terminate = False
@ -1992,7 +1917,6 @@ class IMAP4(object):
rxzero = 0 rxzero = 0
while True: while True:
if bytes != str:
stop = data.find(b'\n', start) stop = data.find(b'\n', start)
if stop < 0: if stop < 0:
line_part += data[start:] line_part += data[start:]
@ -2000,15 +1924,7 @@ class IMAP4(object):
stop += 1 stop += 1
line_part, start, line = \ line_part, start, line = \
b'', stop, (line_part + data[start:stop]).decode(errors='ignore') b'', stop, (line_part + data[start:stop]).decode(errors='ignore')
else: if __debug__: self._log(4, '< %r' % line)
stop = data.find('\n', start)
if stop < 0:
line_part += data[start:]
break
stop += 1
line_part, start, line = \
'', stop, line_part + data[start:stop]
if __debug__: self._log(4, '< %s' % line)
self.inq.put(line) self.inq.put(line)
if self.TerminateReader: if self.TerminateReader:
terminate = True terminate = True
@ -2040,7 +1956,7 @@ class IMAP4(object):
try: try:
self.send(rqb.data) self.send(rqb.data)
if __debug__: self._log(4, '> %s' % rqb.data) if __debug__: self._log(4, '> %r' % rqb.data)
except: except:
reason = 'socket error: %s - %s' % sys.exc_info()[:2] reason = 'socket error: %s - %s' % sys.exc_info()[:2]
if __debug__: if __debug__:
@ -2086,7 +2002,7 @@ class IMAP4(object):
return return
t = '\n\t\t' t = '\n\t\t'
l = ['%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or '') for x in l] l = ['%s: "%s"' % (x[0], x[1][0] and b'" "'.join(x[1]) or '') for x in l]
self.debug_lock.acquire() self.debug_lock.acquire()
self._mesg('untagged responses dump:%s%s' % (t, t.join(l))) self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
self.debug_lock.release() self.debug_lock.release()
@ -2223,9 +2139,6 @@ class IMAP4_SSL(IMAP4):
data = self.compressor.compress(data) data = self.compressor.compress(data)
data += self.compressor.flush(zlib.Z_SYNC_FLUSH) data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
if bytes != str:
data = bytes(data, 'utf8')
if hasattr(self.sock, "sendall"): if hasattr(self.sock, "sendall"):
self.sock.sendall(data) self.sock.sendall(data)
else: else:
@ -2310,9 +2223,6 @@ class IMAP4_stream(IMAP4):
data = self.compressor.compress(data) data = self.compressor.compress(data)
data += self.compressor.flush(zlib.Z_SYNC_FLUSH) data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
if bytes != str:
data = bytes(data, 'utf8')
self.writefile.write(data) self.writefile.write(data)
self.writefile.flush() self.writefile.flush()
@ -2322,6 +2232,7 @@ class IMAP4_stream(IMAP4):
self.readfile.close() self.readfile.close()
self.writefile.close() self.writefile.close()
self._P.wait()
class _Authenticator(object): class _Authenticator(object):
@ -2335,7 +2246,7 @@ class _Authenticator(object):
def process(self, data, rqb): def process(self, data, rqb):
ret = self.mech(self.decode(data)) ret = self.mech(self.decode(data))
if ret is None: if ret is None:
return '*' # Abort conversation return b'*' # Abort conversation
return self.encode(ret) return self.encode(ret)
def encode(self, inp): def encode(self, inp):
@ -2347,17 +2258,16 @@ class _Authenticator(object):
# so when it gets to the end of the 8-bit input # so when it gets to the end of the 8-bit input
# there's no partial 6-bit output. # there's no partial 6-bit output.
# #
if bytes != str:
oup = b'' oup = b''
else: if isinstance(inp, str):
oup = '' inp = inp.encode('utf-8')
while inp: while inp:
if len(inp) > 48: if len(inp) > 48:
t = inp[:48] t = inp[:48]
inp = inp[48:] inp = inp[48:]
else: else:
t = inp t = inp
inp = '' inp = b''
e = binascii.b2a_base64(t) e = binascii.b2a_base64(t)
if e: if e:
oup = oup + e[:-1] oup = oup + e[:-1]
@ -2365,7 +2275,7 @@ class _Authenticator(object):
def decode(self, inp): def decode(self, inp):
if not inp: if not inp:
return '' return b''
return binascii.a2b_base64(inp) return binascii.a2b_base64(inp)
@ -2394,19 +2304,23 @@ class _IdleCont(object):
MonthNames = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', MonthNames = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
Mon2num = dict(list(zip((x for x in MonthNames[1:]), list(range(1, 13))))) Mon2num = {s.encode():n+1 for n, s in enumerate(MonthNames[1:])}
InternalDate = re.compile(r'.*INTERNALDATE "' InternalDate = re.compile(br'.*INTERNALDATE "'
r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])' br'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])' br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])' br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
r'"') br'"')
def Internaldate2Time(resp): def Internaldate2Time(resp):
"""time_tuple = Internaldate2Time(resp) """time_tuple = Internaldate2Time(resp)
Convert IMAP4 INTERNALDATE to UT."""
Parse an IMAP4 INTERNALDATE string.
Return corresponding local time. The return value is a
time.struct_time instance or None if the string has wrong format."""
mo = InternalDate.match(resp) mo = InternalDate.match(resp)
if not mo: if not mo:
@ -2426,23 +2340,11 @@ def Internaldate2Time(resp):
# INTERNALDATE timezone must be subtracted to get UT # INTERNALDATE timezone must be subtracted to get UT
zone = (zoneh*60 + zonem)*60 zone = (zoneh*60 + zonem)*60
if zonen == '-': if zonen == b'-':
zone = -zone zone = -zone
tt = (year, mon, day, hour, min, sec, -1, -1, -1) tt = (year, mon, day, hour, min, sec, -1, -1, -1)
return time.localtime(calendar.timegm(tt) - zone)
utc = time.mktime(tt)
# Following is necessary because the time module has no 'mkgmtime'.
# 'mktime' assumes arg in local timezone, so adds timezone/altzone.
lt = time.localtime(utc)
if time.daylight and lt[-1]:
zone = zone + time.altzone
else:
zone = zone + time.timezone
return time.localtime(utc - zone)
Internaldate2tuple = Internaldate2Time # (Backward compatible) Internaldate2tuple = Internaldate2Time # (Backward compatible)
@ -2451,28 +2353,48 @@ Internaldate2tuple = Internaldate2Time # (Backward compatible)
def Time2Internaldate(date_time): def Time2Internaldate(date_time):
"""'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time) """'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time)
Convert 'date_time' to IMAP4 INTERNALDATE representation."""
Convert 'date_time' to IMAP4 INTERNALDATE representation.
The date_time argument can be a number (int or float) representing
seconds since epoch (as returned by time.time()), a 9-tuple
representing local time, an instance of time.struct_time (as
returned by time.localtime()), an aware datetime instance or a
double-quoted string. In the last case, it is assumed to already
be in the correct format."""
from datetime import datetime, timezone, timedelta
if isinstance(date_time, (int, float)): if isinstance(date_time, (int, float)):
tt = time.localtime(date_time) tt = time.localtime(date_time)
elif isinstance(date_time, (tuple, time.struct_time)): elif isinstance(date_time, tuple):
tt = date_time try:
gmtoff = date_time.tm_gmtoff
except AttributeError:
if time.daylight:
dst = date_time[8]
if dst == -1:
dst = time.localtime(time.mktime(date_time))[8]
gmtoff = -(time.timezone, time.altzone)[dst]
else:
gmtoff = -time.timezone
delta = timedelta(seconds=gmtoff)
dt = datetime(*date_time[:6], tzinfo=timezone(delta))
elif isinstance(date_time, datetime):
if date_time.tzinfo is None:
raise ValueError("date_time must be aware")
dt = date_time
elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'): elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
return date_time # Assume in correct format return date_time # Assume in correct format
else: else:
raise ValueError("date_time not of a known type") raise ValueError("date_time not of a known type")
if time.daylight and tt[-1]: fmt = '"%d-{}-%Y %H:%M:%S %z"'.format(MonthNames[dt.month])
zone = -time.altzone return dt.strftime(fmt)
else:
zone = -time.timezone
return ('"%2d-%s-%04d %02d:%02d:%02d %+03d%02d"' %
((tt[2], MonthNames[tt[1]], tt[0]) + tt[3:6] +
divmod(zone//60, 60)))
FLAGS_cre = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)') FLAGS_cre = re.compile(br'.*FLAGS \((?P<flags>[^\)]*)\)')
def ParseFlags(resp): def ParseFlags(resp):
@ -2538,15 +2460,15 @@ if __name__ == '__main__':
test_seq1 = [ test_seq1 = [
('list', ('""', '""')), ('list', ('""', '""')),
('list', ('""', '%')), ('list', ('""', '"%"')),
('create', ('imaplib2_test0',)), ('create', ('imaplib2_test0',)),
('rename', ('imaplib2_test0', 'imaplib2_test1')), ('rename', ('imaplib2_test0', 'imaplib2_test1')),
('CREATE', ('imaplib2_test2',)), ('CREATE', ('imaplib2_test2',)),
('append', ('imaplib2_test2', None, None, test_mesg)), ('append', ('imaplib2_test2', None, None, test_mesg)),
('list', ('', 'imaplib2_test%')), ('list', ('""', '"imaplib2_test%"')),
('select', ('imaplib2_test2',)), ('select', ('imaplib2_test2',)),
('search', (None, 'SUBJECT', 'IMAP4 test')), ('search', (None, 'SUBJECT', '"IMAP4 test"')),
('fetch', ("'1:*'", '(FLAGS INTERNALDATE RFC822)')), ('fetch', ('1:*', '(FLAGS INTERNALDATE RFC822)')),
('store', ('1', 'FLAGS', '(\Deleted)')), ('store', ('1', 'FLAGS', '(\Deleted)')),
('namespace', ()), ('namespace', ()),
('expunge', ()), ('expunge', ()),
@ -2561,10 +2483,10 @@ if __name__ == '__main__':
('append', (None, None, None, test_mesg)), ('append', (None, None, None, test_mesg)),
('examine', ()), ('examine', ()),
('select', ()), ('select', ()),
('fetch', ("'1:*'", '(FLAGS UID)')), ('fetch', ('1:*', '(FLAGS UID)')),
('examine', ()), ('examine', ()),
('select', ()), ('select', ()),
('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')), ('uid', ('SEARCH', 'SUBJECT', '"IMAP4 test"')),
('uid', ('SEARCH', 'ALL')), ('uid', ('SEARCH', 'ALL')),
('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')), ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')),
('recent', ()), ('recent', ()),
@ -2631,8 +2553,8 @@ if __name__ == '__main__':
for cmd,args in test_seq1: for cmd,args in test_seq1:
run(cmd, args) run(cmd, args)
for ml in run('list', ('', 'imaplib2_test%'), cb=False): for ml in run('list', ('""', '"imaplib2_test%"'), cb=False):
mo = re.match(r'.*"([^"]+)"$', ml) mo = re.match(br'.*"([^"]+)"$', ml)
if mo: path = mo.group(1) if mo: path = mo.group(1)
else: path = ml.split()[-1] else: path = ml.split()[-1]
run('delete', (path,)) run('delete', (path,))