import os import subprocess import confparser import imapclient import contextlib import backports.ssl import socket def conf_boolean_postprocess(src): src = src.lower().strip() if src == 'true' or src == 'on' or src == '1': return True return False def conf_postprocess(conf): d = { 'imap.tls': conf_boolean_postprocess, 'imap.start_tls': conf_boolean_postprocess, 'imap.tls_nocheck_hostname': conf_boolean_postprocess, 'imap.tls_nocheck_ca': conf_boolean_postprocess, } return {key: d[key](value) if key in d else value for key, value in conf.items()} @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 = backports.ssl.CERT_NONE connection = imapclient.IMAPClient(host=conf.get('imap.server'), ssl=conf.get('imap.tls', True), ssl_context=ssl_context) 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') # TODO: replace shutdown by logouts when it will not timeout through ssl try: yield connection #connection.logout() except (connection.AbortError, socket.error, socket.timeout, backports.ssl.SSLError, backports.ssl.CertificateError): raise else: pass #connection.logout() finally: connection.shutdown() 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. ''' print('entering idle mode...') connection.idle() print('waiting for an event') try: while True: 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...') connection.idle_done() return [i for i in events if str(bytes(i[0]), encoding='ascii') != 'OK'] def main(callback=None): assert callback confpath = os.path.expanduser('~/.config/climl/climl.cfg') conf = conf_postprocess(confparser.read_conf(confpath)) 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:', password) while True: print('connection...') 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: events = imap_waiter(connection) if events: print('events:', events) print('calling callback') callback('mail') except (socket.error, socket.timeout): pass except KeyboardInterrupt: break print('end of connection')