Add NAS daily email report wrapper

Prompts used since the previous commit:
- Hoe regelen we het dagrapport?
- Het rapport moet per email naar mij gestuurd worden.
- hans@australius.nl
- akkoord

Special observations:
- dagelijks_overzicht.py now supports --mail-to and sends zero-message reports.
- QNAP plain deployment installs run_daily_report.sh with DAILY_REPORT_TO defaulting to hans@australius.nl.
- verplaats_log.json remains modified from the user's run and was intentionally not staged.
This commit is contained in:
2026-07-04 21:46:15 +02:00
parent 90d97efa6c
commit cfb96bc610
3 changed files with 55 additions and 16 deletions
+20 -10
View File
@@ -7,7 +7,7 @@ berichten die gisteren zijn binnengekomen, en stuurt een HTML-samenvatting
naar hans@australius.nl. naar hans@australius.nl.
Eenmalige setup: python3 dagelijks_overzicht.py --setup Eenmalige setup: python3 dagelijks_overzicht.py --setup
Handmatig testen: python3 dagelijks_overzicht.py [--gisteren | --datum 2026-07-01] Handmatig testen: python3 dagelijks_overzicht.py [--gisteren | --datum 2026-07-01] [--mail-to adres]
Plannen (macOS): zie dagelijks_overzicht.plist Plannen (macOS): zie dagelijks_overzicht.plist
""" """
@@ -27,6 +27,7 @@ from collections import defaultdict
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from imap_utils import list_folders, quote_mailbox from imap_utils import list_folders, quote_mailbox
from mail_imap_ops import fetch_body
# Mappen die NIET in het overzicht komen (systeem + inbox zelf) # Mappen die NIET in het overzicht komen (systeem + inbox zelf)
UITSLUITINGEN = { UITSLUITINGEN = {
@@ -59,6 +60,17 @@ def load_config():
} }
def arg_value(args: list[str], flag: str) -> str | None:
"""Return the value after a simple command-line flag."""
if flag not in args:
return None
idx = args.index(flag)
if idx + 1 >= len(args):
print(f"Ontbrekende waarde voor {flag}")
sys.exit(2)
return args[idx + 1]
# ── helpers ─────────────────────────────────────────────────────────────────── # ── helpers ───────────────────────────────────────────────────────────────────
def decode_hdr(value) -> str: def decode_hdr(value) -> str:
@@ -128,10 +140,11 @@ def haal_berichten_op(mail: imaplib.IMAP4_SSL, zoekdatum: date) -> dict[str, lis
status, msg_data = mail.fetch( status, msg_data = mail.fetch(
uid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])" uid, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])"
) )
if status != "OK" or not msg_data or not msg_data[0]: raw_header = fetch_body(msg_data)
if status != "OK" or raw_header is None:
continue continue
msg = email.message_from_bytes(msg_data[0][1]) msg = email.message_from_bytes(raw_header)
from_raw = decode_hdr(msg.get("From", "")) from_raw = decode_hdr(msg.get("From", ""))
subject = decode_hdr(msg.get("Subject", "(geen onderwerp)")) subject = decode_hdr(msg.get("Subject", "(geen onderwerp)"))
date_str = msg.get("Date", "") date_str = msg.get("Date", "")
@@ -232,12 +245,13 @@ def stuur_overzicht(html: str, zoekdatum: date, config: dict):
def run(): def run():
args = sys.argv[1:] args = sys.argv[1:]
cfg_dict = load_config() cfg_dict = load_config()
if mail_to := arg_value(args, "--mail-to"):
cfg_dict["to"] = mail_to
print(f"Account: {cfg_dict['username']}\n") print(f"Account: {cfg_dict['username']}\n")
# Datum bepalen # Datum bepalen
if "--datum" in args: if datum := arg_value(args, "--datum"):
idx = args.index("--datum") zoekdatum = date.fromisoformat(datum)
zoekdatum = date.fromisoformat(args[idx + 1])
else: else:
zoekdatum = date.today() - timedelta(days=1) # standaard: gisteren zoekdatum = date.today() - timedelta(days=1) # standaard: gisteren
@@ -256,10 +270,6 @@ def run():
totaal = sum(len(v) for v in berichten.values()) totaal = sum(len(v) for v in berichten.values())
print(f"{totaal} berichten gevonden in {len(berichten)} mappen.") print(f"{totaal} berichten gevonden in {len(berichten)} mappen.")
if totaal == 0:
print("Niets te sturen.")
return
html = bouw_html(berichten, zoekdatum) html = bouw_html(berichten, zoekdatum)
stuur_overzicht(html, zoekdatum, cfg_dict) stuur_overzicht(html, zoekdatum, cfg_dict)
+21 -3
View File
@@ -14,6 +14,7 @@ fi
APP_USER="${APP_USER:-mailcat}" APP_USER="${APP_USER:-mailcat}"
SERVICE_NAME="${SERVICE_NAME:-mailcat-sort-backup}" SERVICE_NAME="${SERVICE_NAME:-mailcat-sort-backup}"
ACCOUNT="${ACCOUNT:-backup}" ACCOUNT="${ACCOUNT:-backup}"
DAILY_REPORT_TO="${DAILY_REPORT_TO:-hans@australius.nl}"
REMOTE_PATH="${REMOTE_PATH:-/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin}" REMOTE_PATH="${REMOTE_PATH:-/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin}"
SERVICE_MANAGER="${SERVICE_MANAGER:-systemd}" SERVICE_MANAGER="${SERVICE_MANAGER:-systemd}"
MAILCAT_HOME="${MAILCAT_HOME:-/share/homes/$APP_USER}" MAILCAT_HOME="${MAILCAT_HOME:-/share/homes/$APP_USER}"
@@ -202,11 +203,27 @@ echo "logs: \$LOG_DIR"
exit 1 exit 1
STATUS_SCRIPT STATUS_SCRIPT
cat > /tmp/mailcat-daily-report.sh <<REPORT_SCRIPT
#!/bin/sh
set -eu
APP_DIR="$APP_DIR"
ACCOUNT="$ACCOUNT"
PYTHON="$PYTHON"
LOG_DIR="$LOG_DIR"
MAIL_TO="$DAILY_REPORT_TO"
MKDIR="$MKDIR"
"\$MKDIR" -p "\$LOG_DIR"
cd "\$APP_DIR"
"\$PYTHON" "\$APP_DIR/dagelijks_overzicht.py" --account "\$ACCOUNT" --mail-to "\$MAIL_TO" >> "\$LOG_DIR/daily_report.out" 2>&1
REPORT_SCRIPT
"$SUDO" "$INSTALL" -m 0755 /tmp/mailcat-start.sh "$APP_DIR/bin/start_mailcat.sh" "$SUDO" "$INSTALL" -m 0755 /tmp/mailcat-start.sh "$APP_DIR/bin/start_mailcat.sh"
"$SUDO" "$INSTALL" -m 0755 /tmp/mailcat-stop.sh "$APP_DIR/bin/stop_mailcat.sh" "$SUDO" "$INSTALL" -m 0755 /tmp/mailcat-stop.sh "$APP_DIR/bin/stop_mailcat.sh"
"$SUDO" "$INSTALL" -m 0755 /tmp/mailcat-status.sh "$APP_DIR/bin/status_mailcat.sh" "$SUDO" "$INSTALL" -m 0755 /tmp/mailcat-status.sh "$APP_DIR/bin/status_mailcat.sh"
"$SUDO" "$CHOWN" "$APP_USER:$APP_GROUP" "$APP_DIR/bin/start_mailcat.sh" "$APP_DIR/bin/stop_mailcat.sh" "$APP_DIR/bin/status_mailcat.sh" "$SUDO" "$INSTALL" -m 0755 /tmp/mailcat-daily-report.sh "$APP_DIR/bin/run_daily_report.sh"
"$RM" -f /tmp/mailcat-start.sh /tmp/mailcat-stop.sh /tmp/mailcat-status.sh "$SUDO" "$CHOWN" "$APP_USER:$APP_GROUP" "$APP_DIR/bin/start_mailcat.sh" "$APP_DIR/bin/stop_mailcat.sh" "$APP_DIR/bin/status_mailcat.sh" "$APP_DIR/bin/run_daily_report.sh"
"$RM" -f /tmp/mailcat-start.sh /tmp/mailcat-stop.sh /tmp/mailcat-status.sh /tmp/mailcat-daily-report.sh
else else
"$SUDO" "$INSTALL" -m 0644 "$APP_DIR/systemd/mailcat-sort.service" "/etc/systemd/system/$SERVICE_NAME.service" "$SUDO" "$INSTALL" -m 0644 "$APP_DIR/systemd/mailcat-sort.service" "/etc/systemd/system/$SERVICE_NAME.service"
"$SUDO" "$SED" -i \ "$SUDO" "$SED" -i \
@@ -229,6 +246,7 @@ echo "Create/check $APP_DIR/config.json, then run:"
if [ "$SERVICE_MANAGER" = "plain" ]; then if [ "$SERVICE_MANAGER" = "plain" ]; then
echo " sudo -u $APP_USER $APP_DIR/bin/start_mailcat.sh" echo " sudo -u $APP_USER $APP_DIR/bin/start_mailcat.sh"
echo " sudo -u $APP_USER $APP_DIR/bin/status_mailcat.sh" echo " sudo -u $APP_USER $APP_DIR/bin/status_mailcat.sh"
echo " sudo -u $APP_USER $APP_DIR/bin/run_daily_report.sh"
echo " tail -f $LOG_DIR/sort_mail_daemon.log" echo " tail -f $LOG_DIR/sort_mail_daemon.log"
else else
echo " sudo systemctl start $SERVICE_NAME.service" echo " sudo systemctl start $SERVICE_NAME.service"
@@ -239,5 +257,5 @@ REMOTE
scp "$ARCHIVE" "$REMOTE_USER@$REMOTE_HOST:/tmp/mailcat-deploy.tar.gz" scp "$ARCHIVE" "$REMOTE_USER@$REMOTE_HOST:/tmp/mailcat-deploy.tar.gz"
scp "$REMOTE_SCRIPT" "$REMOTE_USER@$REMOTE_HOST:/tmp/mailcat-remote-deploy.sh" scp "$REMOTE_SCRIPT" "$REMOTE_USER@$REMOTE_HOST:/tmp/mailcat-remote-deploy.sh"
REMOTE_COMMAND=$(printf "APP_USER=%q APP_DIR=%q SERVICE_NAME=%q ACCOUNT=%q REMOTE_PATH=%q SERVICE_MANAGER=%q MAILCAT_HOME=%q STATE_DIR=%q LOG_DIR=%q RUN_DIR=%q PID_FILE=%q PYTHON=%q SUDO=%q USERADD=%q INSTALL=%q SED=%q TAR=%q MKDIR=%q CHOWN=%q CHMOD=%q RM=%q SYSTEMCTL=%q /bin/sh /tmp/mailcat-remote-deploy.sh" "$APP_USER" "$APP_DIR" "$SERVICE_NAME" "$ACCOUNT" "$REMOTE_PATH" "$SERVICE_MANAGER" "$MAILCAT_HOME" "$STATE_DIR" "$LOG_DIR" "$RUN_DIR" "$PID_FILE" "$PYTHON" "$SUDO" "$USERADD" "$INSTALL" "$SED" "$TAR" "$MKDIR" "$CHOWN" "$CHMOD" "$RM" "$SYSTEMCTL") REMOTE_COMMAND=$(printf "APP_USER=%q APP_DIR=%q SERVICE_NAME=%q ACCOUNT=%q DAILY_REPORT_TO=%q REMOTE_PATH=%q SERVICE_MANAGER=%q MAILCAT_HOME=%q STATE_DIR=%q LOG_DIR=%q RUN_DIR=%q PID_FILE=%q PYTHON=%q SUDO=%q USERADD=%q INSTALL=%q SED=%q TAR=%q MKDIR=%q CHOWN=%q CHMOD=%q RM=%q SYSTEMCTL=%q /bin/sh /tmp/mailcat-remote-deploy.sh" "$APP_USER" "$APP_DIR" "$SERVICE_NAME" "$ACCOUNT" "$DAILY_REPORT_TO" "$REMOTE_PATH" "$SERVICE_MANAGER" "$MAILCAT_HOME" "$STATE_DIR" "$LOG_DIR" "$RUN_DIR" "$PID_FILE" "$PYTHON" "$SUDO" "$USERADD" "$INSTALL" "$SED" "$TAR" "$MKDIR" "$CHOWN" "$CHMOD" "$RM" "$SYSTEMCTL")
ssh -tt "$REMOTE_USER@$REMOTE_HOST" "$REMOTE_COMMAND" ssh -tt "$REMOTE_USER@$REMOTE_HOST" "$REMOTE_COMMAND"
+14 -3
View File
@@ -91,6 +91,14 @@ Scripts using shared IMAP folder handling:
- `dagelijks_overzicht.py` - `dagelijks_overzicht.py`
- `sort_mail_daemon.py` - `sort_mail_daemon.py`
`dagelijks_overzicht.py` current behavior:
- Builds an HTML digest of messages that arrived on the selected date, defaulting to yesterday.
- Uses central `config.py`; select the mailbox account with `--account`, for example `--account backup`.
- Sends the digest via SMTP using the selected account credentials.
- Supports `--mail-to <address>`; the NAS daily report target is `hans@australius.nl`.
- Sends the report even when zero messages are found, so scheduled runs are visible.
Shared operation helpers: Shared operation helpers:
- `mail_imap_ops.py` centralizes header decoding, FETCH payload extraction, routing fetched headers to full IMAP destinations, destination mailbox creation, and copy/delete move semantics for live sorting. - `mail_imap_ops.py` centralizes header decoding, FETCH payload extraction, routing fetched headers to full IMAP destinations, destination mailbox creation, and copy/delete move semantics for live sorting.
@@ -153,7 +161,8 @@ Remote/NAS deployment files:
- For the local NAS, use `SERVICE_MANAGER=plain`, `MAILCAT_HOME=/share/homes/mailcat`, and `PYTHON=/opt/bin/python3`. In plain mode, default paths are `APP_DIR=$MAILCAT_HOME/mailcat`, `STATE_DIR=$APP_DIR/state`, `LOG_DIR=$APP_DIR/log`, `RUN_DIR=$APP_DIR/run`, and `PID_FILE=$RUN_DIR/mailcat.pid`. - For the local NAS, use `SERVICE_MANAGER=plain`, `MAILCAT_HOME=/share/homes/mailcat`, and `PYTHON=/opt/bin/python3`. In plain mode, default paths are `APP_DIR=$MAILCAT_HOME/mailcat`, `STATE_DIR=$APP_DIR/state`, `LOG_DIR=$APP_DIR/log`, `RUN_DIR=$APP_DIR/run`, and `PID_FILE=$RUN_DIR/mailcat.pid`.
- QNAP path facts supplied by the user: `SUDO=/usr/bin/sudo`, `USERADD=/usr/local/bin/useradd`, `PYTHON=/opt/bin/python3`, `INSTALL=/usr/bin/install`, `SED=/bin/sed`, `TAR=/bin/tar`, `MKDIR=/bin/mkdir`, `CHOWN=/bin/chown`, `CHMOD=/bin/chmod`, and `RM=/bin/rm`. `nologin`, `python3`, `systemctl`, and `nohup` are not in the user's default PATH. - QNAP path facts supplied by the user: `SUDO=/usr/bin/sudo`, `USERADD=/usr/local/bin/useradd`, `PYTHON=/opt/bin/python3`, `INSTALL=/usr/bin/install`, `SED=/bin/sed`, `TAR=/bin/tar`, `MKDIR=/bin/mkdir`, `CHOWN=/bin/chown`, `CHMOD=/bin/chmod`, and `RM=/bin/rm`. `nologin`, `python3`, `systemctl`, and `nohup` are not in the user's default PATH.
- The deploy script accepts absolute tool overrides for those commands and passes them into the remote script. In plain mode, the generated start/stop/status scripts also bake in absolute `MKDIR` and `RM` paths. - The deploy script accepts absolute tool overrides for those commands and passes them into the remote script. In plain mode, the generated start/stop/status scripts also bake in absolute `MKDIR` and `RM` paths.
- In plain mode, the deploy script does not install systemd files. It writes executable scripts to `$APP_DIR/bin/start_mailcat.sh`, `$APP_DIR/bin/stop_mailcat.sh`, and `$APP_DIR/bin/status_mailcat.sh`. - In plain mode, the deploy script does not install systemd files. It writes executable scripts to `$APP_DIR/bin/start_mailcat.sh`, `$APP_DIR/bin/stop_mailcat.sh`, `$APP_DIR/bin/status_mailcat.sh`, and `$APP_DIR/bin/run_daily_report.sh`.
- `DAILY_REPORT_TO` controls the report recipient during deploy and defaults to `hans@australius.nl`. The daily report wrapper appends output to `$LOG_DIR/daily_report.out`.
- `systemd/mailcat-sort.service` is a template consumed by `deploy_vps.sh`; after placeholder replacement it runs `sort_mail_daemon.py --account backup --folder INBOX` and stores state/logs under `/var/lib/mailcat` and `/var/log/mailcat`. - `systemd/mailcat-sort.service` is a template consumed by `deploy_vps.sh`; after placeholder replacement it runs `sort_mail_daemon.py --account backup --folder INBOX` and stores state/logs under `/var/lib/mailcat` and `/var/log/mailcat`.
- `config.json` remains ignored and must be created manually on the remote host with mode `600` before starting the service. - `config.json` remains ignored and must be created manually on the remote host with mode `600` before starting the service.
@@ -255,8 +264,10 @@ Recommended next work:
- `sudo -u mailcat /share/homes/mailcat/mailcat/bin/start_mailcat.sh` - `sudo -u mailcat /share/homes/mailcat/mailcat/bin/start_mailcat.sh`
- `sudo -u mailcat /share/homes/mailcat/mailcat/bin/status_mailcat.sh` - `sudo -u mailcat /share/homes/mailcat/mailcat/bin/status_mailcat.sh`
- `tail -f /share/homes/mailcat/mailcat/log/sort_mail_daemon.log` - `tail -f /share/homes/mailcat/mailcat/log/sort_mail_daemon.log`
4. Send controlled test messages to `backup@australius.nl` for invoices, clear AI providers, a newsletter/service route, and unmatched mail; verify expected folder moves and daemon logs. 4. Test the daily report manually: `sudo -u mailcat /share/homes/mailcat/mailcat/bin/run_daily_report.sh`, then check `tail -100 /share/homes/mailcat/mailcat/log/daily_report.out` and confirm mail delivery to `hans@australius.nl`.
5. Add `List-ID` header support to mailing-list routing if live daemon tests show sender/domain matching is too coarse. 5. Add the daily report wrapper to QNAP scheduler after the manual test succeeds.
6. Send controlled test messages to `backup@australius.nl` for invoices, clear AI providers, a newsletter/service route, and unmatched mail; verify expected folder moves and daemon logs.
7. Add `List-ID` header support to mailing-list routing if live daemon tests show sender/domain matching is too coarse.
## Persistent File Rule ## Persistent File Rule