diff --git a/.gitignore b/.gitignore
index a82ba2c..285ad65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,8 @@
-execNotifyDir/config/cfg.ini
-__pycache__/
-.idea/workspace.xml
\ No newline at end of file
+/etc/execNotify/cfg.ini
+/mail/
+
+/.idea/workspace.xml
+/__pycache__/
+
+/dist/
+/src/de_p1st_execNotify.egg-info/
diff --git a/.idea/execNotify.iml b/.idea/execNotify.iml
index 84b184a..3cb69b0 100644
--- a/.idea/execNotify.iml
+++ b/.idea/execNotify.iml
@@ -1,8 +1,13 @@
-
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index d4e4db6..14fc843 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/execNotify.xml b/.idea/runConfigurations/execNotify.xml
new file mode 100644
index 0000000..7d065f6
--- /dev/null
+++ b/.idea/runConfigurations/execNotify.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/notify.xml b/.idea/runConfigurations/notify.xml
new file mode 100644
index 0000000..f0e9c04
--- /dev/null
+++ b/.idea/runConfigurations/notify.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index e85d574..0000000
--- a/Makefile
+++ /dev/null
@@ -1,24 +0,0 @@
-.PHONY: all install clean copy remove
-
-all: install
-
-clean: remove
-
-install: permissions
-
-permissions: copy
- chmod 755 /usr/local/bin/notify
- chmod 755 /usr/local/bin/execNotify
- find /usr/local/bin/execNotifyDir \( -type d -exec chmod 755 {} + \) -o \( -type f -exec chmod 644 {} + \)
- chown root:root /usr/local/bin/execNotify
- chown -R root:root /usr/local/bin/execNotifyDir
-
-copy:
- cp notify /usr/local/bin/notify
- cp execNotify /usr/local/bin/execNotify
- cp -r execNotifyDir/ /usr/local/bin/
-
-remove:
- rm /usr/local/bin/notify
- rm /usr/local/bin/execNotify
- rm -r /usr/local/bin/execNotifyDir
diff --git a/README.md b/README.md
index f010842..c26de98 100644
--- a/README.md
+++ b/README.md
@@ -1,43 +1,88 @@
# execNotify
-Send notification on failure.
-Or send an unconditional notification.
+* Send email notification if command fails with [execNotify](src/de/p1st/exec_notify/execNotify).
+* Send unconditional notifications with [notify](src/de/p1st/exec_notify/notify).
-## setup
+## Installation
-Create a `config.ini` file inside `execNotifyDir/config`.
-
-For the required fields, see `execNotifyDir/config/config.ini.example`
-
-## installation
+Create a _venv_ and install the module from TestPyPI:
```shell
-sudo make install
+python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps de-p1st-execNotify
```
-## usage of execNotify
+The above command uses the parameter `--no-deps`:
+
+> Since TestPyPI doesn’t have the same packages as the live PyPI, it’s possible that attempting
+> to install dependencies may fail or install something unexpected. While this package
+> doesn’t have any dependencies, it’s a good practice to avoid installing dependencies when
+> using TestPyPI.
+
+## Configuration
+
+Create configuration file `/etc/execNotify/cfg.ini`.
+
+For the required fields, see [./etc/execNotify/cfg.ini.example](etc/execNotify/cfg.ini.example)
+
+
+## Usage
+
+### Usage of execNotify
Add `execNotify` in front of your command to execute.
You can pipe into `execNotify` but **not** from `execNotify`
as the output gets modified.
-### Example
+**Example:**
-For `ls /home` run `execNotify ls /home`
+`execNotify ls /non/existent/file` will mail you the exit code, stdout and stderr of `ls /non/existent/file`
-## usage of notify
+### Usage of notify
Send stdout via mail:
`echo "Hello world!" | notify`
-Or with optional subject:
+Send stdout and stderr via mail:
+
+`echo "Hello world!" 2>&1 | notify`
+
+Send stdout and specify an optional email subject:
`echo "Hello world!" | notify "someSubject"`
-Or without the pipe:
-
-`notify "someSubject" "Hello" "World!" "What's" "up?"`
+Send message without using a pipe:
`notify "someSubject" "Hello World! What's up?"`
+
+
+## Development
+
+When started with environment variable `DE_P1ST_EXEC_NOTIFY` set,
+then the configuration file is read from [./etc/execNotify/cfg.ini](etc/execNotify/cfg.ini).
+
+### Uploading to TestPyPI
+
+More detailed instructions can be found at https://packaging.python.org/tutorials/packaging-projects/
+
+0) Set up a _venv_
+1) Develop and test locally
+2) Increase/Adjust `[metadata][version]` in [setup.cfg](setup.cfg)
+3) Install the `build` and `twine` modules to your _venv_.
+4) Next generate a [distribution archive](https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives):
+
+```shell
+python3 -m build
+```
+
+5) Upload the distribution packages with twine. For the username, use `__token__`. For the password, use a
+[test.pypi.org API token](https://test.pypi.org/manage/account/#api-tokens):
+
+```shell
+python3 -m twine upload --repository testpypi dist/*
+```
+
+6) Congratulations! You can view the uploaded module under:
+
+* [https://test.pypi.org/project/de-p1st-execNotify/](https://test.pypi.org/project/de-p1st-execNotify/)
diff --git a/execNotifyDir/config/cfg.ini.example b/etc/execNotify/cfg.ini.example
similarity index 76%
rename from execNotifyDir/config/cfg.ini.example
rename to etc/execNotify/cfg.ini.example
index b6a0a8b..107cff8 100644
--- a/execNotifyDir/config/cfg.ini.example
+++ b/etc/execNotify/cfg.ini.example
@@ -9,4 +9,4 @@ from = noreply@example.com
to = me@example.com
[file]
-errorfile = /home/exampleUser/ERROR.mail
+maildir = /home/exampleUser/mail/
diff --git a/execNotify b/execNotify
deleted file mode 100755
index 8a1a0b6..0000000
--- a/execNotify
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/usr/bin/env python3
-from sys import argv
-import socket
-from typing import List
-
-from execNotifyDir import exec, config, mail
-
-
-def main():
- if len(argv) >= 2:
- if not executeCommand(argv[1:]):
- exit(1)
- exit(0)
-
-
-def executeCommand(command: List) -> bool:
- keys = ['command', 'status', 'stderr', 'stdout']
- code, stdout, stderr = exec.execute(command)
- values = [str(command), str(code), stderr, stdout]
-
- BODY = ''
- for key, value in zip(keys, values):
- BODY += '=== {} ===\n{}\n'.format(key, value)
- print(BODY)
-
- if code != 0:
- hostname = socket.gethostname()
-
- SUBJECT = '{} | {}'.format(hostname, str(command))
- mail.sendMailOrWriteToFile(SUBJECT=SUBJECT, BODY=BODY)
-
- return False
- else:
- return True
-
-
-if __name__ == '__main__':
- main()
- if mail.prevMailNotSent():
- mail.informAboutOldMail()
diff --git a/execNotifyDir/util.py b/execNotifyDir/util.py
deleted file mode 100644
index b2768e9..0000000
--- a/execNotifyDir/util.py
+++ /dev/null
@@ -1,15 +0,0 @@
-import os
-from pathlib import Path
-
-
-def getProjectBase() -> Path:
- return Path(os.path.realpath(__file__)).parent.parent
-
-
-def readFirstLine(file: Path) -> str:
- """
- :param file: Path to file
- :return: first line of file
- """
- with open(file, "r") as f:
- return f.readline()
diff --git a/notify b/notify
deleted file mode 100755
index 5f8c40c..0000000
--- a/notify
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env python3
-from sys import argv, stderr, stdin
-import socket
-from execNotifyDir import exec, config, mail
-
-
-def main():
- """
- echo
| ./notify
- echo | ./notify
- ./notify ...
- """
-
- BODY = None
- subj = None
- hostname = socket.gethostname()
-
- if len(argv) >= 2:
- subj = argv[1]
- if len(argv) >= 3:
- BODY = str(argv[2:])
- if subj is None:
- subj = "notify"
- if BODY is None:
- BODY = "=== stdin ===\n" + stdin.read()
-
- SUBJECT = "{} | {}".format(hostname, subj)
- print(BODY)
-
- mail.sendMailOrWriteToFile(SUBJECT=SUBJECT, BODY=BODY)
-
-
-if __name__ == '__main__':
- main()
- if mail.prevMailNotSent():
- mail.informAboutOldMail()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c035a4f
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,8 @@
+# https://packaging.python.org/tutorials/packaging-projects/#creating-pyproject-toml
+
+[build-system]
+requires = [
+ "setuptools>=42",
+ "wheel"
+]
+build-backend = "setuptools.build_meta"
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..78a0746
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,28 @@
+# setup.cfg is the configuration file for setuptools.
+# https://packaging.python.org/tutorials/packaging-projects/#configuring-metadata
+# https://pypi.org/classifiers/
+
+[metadata]
+name = de-p1st-execNotify
+version = 0.0.2
+author = Daniel Langbein
+author_email = daniel@systemli.org
+description = Send mail with process output
+long_description = file: README.md
+long_description_content_type = text/markdown
+url = https://codeberg.org/langfingaz/execNotify
+project_urls =
+ Bug Tracker = https://codeberg.org/langfingaz/execNotify/issues
+classifiers =
+ Programming Language :: Python :: 3
+ License :: OSI Approved :: MIT License
+ Operating System :: Unix
+
+[options]
+package_dir =
+ = src
+packages = find:
+python_requires = >=3.8
+
+[options.packages.find]
+where = src
\ No newline at end of file
diff --git a/execNotifyDir/__init__.py b/src/de/__init__.py
similarity index 100%
rename from execNotifyDir/__init__.py
rename to src/de/__init__.py
diff --git a/src/de/p1st/__init__.py b/src/de/p1st/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/de/p1st/exec_notify/__init__.py b/src/de/p1st/exec_notify/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/de/p1st/exec_notify/execNotify b/src/de/p1st/exec_notify/execNotify
new file mode 100755
index 0000000..fbc917a
--- /dev/null
+++ b/src/de/p1st/exec_notify/execNotify
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+import sys
+from sys import argv
+import socket
+from typing import List
+
+from de.p1st.exec_notify.lib import exec, config, mail
+
+
+def main():
+ if len(argv) <= 1:
+ print('No command given to execute!', file=sys.stderr)
+ exit(1)
+
+ exit(executeCommand(argv[1:]))
+
+
+def executeCommand(command: List) -> int:
+ """
+ Executes the given command and sends an email on failure.
+
+ :return: exit code of executed command
+ """
+
+ keys = ['command', 'exit code', 'stderr', 'stdout']
+ exitCode, stdout, stderr = exec.execute(command)
+ values = [str(command), str(exitCode), stderr, stdout]
+
+ BODY = ''
+ for key, value in zip(keys, values):
+ BODY += '=== {} ===\n{}\n'.format(key, value)
+ print(BODY)
+
+ if exitCode != 0:
+ hostname = socket.gethostname()
+
+ SUBJECT = '{} | {}'.format(hostname, str(command))
+ mail.sendMailOrWriteToFile(SUBJECT=SUBJECT, BODY=BODY)
+
+ return exitCode
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/de/p1st/exec_notify/lib/__init__.py b/src/de/p1st/exec_notify/lib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/execNotifyDir/config.py b/src/de/p1st/exec_notify/lib/config.py
similarity index 61%
rename from execNotifyDir/config.py
rename to src/de/p1st/exec_notify/lib/config.py
index 533ff12..22aa106 100644
--- a/execNotifyDir/config.py
+++ b/src/de/p1st/exec_notify/lib/config.py
@@ -1,7 +1,8 @@
-from pathlib import Path
+from pathlib import Path, PosixPath
import configparser
-from execNotifyDir import util
+from de.p1st.exec_notify.lib import util
+
def getHostAndPort():
return config['mail']['host'], config['mail']['port']
@@ -19,8 +20,8 @@ def getTo():
return config['mail']['to']
-def getErrorFile() -> Path:
- return Path(config['file']['errorfile'])
+def getMailDir() -> Path:
+ return Path(config['file']['maildir'])
def _getCfgFile() -> Path:
@@ -28,7 +29,11 @@ def _getCfgFile() -> Path:
def _getCfgDir() -> Path:
- return util.getProjectBase().joinpath('execNotifyDir').joinpath('config')
+ if util.isInDevelopment():
+ return util.getProjectBase().joinpath('etc','execNotify')
+ else:
+ return PosixPath('/etc/execNotify/')
+
config: configparser.ConfigParser = configparser.ConfigParser()
config.read(_getCfgFile())
diff --git a/execNotifyDir/exec.py b/src/de/p1st/exec_notify/lib/exec.py
similarity index 93%
rename from execNotifyDir/exec.py
rename to src/de/p1st/exec_notify/lib/exec.py
index 1a89044..f4cef9b 100644
--- a/execNotifyDir/exec.py
+++ b/src/de/p1st/exec_notify/lib/exec.py
@@ -9,7 +9,7 @@ def execute(command: List[str]):
Wait for command to complete.
:param command: A command to executed as list of words, e.g. ["echo", "hello"]
- :return: (status, stdout, stderr)
+ :return: (exit_code, stdout, stderr)
"""
completed: subprocess.CompletedProcess = subprocess.run(
diff --git a/execNotifyDir/mail.py b/src/de/p1st/exec_notify/lib/mail.py
similarity index 55%
rename from execNotifyDir/mail.py
rename to src/de/p1st/exec_notify/lib/mail.py
index 29c56a8..1840fd4 100644
--- a/execNotifyDir/mail.py
+++ b/src/de/p1st/exec_notify/lib/mail.py
@@ -6,15 +6,20 @@ import socket
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
-from execNotifyDir import config
+from de.p1st.exec_notify.lib import config
-def sendMailOrWriteToFile(SUBJECT: str, BODY: str):
+def sendMailOrWriteToFile(SUBJECT: str, BODY: str, informAboutLocalMail: bool = True):
+ if informAboutLocalMail and localMailExists():
+ BODY=f'[!] Note [!]\nThere is some local mail inside [file][maildir] that could not be sent previously! ' \
+ f'Please read and then delete the local mail.\n\n\n' \
+ f'{BODY}'
+
try:
sendMail(SUBJECT=SUBJECT, BODY=BODY)
except Exception as e:
- print("Error: Could not send mail! Writing to file instead ...", file=sys.stderr)
- print(e, file=sys.stderr)
+ print(f'execNotify>> Could not send mail: {e}', file=sys.stderr)
+ print(f'execNotify>> Writing to file instead ...', file=sys.stderr)
# Instead, try to save the mail so that the user can read
# it later when connected to this computer
@@ -23,7 +28,7 @@ def sendMailOrWriteToFile(SUBJECT: str, BODY: str):
def sendMail(SUBJECT: str, BODY: str):
"""
- :throws Exception: if mail could not be sent
+ :throws Exception: If mail could not be sent
"""
FROM = config.getFrom()
@@ -55,32 +60,20 @@ def saveMail(SUBJECT: str, BODY: str):
time = datetime.datetime.now()
timeStr = time.strftime('%Y%m%d_%H%M%S')
- DATE = '=' * 20 + '\n=== date ===\n' + timeStr + '\n'
- SUBJECT = '=== subject ===\n' + SUBJECT + '\n'
-
try:
+ # create parent directory if not existent
+ mailDir = config.getMailDir()
+ mailDir.mkdir(parents=True, exist_ok=True)
+
# append to file; create file if not existent
- with open(config.getErrorFile(), "a") as f:
- f.write(DATE)
- f.write(SUBJECT)
- f.write(BODY)
+ with open(mailDir.joinpath(timeStr), "a") as f:
+ f.write(f'{"=" * 20}\n=== date ===\n{timeStr}\n=== subject ===\n{SUBJECT}\n=== body ===\n{BODY}\n')
except Exception as e:
- print('Error: Could not write to file!', file=sys.stderr)
- print(e, file=sys.stderr)
+ print(f'execNotify>> Could not write to file: {e}', file=sys.stderr)
-def prevMailNotSent():
- return config.getErrorFile().exists()
-
-
-def informAboutOldMail():
+def localMailExists():
"""
- Try to inform user via mail about previous error(s) that could not be sent to him before.
- Maybe this time sending of an email works ;)
+ :return: True if local mail exists in maildir folder. Once the mail is read the user shall delete (or move) it.
"""
-
- SUBJECT = '{} | Some mails not sent!'.format(socket.gethostname())
- BODY = 'Please check the file {} for mails which could previously not be sent to you!\n' \
- 'Note: You may delete the file after reading it ;)' \
- .format(config.getErrorFile())
- sendMail(SUBJECT=SUBJECT, BODY=BODY)
+ return len(list(config.getMailDir().iterdir())) > 0
diff --git a/src/de/p1st/exec_notify/lib/util.py b/src/de/p1st/exec_notify/lib/util.py
new file mode 100644
index 0000000..3ea65a1
--- /dev/null
+++ b/src/de/p1st/exec_notify/lib/util.py
@@ -0,0 +1,24 @@
+import os
+from pathlib import Path
+
+
+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.
+ """
+ return 'DE_P1ST_EXEC_NOTIFY' in os.environ
+
+
+def getProjectBase() -> Path:
+ return Path(os.path.realpath(__file__)).parent.parent.parent.parent.parent.parent
+
+
+def readFirstLine(file: Path) -> str:
+ """
+ :param file: Path to file
+ :return: first line of file
+ """
+ with open(file, "r") as f:
+ return f.readline()
diff --git a/src/de/p1st/exec_notify/notify b/src/de/p1st/exec_notify/notify
new file mode 100755
index 0000000..453cffc
--- /dev/null
+++ b/src/de/p1st/exec_notify/notify
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+from sys import argv, stderr, stdin
+import socket
+
+from de.p1st.exec_notify.lib import exec, config, mail
+
+
+def main():
+ """
+ echo | ./notify
+ echo | ./notify
+ ./notify
+ ./notify ...
+ """
+
+ BODY = None
+ subj = None
+ hostname = socket.gethostname()
+
+ if len(argv) > 3:
+ print('execNotify>> Expected at most two arguments!')
+ exit(1)
+ if len(argv) == 2 or len(argv) == 3:
+ subj = argv[1]
+ if len(argv) == 3:
+ BODY = argv[2]
+
+ if subj is None:
+ subj = 'notify'
+ if BODY is None:
+ BODY = f'=== stdin ===\n{stdin.read()}'
+
+ SUBJECT = f'{hostname} | {subj}'
+ print(BODY)
+
+ mail.sendMailOrWriteToFile(SUBJECT=SUBJECT, BODY=BODY)
+
+
+if __name__ == '__main__':
+ main()