2

I am attempting to have a python script that constantly monitors a gmail account for new emails. Using IMAPClient, it opens two imap connections, one in idle mode. Whenever a new message is received, the connection in idle mode tells the other connection that it should fetch new mail. The non-idle connection will fetch the mail, perform some processing, then archive the email.

The problem comes when I have many emails arriving in a short period of time, more than a few in a minute. In this case, I get an AssertionError. Below is a minimal example to reproduce the error. In addition to the imap connection, it also opens an smtp connection so that it can send the emails to itself. It will usually fail with the AssertionError at some point after 5-7 emails have been sent. The AssertionError comes in the call to idle_check.

A few short comments on running the code. It does not use OAuth, and so gmail's must be set to allow less secure apps. The "username" and "password" fields at the bottom of the script must be set. The script will also archive any emails that are currently in the inbox, and so it should not be run on a primary email account.

#!/usr/bin/env python3

import smtplib
import imapclient
import email
import threading
import time

class Server(object):
    def __init__(self,username,password):
        self.username = username
        self.password = password

    def start(self):
        self.stop_running = threading.Event()
        self.has_mail = threading.Event()

        threading.Thread(target=self._idle).start()
        threading.Thread(target=self._poll).start()

        print('Listening for messages now')

    def _idle(self):
        imap_idle = self.imap_connect()

        while not self.stop_running.is_set():
            imap_idle.idle()

            for i in range(600):
                try:
                    if imap_idle.idle_check(1):
                        self.has_mail.set()
                except AssertionError as e:
                    self.stop_running.set()
                    raise

            imap_idle.idle_done()
            imap_idle.noop()

    def _poll(self):
        imap_poll = self.imap_connect()
        self.process_unread(imap_poll)

        while True:
            if self.has_mail.wait(1):
                self.has_mail.clear()
                self.process_unread(imap_poll)
            if self.stop_running.is_set():
                return

    def imap_connect(self):
        imap = imapclient.IMAPClient('imap.gmail.com',use_uid=True,ssl=True)
        imap.login(self.username,self.password)
        imap.select_folder('INBOX')
        return imap

    def process_unread(self, imap):
        imap.select_folder('INBOX')
        messages = imap.search()
        if messages:
            imap.copy(messages,'[Gmail]/All Mail')
            imap.delete_messages(messages)

    def smtp_connect(self):
        smtp = smtplib.SMTP('smtp.gmail.com',587)
        smtp.ehlo()
        smtp.starttls()
        smtp.ehlo()
        smtp.login(self.username,self.password)
        return smtp

    def send(self,recipient,subject='',body=''):
        headers = ['from: ' + self.username,
                   'subject: ' + subject,
                   'to: ' + recipient,
                   'mime-version: 1.0',
                   'content-type: text/html']
        headers = '\r\n'.join(headers)
        self.smtp_connect().sendmail(self.username,recipient,headers+'\r\n\r\n'+body)



def main():
    username = 'username@gmail.com'
    password = 'password'

    s = Server(username, password)
    s.start()

    for i in range(8):
        if s.stop_running.is_set():
            break

        print('Sending message',i)
        s.send(username,
               'Test Message'.format(i),
               'Body {}'.format(i))
        time.sleep(15)


if __name__=='__main__':
    main()

The error messsage given is as follows. The text variable at the time of error is sometimes b'XISTS' and sometimes b' FLAGS (\\Seen))'.

Exception in thread Thread-1:                                                                                                                                                          
Traceback (most recent call last):                                                                                                                                                     
  File "/usr/lib/python3.4/threading.py", line 920, in _bootstrap_inner                                                                                                                
    self.run()                                                                                                                                                                         
  File "/usr/lib/python3.4/threading.py", line 868, in run                                                                                                                             
    self._target(*self._args, **self._kwargs)                                                                                                                                          
  File "./emailer.py", line 31, in _idle                                                                                                                                               
    if imap_idle.idle_check(1):                                                                                                                                                        
  File "/path/to/the/venv/lib/python3.4/site-packages/imapclient/imapclient.py", line 519, in idle_check                                                                               
    resps.append(_parse_untagged_response(line))                                                                                                                                       
  File "/path/to/the/venv/lib/python3.4/site-packages/imapclient/imapclient.py", line 1093, in _parse_untagged_response                                                                
    assert text.startswith(b'* ')                                                                                                                                                      
AssertionError 

I am running this with Python 3.4.0, using IMAPClient 0.13. This is being run in a clean virtualenv, with no other libraries installed. Any assistance would be quite appreciated.

Eldritch Cheese
  • 1,177
  • 11
  • 21
  • What's the error? Why the oddball setup with two clients? – tripleee Sep 25 '15 at 05:04
  • The post has now been edited with the error message. I am using two clients because the processing step may take a significant amount of time. If I have a single client and take it out of idle mode, new messages may arrive while I am processing, and may therefore be ignored until another message arrives. I had read that the standard way to handle this was to have two connections, one that is always in idle mode, and one that polls whenever the idle connection receives a message. – Eldritch Cheese Sep 25 '15 at 10:54
  • Looks like both clients are sharing a single connection? Your code does not seem to support that, but the error diagnostic certainly suggests that that's what's going on here. – tripleee Sep 25 '15 at 10:58
  • I had looked through the IMAPClient code to see if that was the case, and each client makes a separate call to imaplib.IMAP4_SSL. Checking with `lsof -i`, there are two separate TCP connections that are opened, and so it doesn't seem that they share a connection. – Eldritch Cheese Sep 25 '15 at 11:17
  • Did you ever get this solved? I'm looking to do something similar and this looks like it should work. – Matthew Jul 22 '16 at 15:47
  • 1
    I did not find a full solution, only a partial workaround. If I create only a single `IMAPClient`, switching between idle and active mode as needed, then the issue does not arrive. Unfortunately, I don't know enough about the underlying imap protocol to determine whether it is an issue with `IMAPClient`, with gmail, or (most likely) an issue in the way I am using them. – Eldritch Cheese Jul 22 '16 at 17:22

0 Answers0