USERNAME_G = 'bob@example.com' # such as bob@example.com or example@gmail.com PASSWORD_G = 'password' # PASSWORD!!!!!!!!!!!!!!!! *********** ''' Python script (run in Py 3.3.2) to listen to IMAP-accessible mailboxes for incoming emails and other events (deletion / expunge). It keeps a log of mailbox activity. This uses push rather than polling, with IMAP IDLE. I'd call this a listener / event handler. You can use "tail -f" on the log file to perform other actions--logging activity to a database, for example. ''' import socket, ssl, _thread, threading, time, re, os, sys ''' Created by Kwynn Buess (kwynn.com) version 0.3.5 2013/06/06 (June 6), 3:52am EDT (GMT -4) Do whatever you want with the code and documentation, but I would like attribution even if this is just a distant inspiration. A link back to my documentation page would be nice. Speaking of inspiration, I started in PHP with https://github.com/2naive/php-imap-idle . That code was very helpful. As you might guess from a 0.3 version, this has not been thoroughly tested yet; for one, I haven't run a single version for long periods of time. However, I'm close enough that I'm sure someone will find this useful. Also, this is my very first Python program, so don't take the nuances as gospel. More documentation at http://kwynn.com/t/3/06/imap/imap_idle.html ''' PARAMS_G = [ 'imap.gmail.com', 993, # 993 = port USERNAME_G, PASSWORD_G, 'cert.pem', # you'll need to create a cert. More just below 'mailLog.txt' # the activity log file - output only, will append ] # Regarding SSL Certificates, see http://docs.python.org/3.3/library/ssl.html#self-signed-certificates # Note that you don't need to enter any real data (or maybe any at all) into the certificate. GMail isn't going to check it, at least. # ****** # As best I understand, "All Mail" includes Inbox, sent mail, and all of your labels. I am pretty sure that it does not include Trash or Spam. # The one character abbreviation is prefixed to all log entries. # Note that I have NOT extensively tested with multiple mailboxes. MAILBOX_G = [ {'box' : '"[Gmail]/All Mail"','abbr' : 'A'}, # {'box' : '"[Gmail]/Trash"' ,'abbr' : 'T'}, # {'box' : '"[Gmail]/Spam"' ,'abbr' : 'S'} ] ''' The cast of characters, in reverse order: One "main" line: for mailboxData in MAILBOX_G : startListener(mailboxData, PARAMS_G) def startListener class listenerThread - allows multiple, independent mailboxes def listen imapClass - Does the I/O with server and log file def doIdle - starts idle Thread imapIdleThread - Starts and stops IDLE every few minutes to keep connection alive. ''' def getTS() : return time.time() # I'll try to discuss the IMAP command details and log file details on my web site class imapClass: def __init__(self, mboxData, imapServer, port, username, pwd, certfileIN, logfile) : self.log = open(logfile,'a', 1) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ss = ssl.wrap_socket(s, ssl_version=ssl.PROTOCOL_TLSv1, certfile=certfileIN) ss.connect((imapServer, port)) self.cmdCnt = 0 self.readStream = ss.makefile() self.logP = mboxData['abbr'] self.sendStream = ss self.getLine() self.doCmd('LOGIN ' + username + ' ' + pwd) self.doCmd('SELECT ' + mboxData['box']) def getLine(self) : data = self.readStream.readline() if data=="" : raise ConnectionError('"Manually" raised by Kwynn\'s script: Python\'s EOF from IMAP server') data = data.strip() if data=="" : return "" # the FETCH command returns blank lines and ) These aren't useful to me. if data==')' : return "" dataOut = self.logP + ' ' + self.getTSForLog() + ' ' + self.getCount() + ' ' + data print(dataOut) sys.stdout.flush() self.log.write(dataOut + '\n') return data def getTSForLog(self) : return "{:10.6f}".format(getTS()) def getIdleLine(self) : data = 'no idle response yet' while data != '+ idling' : data = self.getLine() def doCmd(self, cmdwa) : # cmdwa - command with arguments cmdwa = cmdwa.strip() if (cmdwa == 'DONE') : line = cmdwa else : line = 'kwynn' + cmdwa[0:1] + ' ' + cmdwa line += '\r\n' cmdB = bytes(line, 'utf-8') self.sendStream.sendall(cmdB) m = re.search('^\w+', cmdwa) if m == None : print("ERROR: bad IMAP command") os._exit(2) cmd = m.group(0) if (cmd == 'LOGIN' or cmd == 'SELECT' or cmd == 'FETCH') : self.getLine() def getCount(self) : self.cmdCnt += 1 if self.cmdCnt > 9999 : self.cmdCnt = 1 cntS = str(self.cmdCnt) return cntS.zfill(4) class imapIdleThread(threading.Thread) : def __init__(self, imapO) : # imapO = imap object threading.Thread.__init__(self) self.lock = threading.Lock() self.idling = False self.timer = False self.doCmd = imapO.doCmd self.gir = imapO.getIdleLine # gir = get idle response lines from server self.startIdle(False, True) # IDLE and DONE are part of the same command and must be paired--no other commands in between. That's why I'm using locks. Otherwise, the timer noop call and the # listen() / FETCH call could conflict def startIdle(self, fromTimer=False, firstCall = False) : self.lock.acquire() # ************************** if (fromTimer == True and self.idling == False) : # if the noop timer runs and idling == False, that means listen() has found a message to process, in which case self.lock.release() # startIdle will run inside listen() and there is no need to do anything return; if (self.idling == True) : self.stopIdleInternal() if (fromTimer == True) : self.doCmd('NOOP') # Don't read responses here, or else this getLine() might not get the response over listen(), and the script will hang here waiting self.doCmd('IDLE') if (firstCall == True) : self.gir() # read this line, or else the listen() function will FETCH the message displayed on mailbox login, which is not self.idling = True # part of the goal of this script. It's safe to getIdleLine() because this thread is created before listen() runs self.lock.release() # *************************** if (self.timer != False and fromTimer == False) : self.timer.cancel() # if we just ran FETCH, no need for the previous timer and a NOOP. We'll set a new timer below. self.timer = self.getTimer() self.timer.start() def stopIdleInternal(self) : self.doCmd('DONE') self.idling = False def stopIdle(self) : if (self.timer != False) : self.timer.cancel() self.lock.acquire() # *** if (self.idling == True) : self.stopIdleInternal() self.lock.release() # *** def noop(self) : self.startIdle(True) def getTimer(self) : delay = 14 * 60 # delay = 5 # if you use delay <= 3 for testing purposes, it seems weird things happen because the commands dont' always run in time return threading.Timer(delay, self.noop) def myExit(self) : if (self.timer != False) : self.timer.cancel() _thread.exit() def doIdle(imapIN) : # imapIN = imap object idleT = imapIdleThread(imapIN) idleT.start() return idleT def listen(imap, idleT) : # imap object, idle thread data = imap.getLine() m = re.search('\* (\d+) EXISTS', data) if m == None : return msgid = m.group(1) idleT.stopIdle() fetchCmd = 'FETCH ' + msgid + ' (UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)])' imap.doCmd(fetchCmd) idleT.startIdle() class listenerThread(threading.Thread) : def __init__(self, mailboxData, args) : threading.Thread.__init__(self) self.args = args; def myInit(self) : self.imapO = imapClass(mailboxData, *self.args) self.idleT = doIdle(self.imapO) def run(self) : tempErrCnt = 0 totErrCnt = 0 lastErrTS = False self.myInit() while True : try : listen(self.imapO, self.idleT) except Exception as errV : print('ERROR: local exception. Message = ' + str(errV)) print('temp error count / total error count ' + str(tempErrCnt) + ' ' + str(totErrCnt)) delayInMin = [5/60, 1, 5, 11, 23, 61] nowErrTS = getTS() if (lastErrTS != False) : sinceLastErrInMin = (nowErrTS - lastErrTS) / 60 if (sinceLastErrInMin > delayInMin[-1]) : tempErrCnt = 0 if (tempErrCnt >= len(delayInMin)) : self.idleT.myExit() _thread.exit() return lastErrTS = nowErrTS time.sleep(delayInMin[tempErrCnt] * 60) tempErrCnt += 1 totErrCnt += 1 self.myInit() def startListener(mailboxData, args) : listenerT = listenerThread(mailboxData, args) listenerT.start() time.sleep(3) # For appearance's sake in the log only. Give each box plenty of time to finish connecting. # ******** MAIN (Py equivalent) *************** for mailboxData in MAILBOX_G : startListener(mailboxData, PARAMS_G) # MAIN *********