Files
mailnag/Mailnag/daemon/mailnagdaemon.py
2019-10-22 19:13:15 +02:00

293 lines
8.8 KiB
Python

# Copyright 2016 Timo Kankare <timo.kankare@iki.fi>
# Copyright 2014, 2015 Patrick Ulbrich <zulu99@gmx.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
import threading
import logging
import time
from Mailnag.common.accounts import AccountManager
from Mailnag.common.credentialstore import CredentialStore
from Mailnag.daemon.mailchecker import MailChecker
from Mailnag.daemon.mails import Memorizer
from Mailnag.daemon.idlers import IdlerRunner
from Mailnag.daemon.conntest import ConnectivityTest, TestModes
from Mailnag.common.plugins import Plugin, HookRegistry, HookTypes, MailnagController
from Mailnag.common.exceptions import InvalidOperationException
from Mailnag.common.config import read_cfg
from Mailnag.common.utils import try_call
testmode_mapping = {
'auto' : TestModes.AUTO,
'networkmanager' : TestModes.NETWORKMANAGER,
'ping' : TestModes.PING
}
class MailnagDaemon:
def __init__(self, fatal_error_handler = None, shutdown_request_handler = None):
self._cfg = None
self._fatal_error_handler = fatal_error_handler
self._shutdown_request_handler = shutdown_request_handler
self._plugins = []
self._hookreg = None
self._conntest = None
self._accounts = None
self._mailchecker = None
self._start_thread = None
self._poll_thread = None
self._poll_thread_stop = threading.Event()
self._idlrunner = None
# Lock ensures that init() and dispose()
# are non-reentrant.
self._lock = threading.Lock()
# Flag indicating complete
# daemon initialization.
self._initialized = False
self._disposed = False
# Initializes the daemon and starts checking threads.
def init(self):
with self._lock:
if self._disposed:
raise InvalidOperationException("Daemon has been disposed")
if self._initialized:
raise InvalidOperationException("Daemon has already been initialized")
self._cfg = read_cfg()
accountman = AccountManager(CredentialStore.from_string(self._cfg.get('core', 'credentialstore')))
accountman.load_from_cfg(self._cfg, enabled_only = True)
self._accounts = accountman.to_list()
self._hookreg = HookRegistry()
self._conntest = ConnectivityTest(testmode_mapping[self._cfg.get('core', 'connectivity_test')])
memorizer = Memorizer()
memorizer.load()
self._mailchecker = MailChecker(self._cfg, memorizer, self._hookreg, self._conntest)
# Note: all code following _load_plugins() should be executed
# asynchronously because the dbus plugin requires an active mainloop
# (usually started in the programs main function).
self._load_plugins(self._cfg, self._hookreg, memorizer)
# Start checking for mails asynchronously.
self._start_thread = threading.Thread(target = self._start)
self._start_thread.start()
self._initialized = True
def dispose(self):
with self._lock:
if self._disposed:
return
# Note: _disposed must be set
# before cleaning up resources
# (in case an exception occurs)
# and before unloading plugins.
# Also required by _wait_for_inet_connection().
self._disposed = True
# clean up resources
if (self._start_thread != None) and (self._start_thread.is_alive()):
self._start_thread.join()
logging.info('Starter thread exited successfully.')
if (self._poll_thread != None) and (self._poll_thread.is_alive()):
self._poll_thread_stop.set()
self._poll_thread.join()
logging.info('Polling thread exited successfully.')
if self._idlrunner != None:
self._idlrunner.dispose()
if self._accounts != None:
for acc in self._accounts:
if acc.is_open():
acc.close()
self._unload_plugins()
def is_initialized(self):
return self._initialized
def is_disposed(self):
return self._disposed
# Enforces manual mail checks
def check_for_mails(self):
# Don't allow mail checks before initialization or
# after object disposal. F.i. plugins may not be
# loaded/unloaded completely or connections may
# have been closed already.
self._ensure_valid_state()
non_idle_accounts = self._get_non_idle_accounts(self._accounts)
self._mailchecker.check(non_idle_accounts)
def _ensure_valid_state(self):
if not self._initialized:
raise InvalidOperationException(
"Daemon has not been initialized")
if self._disposed:
raise InvalidOperationException(
"Daemon has been disposed")
def _start(self):
try:
# Call Accounts-Loaded plugin hooks
for f in self._hookreg.get_hook_funcs(HookTypes.ACCOUNTS_LOADED):
try_call( lambda: f(self._accounts) )
if not self._wait_for_inet_connection():
return
# Immediate check, check *all* accounts
try:
self._mailchecker.check(self._accounts)
except:
logging.exception('Caught an exception.')
idle_accounts = self._get_idle_accounts(self._accounts)
non_idle_accounts = self._get_non_idle_accounts(self._accounts)
# start polling thread for POP3 accounts and
# IMAP accounts without idle support
if len(non_idle_accounts) > 0:
poll_interval = int(self._cfg.get('core', 'poll_interval'))
def poll_func():
try:
while True:
self._poll_thread_stop.wait(timeout = 60.0 * poll_interval)
if self._poll_thread_stop.is_set():
break
self._mailchecker.check(non_idle_accounts)
except:
logging.exception('Caught an exception.')
self._poll_thread = threading.Thread(target = poll_func)
self._poll_thread.start()
# start idler threads for IMAP accounts with idle support
if len(idle_accounts) > 0:
def sync_func(account):
try:
self._mailchecker.check([account])
except:
logging.exception('Caught an exception.')
idle_timeout = int(self._cfg.get('core', 'imap_idle_timeout'))
self._idlrunner = IdlerRunner(idle_accounts, sync_func, idle_timeout)
self._idlrunner.start()
except Exception as ex:
logging.exception('Caught an exception.')
if self._fatal_error_handler != None:
self._fatal_error_handler(ex)
def _wait_for_inet_connection(self):
if self._conntest.is_offline():
logging.info('Waiting for internet connection...')
while True:
if self._disposed: return False
if not self._conntest.is_offline(): return True
# Note: don't sleep too long
# (see timeout in mailnag.cleanup())
# ..but also don't sleep to short in case of a ping connection test.
time.sleep(3)
def _get_idle_accounts(self, accounts):
return [acc for acc in self._accounts if acc.supports_notifications()]
def _get_non_idle_accounts(self, accounts):
return [acc for acc in self._accounts if not acc.supports_notifications()]
def _load_plugins(self, cfg, hookreg, memorizer):
class MailnagController_Impl(MailnagController):
def __init__(self, daemon, memorizer, hookreg, shutdown_request_hdlr):
self._daemon = daemon
self._memorizer = memorizer
self._hookreg = hookreg
self._shutdown_request_handler = shutdown_request_hdlr
def get_hooks(self):
return self._hookreg
def shutdown(self):
if self._shutdown_request_handler != None:
self._shutdown_request_handler()
def check_for_mails(self):
self._daemon.check_for_mails()
def mark_mail_as_read(self, mail_id):
# Note: ensure_valid_state() is not really necessary here
# (the memorizer object is available in init() and dispose()),
# but better be consistent with other daemon methods.
self._daemon._ensure_valid_state()
self._memorizer.set_to_seen(mail_id)
self._memorizer.save()
controller = MailnagController_Impl(self, memorizer, hookreg, self._shutdown_request_handler)
enabled_lst = cfg.get('core', 'enabled_plugins').split(',')
enabled_lst = [s for s in [s.strip() for s in enabled_lst] if s != '']
self._plugins = Plugin.load_plugins(cfg, controller, enabled_lst)
for p in self._plugins:
try:
p.enable()
logging.info("Successfully enabled plugin '%s'." % p.get_modname())
except:
logging.error("Failed to enable plugin '%s'." % p.get_modname())
def _unload_plugins(self):
if len(self._plugins) > 0:
err = False
for p in self._plugins:
try:
p.disable()
except:
err = True
logging.error("Failed to disable plugin '%s'." % p.get_modname())
if not err:
logging.info('Plugins disabled successfully.')