pythonasynchronousexceptionaio

Python: Aioimaplib catch exceptions


im trying to check multiple imap login informations asynchronously with aioimaplib. This code works as long as the imap servers are reachable and / or the clients don't time out.

What is the correct way to catch the exceptions?

Example exception:

ERROR:asyncio:Task exception was never retrieved future: <Task finished coro=<BaseEventLoop.create_connection() done, defined at G:\WinPython-3.5.4\python-3.5.4.amd64\lib\asyncio\base_events.py:679> exception=TimeoutError(10060, "Connect call failed ('74.117.114.100', 993)")>

Code:

account_infos = [
    # User            Password     Server
    ('user1@web.com', 'password1', 'imap.google.com'),
    ('user2@web.com', 'password2', 'imap.yandex.com'),
    ('user3@web.com', 'password3', 'imap.server3.com'),
]


class MailLogin:
    def __init__(self):
        self.loop = asyncio.get_event_loop()
        self.queue = asyncio.Queue(loop=self.loop)
        self.max_workers = 2

    async def produce_work(self):
        for i in account_infos:
            await self.queue.put(i)
        for _ in range(max_workers):
            await self.queue.put((None, None, None))

    async def worker(self):
        while True:
            (username, password, server) = await self.queue.get()
            if username is None:
                break

            while True:
                try:
                    s = IMAP4_SSL(server)
                    await s.wait_hello_from_server()
                    r = await s.login(username, password)
                    await s.logout()
                    if r.result != 'NO':
                        print('Information works')
                except Exception as e:
                    # DOES NOT CATCH
                    print(str(e))
                else:
                    break

    def start(self):
        try:
            self.loop.run_until_complete(
                asyncio.gather(self.produce_work(), *[self.worker() for _ in range(self.max_workers)],
                               loop=self.loop, return_exceptions=True)
            )
        finally:
            print('Done')


if __name__ == '__main__':
    MailLogin().start()

Solution

  • There are several ways to do this but the TimeoutError is probably caught in your except. You don't see it because str(e) is an empty string.

    You can see the stacks enabling debug mode of asyncio.

    First, you can catch the exception as you did:

    async def fail_fun():
        try:
            imap_client = aioimaplib.IMAP4_SSL(host='foo', timeout=1)
            await imap_client.wait_hello_from_server()
        except Exception as e:
            print('Exception : ' + str(e))
    
    if __name__ == '__main__':
        get_event_loop().run_until_complete(fail_fun())
    

    Second, you can catch the exception at run_until_complete

    async def fail_fun():
        imap_client = aioimaplib.IMAP4_SSL(host='foo', timeout=1)
        await imap_client.wait_hello_from_server()
    
    if __name__ == '__main__':
        try:
            get_event_loop().run_until_complete(fail_fun())
        except Exception as e:
            print('Exception : ' + str(e))
    

    The connection is established wrapping the loop.create_connection coroutine with create_task : we wanted to establish the connection in the IMAP4 constructor and __init__ should return None.

    So if your host has a wrong value, you could test it before, or wait for the timeout :

    socket.gaierror: [Errno -5] No address associated with hostname
    

    if a host is not responding before the timeout, you can raise the timeout. And if the connection is lost during the connection, you can add a connection lost callback in the IMAP4 constructor.