diff --git a/README.md b/README.md index a287bae..9119aa5 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,26 @@ sudo python3 -m pip install --upgrade --index-url https://test.pypi.org/simple/ ## Configuration -Create configuration file `/etc/execNotify/cfg.ini`. +Create configuration file `/etc/execNotify/cfg.ini`: -For the required fields, see [./etc/execNotify/cfg.ini.example](etc/execNotify/cfg.ini.example) +```shell +install -Dm0600 /etc/execNotify/cfg.ini << 'EOF' +[DEFAULT] + +[mail] +host = smtp.example.com +port = 465 +password = examplePassword + +from = noreply@example.com +to = me@example.com + +[file] +maildir = /home/exampleUser/mail/ +EOF +``` + +See also: [./etc/execNotify/cfg.ini.example](etc/execNotify/cfg.ini.example) ## Usage @@ -64,9 +81,6 @@ For the required fields, see [./etc/execNotify/cfg.ini.example](etc/execNotify/c Add `de-p1st-execNotify` in front of your command to execute. -You can pipe into `de-p1st-execNotify` but **not** from `de-p1st-execNotify` - as the output gets modified. - **Example:** `de-p1st-execNotify ls /non/existent/file` will mail you the exit code, stdout and stderr of `ls /non/existent/file` diff --git a/setup.cfg b/setup.cfg index 5832c8f..3b57615 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,10 +3,10 @@ [metadata] name = de.p1st.exec_notify -version = 0.1.4 +version = 0.1.5 author = Daniel Langbein author_email = daniel@systemli.org -description = Send mail with process output +description = mail notification if command fails long_description = file: README.md long_description_content_type = text/markdown url = https://codeberg.org/privacy1st/execNotify diff --git a/src/de/p1st/exec_notify/execNotify.py b/src/de/p1st/exec_notify/execNotify.py index afc8a7c..2113b33 100755 --- a/src/de/p1st/exec_notify/execNotify.py +++ b/src/de/p1st/exec_notify/execNotify.py @@ -8,6 +8,15 @@ from de.p1st.exec_notify.lib import exec, config, mail, util def main(): + """ + Usage: + de-p1st-execNotify + + with command = [ ...] + + :return: exit code of executed command + """ + if len(argv) <= 1: print('No command given to execute!', file=sys.stderr) exit(1) @@ -15,7 +24,7 @@ def main(): exit(executeCommand(argv[1:])) -def executeCommand(command: List) -> int: +def executeCommand(command: List[str]) -> int: """ Executes the given command and sends an email on failure. diff --git a/src/de/p1st/exec_notify/lib/config.py b/src/de/p1st/exec_notify/lib/config.py index 5d4ddaf..9ae3644 100644 --- a/src/de/p1st/exec_notify/lib/config.py +++ b/src/de/p1st/exec_notify/lib/config.py @@ -1,60 +1,80 @@ -import sys +import traceback +from sys import stderr from pathlib import Path, PosixPath import configparser +from typing import Tuple from de.p1st.exec_notify.lib import util -def getHostAndPort(): +class LazyConfig: + # (static) class variable + _config: configparser.ConfigParser = None + + @staticmethod + def get(section: str, key: str): + if LazyConfig._config is None: + LazyConfig._config = configparser.ConfigParser() + LazyConfig._config.read(_getCfgFile()) + return LazyConfig._config[section][key] + + +def getHostAndPort() -> Tuple[str, str]: try: - return config['mail']['host'], config['mail']['port'] + return LazyConfig.get('mail', 'host'), LazyConfig.get('mail', 'port') except Exception as e: - print(f'execNotify>> Could not read value [mail][host] from {_getCfgFile()}', file=sys.stderr) + print(f'execNotify>> Could not read value [mail][host] from {_getCfgFile()}', file=stderr) + traceback.print_exc() exit(1) -def getPassword(): +def getPassword() -> str: try: - return config['mail']['password'] + return LazyConfig.get('mail', 'password') except Exception as e: - print(f'execNotify>> Could not read value [mail][password] from {_getCfgFile()}', file=sys.stderr) + print(f'execNotify>> Could not read value [mail][password] from {_getCfgFile()}', file=stderr) + traceback.print_exc() exit(1) -def getFrom(): +def getFrom() -> str: try: - return config['mail']['from'] # used for mail login as well + return LazyConfig.get('mail', 'from') # used for mail login as well except Exception as e: - print(f'execNotify>> Could not read value [mail][from] from {_getCfgFile()}', file=sys.stderr) + print(f'execNotify>> Could not read value [mail][from] from {_getCfgFile()}', file=stderr) + traceback.print_exc() exit(1) -def getTo(): +def getTo() -> str: try: - return config['mail']['to'] + return LazyConfig.get('mail', 'to') except Exception as e: - print(f'execNotify>> Could not read value [mail][to] from {_getCfgFile()}', file=sys.stderr) + print(f'execNotify>> Could not read value [mail][to] from {_getCfgFile()}', file=stderr) + traceback.print_exc() exit(1) def getMailDir() -> Path: try: - return Path(config['file']['maildir']) + return Path(LazyConfig.get('file', 'maildir')) except Exception as e: - print(f'execNotify>> Could not read value [file][maildir] from {_getCfgFile()}', file=sys.stderr) + print(f'execNotify>> Could not read value [file][maildir] from {_getCfgFile()}', file=stderr) + traceback.print_exc() exit(1) +# +# Helper Methods +# + + def _getCfgFile() -> Path: return _getCfgDir().joinpath('cfg.ini') def _getCfgDir() -> Path: if util.isInDevelopment(): - return util.getProjectBase().joinpath('etc','execNotify') + return util.getProjectBase().joinpath('etc', 'execNotify') else: return PosixPath('/etc/execNotify/') - - -config: configparser.ConfigParser = configparser.ConfigParser() -config.read(_getCfgFile()) diff --git a/src/de/p1st/exec_notify/lib/exec.py b/src/de/p1st/exec_notify/lib/exec.py index c3bb3e5..ad3bf23 100644 --- a/src/de/p1st/exec_notify/lib/exec.py +++ b/src/de/p1st/exec_notify/lib/exec.py @@ -1,12 +1,12 @@ from typing import List -import sys +from sys import stdin import subprocess def execute(command: List[str]): """ - Run the given command in a subprocess and pass stdin (if given) to that process. - Wait for command to complete. + Runs the given command in a subprocess and passes stdin (if given) to that process. + Waits for command to complete. :param command: A command to executed as list of words, e.g. ['echo', 'Hello world!'] :return: (exit_code, stdout, stderr) @@ -14,7 +14,7 @@ def execute(command: List[str]): completed: subprocess.CompletedProcess = subprocess.run( command, - stdin=sys.stdin, + stdin=stdin, capture_output=True, check=False, text=True diff --git a/src/de/p1st/exec_notify/lib/mail.py b/src/de/p1st/exec_notify/lib/mail.py index 45da696..6291edd 100644 --- a/src/de/p1st/exec_notify/lib/mail.py +++ b/src/de/p1st/exec_notify/lib/mail.py @@ -1,4 +1,4 @@ -import sys +from sys import stderr import datetime import smtplib, ssl import socket @@ -10,9 +10,9 @@ from de.p1st.exec_notify.lib import config, util def sendMailOrWriteToFile(SUBJECT: str, BODY: str, informAboutLocalMail: bool = True): - mailDir = config.getMailDir() - if informAboutLocalMail and _localMailExists(): + mailDir = config.getMailDir() + SUBJECT=f'{SUBJECT} | UNREAD LOCAL MAIL' BODY=f'[!] Note [!]\n' \ f'There is local mail inside {mailDir} that was not delivered previously! ' \ @@ -22,8 +22,8 @@ def sendMailOrWriteToFile(SUBJECT: str, BODY: str, informAboutLocalMail: bool = try: sendMail(SUBJECT=SUBJECT, BODY=BODY) except Exception as e: - print(f'execNotify>> Could not send mail: {e}', file=sys.stderr) - print(f'execNotify>> Writing to file instead ...', file=sys.stderr) + print(f'execNotify>> Could not send mail: {e}', file=stderr) + print(f'execNotify>> Writing to file instead ...', file=stderr) # Instead, try to save the mail so that the user can read # it later when connected to this computer @@ -32,7 +32,7 @@ def sendMailOrWriteToFile(SUBJECT: str, BODY: str, informAboutLocalMail: bool = def sendMail(SUBJECT: str, BODY: str): """ - :throws Exception: If mail could not be sent + :raises Exception: If mail could not be sent """ FROM = config.getFrom() @@ -92,8 +92,10 @@ def _localMailExists(): """ :return: True if local mail exists in maildir folder. Once the mail is read the user shall delete (or move) it. """ + mailDir = config.getMailDir() if not mailDir.exists(): return False else: + # mailDir has at least one child (file/folder) return len(list(mailDir.iterdir())) > 0 diff --git a/src/de/p1st/exec_notify/lib/util.py b/src/de/p1st/exec_notify/lib/util.py index a235ec4..d378ce5 100644 --- a/src/de/p1st/exec_notify/lib/util.py +++ b/src/de/p1st/exec_notify/lib/util.py @@ -6,7 +6,7 @@ def isInDevelopment() -> bool: """ Helpful for local development where this python module is not installed. - :return: True if environment variable DE_P1ST_EXEC_NOTIFY is set. + :returns: True if environment variable DE_P1ST_EXEC_NOTIFY is set. """ return 'DE_P1ST_EXEC_NOTIFY' in os.environ @@ -18,13 +18,19 @@ def getProjectBase() -> Path: def readFirstLine(file: Path) -> str: """ :param file: Path to file - :return: first line of file + :returns: first line of file """ with open(file, "r") as f: return f.readline() def appendLinePrefix(prefix: str, s: str) -> str: + """ + Append `prefix` to the left of each line in `s` + + :returns: modified `s` + """ + if s is None or len(s) == 0: return prefix result = ''.join([prefix + line for line in s.splitlines(keepends=True)]) diff --git a/src/de/p1st/exec_notify/notify.py b/src/de/p1st/exec_notify/notify.py index 96f7700..bb2e01b 100755 --- a/src/de/p1st/exec_notify/notify.py +++ b/src/de/p1st/exec_notify/notify.py @@ -7,10 +7,16 @@ from de.p1st.exec_notify.lib import exec, config, mail def main(): """ - echo | ./notify - echo | ./notify - ./notify - ./notify ... + Send an email with some content and optionally specify an email subject. + + Usage: + echo | de-p1st-notify + + echo | de-p1st-notify + + de-p1st-notify + + de-p1st-notify """ BODY = None @@ -18,7 +24,7 @@ def main(): hostname = socket.gethostname() if len(argv) > 3: - print('execNotify>> Expected at most two arguments!') + print('execNotify>> Expected at most two arguments!', file=stderr) exit(1) if len(argv) == 2 or len(argv) == 3: subj = argv[1] @@ -31,6 +37,7 @@ def main(): BODY = stdin.read() SUBJECT = f'{hostname} | {subj}' + print(SUBJECT) print(BODY) mail.sendMailOrWriteToFile(SUBJECT=SUBJECT, BODY=BODY)