bamthomas / aioimaplib

Python asyncio IMAP4rev1 client library
GNU General Public License v3.0
133 stars 59 forks source link

STARTTLS command #1

Open bamthomas opened 8 years ago

alkim0 commented 7 years ago

Here's a monkey patch I'm using in my project to get STARTTLS working. It's confirmed to work with two different IMAP servers.

from aioimaplib import aioimaplib

# Monkey patching aioimaplib to support starttls
async def protocol_starttls(self, host, ssl_context=None):
    if 'STARTTLS' not in self.capabilities:
        aioimaplib.Abort('server does not have STARTTLS capability')
    if hasattr(self, '_tls_established') and self._tls_established:
        aioimaplib.Abort('TLS session already established')

    response = await self.execute(aioimaplib.Command(
        'STARTTLS', self.new_tag(), loop=self.loop))
    if response.result != 'OK':
        return response

    if ssl_context is None:
        ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
    sock = self.transport.get_extra_info('socket')
    sock.setblocking(True)
    sock = ssl_context.wrap_socket(sock, server_hostname=host)
    # XXX: This is kind of a hack. It works since the transport interface
    # totally encapsulates this, but in the future it might break if people
    # change the internals of the base transport.
    sock.setblocking(False)
    self.transport._sock = sock
    self._tls_established = True

    await self.capability()

    return response

async def imap_starttls(self):
    return (await asyncio.wait_for(
        self.protocol.starttls(self.host), self.timeout))

aioimaplib.IMAP4ClientProtocol.starttls = protocol_starttls
aioimaplib.IMAP4.starttls = imap_starttls

Unfortunately, I don't have time to do a formal pull request, but if someone looks into doing this in the future, hopefully this serves as a starting point. Note that this code is using python3.6 instead of python3.4.

aiosmtplib has a slightly different implementation for STARTTLS based on set_protocol (see https://github.com/cole/aiosmtplib/blob/master/src/aiosmtplib/protocol.py). But when I tried implementing that, I kept getting wrong SSL version errors, so I ended up going the wrap_socket route.

dadokkio commented 4 years ago

There is any progress on this? It would be very useful in my use case Trying to use the proposed patch I'm receiving: aioimaplib.aioimaplib.Abort: command STARTTLS illegal in state STARTED

bamthomas commented 4 years ago

hello @dadokkio I must admit that I didn't have a look at this issue.

You see this error because you must before sending STARTTLS call

await imap_client.wait_hello_from_server()

Then you'll be in NONAUTH state that is required to send STARTTLS so you can call the above patch.

dadokkio commented 4 years ago

Hi, thanks for the reply. I tried to add the wait_hello_from_server but then I'm receiving this error:

Traceback (most recent call last):
  File "manage.py", line 31, in <module>
    execute_from_command_line(sys.argv)
  File "/usr/local/lib/python3.8/site-packages/django/core/management/__init__.py", line 401, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.8/site-packages/django/core/management/__init__.py", line 395, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.8/site-packages/django/core/management/base.py", line 328, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/local/lib/python3.8/site-packages/django/core/management/base.py", line 369, in execute
    output = self.handle(*args, **options)
  File "/app/shop/management/commands/monitor.py", line 101, in handle
    asyncio.run(_check_inbox())
  File "/usr/local/lib/python3.8/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/usr/local/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File "/app/shop/management/commands/monitor.py", line 73, in _check_inbox
    yield from imap_client.starttls()
  File "/app/shop/management/commands/monitor.py", line 46, in imap_starttls
    return await asyncio.wait_for(self.protocol.starttls(self.host), self.timeout)
  File "/usr/local/lib/python3.8/asyncio/tasks.py", line 483, in wait_for
    return fut.result()
  File "/app/shop/management/commands/monitor.py", line 34, in protocol_starttls
    sock.setblocking(True)
  File "/usr/local/lib/python3.8/asyncio/trsock.py", line 197, in setblocking
    raise ValueError(
ValueError: setblocking(): transport sockets cannot be blocking

The idea is to use the integrate the tool with django managments command, and the value of sock at the error is:

<asyncio.TransportSocket fd=10, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('xxx.xxx.xxx.xxx', 41514), raddr=('xxx.xxx.xxx.xxx', 143)>

alkim0 commented 4 years ago

In case you're still having issues with this, feel free to take my code as an example: https://github.com/alkim0/mbsync-watcher/blob/master/mbsync_watcher/main.py.