import os import subprocess import imapclient import contextlib import backports.ssl as ssl import socket import time import traceback from . import interface @contextlib.contextmanager def connect_to_imap(conf, password): cafile = conf.get('imap.tls_ca', None) if cafile: cafile = os.path.expanduser(cafile) ssl_context = imapclient.create_default_context(cafile=cafile) if conf.get('imap.tls_nocheck_hostname', False): # don't check if certificate hostname doesn't match target hostname ssl_context.check_hostname = False if conf.get('imap.tls_nocheck_ca', False): # don't check if the certificate is trusted by a certificate authority ssl_context.verify_mode = ssl.CERT_NONE connection = imapclient.IMAPClient(host=conf.get('imap.server'), ssl=conf.get('imap.tls', True), ssl_context=ssl_context, use_uid=True) if conf.get('imap.start_tls', False): connection.start_tls(ssl_context=ssl_context) print('connection succeed') connection.login(username=conf.get('imap.username'), password=password) print('successfuly logged') # never try to shutdown socket or send a logout since the goal of climl is # to stay connected, if this fails it is an error and thus no need to try # to be nice with a logout to a dead connection. print('connection...') try: yield connection except: print('end of connection') raise def imap_waiter(connection): ''' from dovecot wiki: > If IDLE command is started, Dovecot never > disconnects. Only if the connection is lost there will be > a disconnection. A dead connection is detected by Dovecot > periodically sending "I'm still here" notifications to > client (imap_idle_notify_interval setting - default every > 2 minutes). > > IMAP clients are supposed to send something before 30 > minutes are up, but several clients don't do this. Some > Outlook versions even stop receiving new mails entirely > until manual intervention if IMAP server disconnects the > client. We are a compliant client. To be compatible with a lot of servers (general assumption is a 29minutes timeout), we try to redo idle-mode every 24 minutes. Each call to imap_waiter is a single idle iteration. Thus this function may return empty list. Note: on some servers, there might be events between the last search command and the begining of idle mode. If those events represent new mails, they will be checked at the idle timeout (it takes time to see them, but they are not lost). ''' print('entering idle mode...') connection.idle() print('waiting for an event') try: tenter = time.monotonic() while (time.monotonic() - tenter) <= 24*60: events = connection.idle_check(timeout=24*60) if len(events) == 1 and \ str(bytes(events[0][0]), encoding='ascii') == 'OK': # in case of dovecot 'Ok still here' keepalives, timeout did # not expired (and never will appart from a disconnection) continue else: break except KeyboardInterrupt: print('quitting idle mode on interrupt.') connection.idle_done() raise print('quitting idle mode...') command_text, idle_responses = connection.idle_done() events.extend(idle_responses) print('idle_done results:', command_text, idle_responses) return events def process_emails(connection, callback=None, maxsize=None): assert connection assert callback assert maxsize idlist = connection.search(['UNANSWERED']) print('found unprocessed idlist: ', idlist) for oneid in idlist: print('new mail:', oneid) print('checking size of mail...') response = connection.fetch([oneid], ['RFC822.SIZE']) size = int(response[oneid][b'RFC822.SIZE']) print('message size: {} bytes'.format(size)) if size > maxsize: print('message is too big, skip and mark it') connection.add_flags([oneid], ['\Answered']) continue print('getting mail...') response = connection.fetch([oneid], ['RFC822']) data = response[oneid][b'RFC822'] print('calling callback...') try: #callback('mail: ' + str(oneid) + ': ' + str(data)) callback(data) print('mark mail {}'.format(oneid)) connection.add_flags([oneid], ['\Answered']) except interface.HookAbortError: print('callback wanted to skip marking of mail {}'.format(oneid)) traceback.print_exc() data = None def main(callback=None, conf=None): assert callback assert conf print('Read conf:', conf) password_command = conf.get('imap.password_command', None) if password_command: password = subprocess.check_output(password_command, shell=True) password = password.rstrip().decode('utf8') print('got pasword: hidden') maxsize = conf.get('mail.maxsize', 100*1024) # 100k default maxsize = int(maxsize) while True: try: with connect_to_imap(conf, password) as connection: print('selecting folder', conf.get('imap.mailbox')) connection.select_folder(conf.get('imap.mailbox')) while True: process_emails(connection, callback, maxsize) imap_waiter(connection) except (socket.error, socket.timeout, ssl.SSLError, ssl.CertificateError): print('socket/ssl error, retrying in 10s...') traceback.print_exc() time.sleep(10) # wait between retries