From c07831944cf0623fb71d9846a8c2487f8531fda7 Mon Sep 17 00:00:00 2001 From: Jerry Date: Thu, 1 Jul 2021 11:37:54 +0800 Subject: [PATCH] add smtp --- .../{config.json.example => config.json} | 0 src/pacroller/config.py | 27 +++++++++++ src/pacroller/mailer.py | 46 +++++++++++++++++++ src/pacroller/main.py | 17 +++++-- src/pacroller/smtp.json | 15 ++++++ 5 files changed, 102 insertions(+), 3 deletions(-) rename src/pacroller/{config.json.example => config.json} (100%) create mode 100644 src/pacroller/mailer.py create mode 100644 src/pacroller/smtp.json diff --git a/src/pacroller/config.json.example b/src/pacroller/config.json similarity index 100% rename from src/pacroller/config.json.example rename to src/pacroller/config.json diff --git a/src/pacroller/config.py b/src/pacroller/config.py index 13804c6..7ef1ea4 100644 --- a/src/pacroller/config.py +++ b/src/pacroller/config.py @@ -1,11 +1,13 @@ import json from pathlib import Path import importlib.util +from base64 import b64decode import sys from typing import Any CONFIG_DIR = Path('/etc/pacroller') CONFIG_FILE = 'config.json' +CONFIG_FILE_SMTP = 'smtp.json' F_KNOWN_OUTPUT_OVERRIDE = 'known_output_override.py' LIB_DIR = Path('/var/lib/pacroller') DB_FILE = 'db' @@ -21,6 +23,11 @@ if (cfg := (CONFIG_DIR / CONFIG_FILE)).exists(): else: _config = dict() +if (smtp_cfg := (CONFIG_DIR / CONFIG_FILE_SMTP)).exists(): + _smtp_config: dict = json.loads(smtp_cfg.read_text()) +else: + _smtp_config = dict() + def _import_module(fpath: Path) -> Any: spec = importlib.util.spec_from_file_location(str(fpath).removesuffix('.py').replace('/', '.'), fpath) mod = importlib.util.module_from_spec(spec) @@ -64,3 +71,23 @@ for i in NEEDRESTART_CMD: SYSTEMD = bool(_config.get('systemd-check', True)) PACMAN_SCC = bool(_config.get('clear_pkg_cache', False)) + +SMTP_ENABLED = bool(_smtp_config.get('enabled', False)) +SMTP_SSL = bool(_smtp_config.get('ssl', True)) +SMTP_HOST = _smtp_config.get('host', "") +SMTP_PORT = int(_smtp_config.get('port', 0)) +SMTP_FROM = _smtp_config.get('from', "") +SMTP_TO = _smtp_config.get('to', "") +SMTP_AUTH = dict(_smtp_config.get('auth', {})) +if SMTP_ENABLED: + assert SMTP_HOST + assert SMTP_FROM + assert SMTP_TO + assert 0 <= SMTP_PORT <= 65536 + if SMTP_AUTH: + assert SMTP_AUTH['username'] + if _smtp_auth_b64 := SMTP_AUTH.get('password_base64', ''): + SMTP_AUTH['password'] = b64decode(_smtp_auth_b64).decode('utf-8') + SMTP_AUTH.pop('password_base64') + assert SMTP_AUTH['password'] + SMTP_AUTH = {k:v for k, v in SMTP_AUTH.items() if k in {'username', 'password'}} diff --git a/src/pacroller/mailer.py b/src/pacroller/mailer.py new file mode 100644 index 0000000..02f5fe8 --- /dev/null +++ b/src/pacroller/mailer.py @@ -0,0 +1,46 @@ +import smtplib +from email.message import EmailMessage +from typing import List +import logging +from platform import node +from pacroller.config import NETWORK_RETRY, SMTP_ENABLED, SMTP_SSL, SMTP_HOST, SMTP_PORT, SMTP_FROM, SMTP_TO, SMTP_AUTH + +logger = logging.getLogger() +hostname = node() or "unknown-host" + +class MailSender: + def __init__(self) -> None: + self.host = SMTP_HOST + self.port = SMTP_PORT + self.ssl = SMTP_SSL + self.auth = SMTP_AUTH + self.mailfrom = SMTP_FROM + self.mailto = SMTP_TO.split() + self.smtp_cls = smtplib.SMTP_SSL if self.ssl else smtplib.SMTP + def send_text_plain(self, text: str, subject: str = f"pacroller from {hostname}", mailto: List[str] = list()) -> None: + if not SMTP_ENABLED: + return + for _ in range(NETWORK_RETRY): + try: + server = self.smtp_cls(self.host, self.port) + if self.auth: + server.login(self.auth["username"], self.auth["password"]) + mailto = mailto if mailto else self.mailto + msg = EmailMessage() + msg.set_content(f"from pacroller running on {hostname=}:\n\n{text}") + msg['Subject'] = subject + msg['From'] = self.mailfrom + msg['To'] = ', '.join(mailto) + server.send_message(msg) + server.quit() + except Exception: + logger.exception("error while smtp send_message") + else: + logger.debug(f"smtp sent {text=}") + break + else: + logger.error(f"unable to send email after {NETWORK_RETRY} attempts {text=}") + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(module)s - %(funcName)s - %(levelname)s - %(message)s') + MailSender().send_text_plain("This is a test mail\nIf you see this email, your smtp config is working.") diff --git a/src/pacroller/main.py b/src/pacroller/main.py index 1391da1..89c7a4a 100644 --- a/src/pacroller/main.py +++ b/src/pacroller/main.py @@ -16,6 +16,7 @@ from pacroller.config import (CONFIG_DIR, CONFIG_FILE, LIB_DIR, DB_FILE, PACMAN_ TIMEOUT, UPGRADE_TIMEOUT, NETWORK_RETRY, CUSTOM_SYNC, SYNC_SH, EXTRA_SAFE, SHELL, HOLD, NEEDRESTART, NEEDRESTART_CMD, SYSTEMD, PACMAN_PKG_DIR, PACMAN_SCC, PACMAN_DB_LCK, SAVE_STDOUT, LOG_DIR) +from pacroller.mailer import MailSender logger = logging.getLogger() @@ -279,6 +280,7 @@ def main() -> None: logger.handlers[0].setLevel(logging.INFO) locale_set() interactive = args.interactive == "on" or not (args.interactive == 'off' or not isatty(0)) + send_mail = MailSender().send_text_plain if not interactive else lambda *_, **_: None logger.debug(f"interactive questions {'enabled' if interactive else 'disabled'}") if args.action == 'run': @@ -286,26 +288,35 @@ def main() -> None: logger.error('you need to be root') exit(1) if prev_err := has_previous_error(): - logger.error(f'Cannot continue, a previous error {prev_err} is still present. Please resolve this issue and run reset.') + _err = f'Cannot continue, a previous error {prev_err} is still present. Please resolve this issue and run reset.' + logger.error(_err) + send_mail(_err) exit(2) if SYSTEMD: if _s := is_system_failed(): - logger.error(f'systemd is in {_s} state, refused') + _err = f'systemd is in {_s} state, refused' + logger.error(_err) + send_mail(_err) exit(11) if Path(PACMAN_DB_LCK).exists(): - logger.error(f'Database is locked at {PACMAN_DB_LCK}') + _err = f'Database is locked at {PACMAN_DB_LCK}' + logger.error(_err) + send_mail(_err) exit(2) try: report = do_system_upgrade(debug=args.debug, interactive=interactive) except NonFatal: + send_mail(f"NonFatal Error:\n{traceback.format_exc()}") raise except Exception as e: write_db(None, e) + send_mail(f"Fatal Error:\n{traceback.format_exc()}") raise else: exc = CheckFailed('manual inspection required') if report.failed else None write_db(report, exc) if exc: + send_mail(f"{exc}\n\n{report.summary(verbose=args.verbose, show_package=False)}") exit(2) if NEEDRESTART: run_needrestart() diff --git a/src/pacroller/smtp.json b/src/pacroller/smtp.json new file mode 100644 index 0000000..fe557d0 --- /dev/null +++ b/src/pacroller/smtp.json @@ -0,0 +1,15 @@ +{ + "enabled": false, + "ssl": true, + "host": "smtp.example.com", + "port": 465, + "from": "me@example.com", + "to": "you1@example.com you2@example.com", + "auth": { + "comment1": "## if you don't want any kind of authorization, delete the whole auth section", + "comment2": "## you can have either one of the two following password keys", + "username": "myname", + "password": "mypassword", + "password_base64": "bXlwYXNzd29yZA==" + } +}