fix: update version.txt for 1.26.2 release

This commit is contained in:
Hernâni Marques 2026-09-08 01:39:31 +02:00
parent 6d5adeef9c
commit fb68a1b011
No known key found for this signature in database
8 changed files with 691 additions and 0 deletions

232
mail_deliv_probe.py Executable file
View file

@ -0,0 +1,232 @@
#!/usr/bin/env python3
"""Send a tagged probe mail via sink SMTP, then IMAP-search INBOX vs Junk/spam.
SUT remains Taler mail domains elsewhere; this only exercises mon* sinks
(and optional g0v) for inbox-vs-spam visibility.
Env:
MAIL_DELIV_IMAP_PASS / MAIL_DELIV_ALERT_PASS required (shared default)
MAIL_DELIV_PASS_mon001 / _mon002 / _mon003 optional per-mailbox override
MAIL_DELIV_PROBE_FROM default mon001@hacktivism.ch
MAIL_DELIV_PROBE_TO default mon001 mon002 mon003 (keep all — odd-looking addrs)
MAIL_DELIV_PROBE_SMTP default mail.cyon.ch:587
MAIL_DELIV_IMAP_HOST default mail.cyon.ch
MAIL_DELIV_PROBE_WAIT_S default 25
MAIL_DELIV_PROBE_ALERT_TO optional extra alert recipients on spam/miss
"""
from __future__ import annotations
import imaplib
import os
import smtplib
import ssl
import sys
import time
import uuid
from datetime import datetime, timezone
from email.message import EmailMessage
from pathlib import Path
def _env(name: str, default: str = "") -> str:
return (os.environ.get(name) or default).strip()
def _pass_for(addr: str) -> str:
"""Shared pass, or MAIL_DELIV_PASS_<localpart> (e.g. MAIL_DELIV_PASS_mon002)."""
local = addr.split("@", 1)[0].replace(".", "_").replace("+", "_")
specific = _env(f"MAIL_DELIV_PASS_{local}") or _env(f"MAIL_DELIV_IMAP_PASS_{local}")
if specific:
return specific
return _env("MAIL_DELIV_IMAP_PASS") or _env("MAIL_DELIV_ALERT_PASS")
def smtp_send(host: str, port: int, user: str, pw: str, to: str, subj: str, body: str, msgid: str) -> None:
msg = EmailMessage()
msg["From"] = user
msg["To"] = to
msg["Subject"] = subj
msg["Message-ID"] = msgid
msg["X-Taler-Mon-Probe"] = msgid.strip("<>")
msg.set_content(body)
ctx = ssl.create_default_context()
with smtplib.SMTP(host, port, timeout=45) as s:
s.ehlo()
s.starttls(context=ctx)
s.ehlo()
s.login(user, pw)
s.send_message(msg)
def imap_find(host: str, user: str, pw: str, token: str) -> dict:
"""Return {folder: count} for folders where token appears in Message-ID/Subject/body."""
ctx = ssl.create_default_context()
M = imaplib.IMAP4_SSL(host, 993, ssl_context=ctx)
M.login(user, pw)
typ, bl = M.list()
folders = []
for raw in bl or []:
line = raw.decode(errors="replace")
# LIST (\HasNoChildren) "." INBOX.Junk
if '"/"' in line or ' "." ' in line or ' "."' in line:
name = line.split(' "." ')[-1].strip().strip('"')
elif ' "/" ' in line:
name = line.split(' "/" ')[-1].strip().strip('"')
else:
parts = line.rsplit(" ", 1)
name = parts[-1].strip().strip('"') if parts else ""
if name:
folders.append(name)
# ensure common names
for n in ("INBOX", "INBOX.Junk", "INBOX.spam", "Junk", "Spam"):
if n not in folders:
folders.append(n)
hits: dict[str, int] = {}
needle = token.encode()
for folder in folders:
try:
typ, _ = M.select(folder, readonly=True)
if typ != "OK":
continue
except Exception:
continue
# SEARCH TEXT is broad; prefer HEADER Message-ID
found = 0
for crit in (f'(HEADER Message-ID "{token}")', f'(HEADER X-Taler-Mon-Probe "{token}")', f'(SUBJECT "{token}")'):
try:
typ, data = M.search(None, crit)
if typ == "OK" and data and data[0]:
ids = data[0].split()
found = max(found, len(ids))
except Exception:
continue
if found:
hits[folder] = found
M.logout()
return hits
def classify(hits: dict[str, int]) -> str:
if not hits:
return "missing"
# prefer INBOX without junk
junkish = []
inboxish = []
for f, n in hits.items():
fl = f.lower()
if "junk" in fl or "spam" in fl:
junkish.append(f)
elif fl == "inbox" or fl.endswith(".inbox") or fl == "inbox.inbox":
inboxish.append(f)
elif "inbox" in fl and "junk" not in fl and "spam" not in fl and "trash" not in fl:
inboxish.append(f)
if inboxish and not junkish:
return "inbox"
if junkish and not inboxish:
return "spam"
if inboxish and junkish:
return "both"
return "other:" + ",".join(hits.keys())
def main() -> int:
frm = _env("MAIL_DELIV_PROBE_FROM", "mon001@hacktivism.ch")
# Keep mon001–003 even if one address looks odd / one pass differs.
tos = _env(
"MAIL_DELIV_PROBE_TO",
"mon001@hacktivism.ch mon002@hacktivism.ch mon003@hacktivism.ch",
).split()
pw_from = _pass_for(frm)
if not pw_from:
print("FAIL missing MAIL_DELIV_IMAP_PASS (or per-mailbox MAIL_DELIV_PASS_*)", file=sys.stderr)
return 2
smtp = _env("MAIL_DELIV_PROBE_SMTP", "mail.cyon.ch:587")
imap_host = _env("MAIL_DELIV_IMAP_HOST", "mail.cyon.ch")
wait_s = float(_env("MAIL_DELIV_PROBE_WAIT_S", "25") or "25")
host, _, port_s = smtp.partition(":")
port = int(port_s or "587")
token = uuid.uuid4().hex
msgid = f"<{token}@taler-monitoring.probe>"
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
subj = f"[taler-mon probe] {token[:12]}"
body = (
f"taler-monitoring regular mon* probe\n"
f"token={token}\n"
f"when={now}\n"
f"Hallo Hernâni, hallo Florian — Inbox-vs-Spam check.\n"
)
results = []
sent_ok = []
for to in tos:
try:
smtp_send(host, port, frm, pw_from, to, subj, body, msgid)
print(f"SENT to={to} token={token[:12]}")
sent_ok.append(to)
except Exception as e:
print(f"SEND_FAIL to={to} {type(e).__name__}: {e}")
results.append((to, "send_fail", {}))
time.sleep(max(5.0, wait_s))
rc = 0
for to in tos:
pw = _pass_for(to)
if not pw:
print(f"IMAP_SKIP to={to} no password (set MAIL_DELIV_PASS_{to.split('@')[0]})")
results.append((to, "no_pass", {}))
rc = 1
continue
try:
hits = imap_find(imap_host, to, pw, token)
kind = classify(hits)
print(f"IMAP to={to} result={kind} hits={hits}")
results.append((to, kind, hits))
if kind != "inbox":
rc = 1
except Exception as e:
# Keep name in rotation; soft note — odd addr / wrong pass still listed
print(f"IMAP_FAIL to={to} {type(e).__name__}: {e}")
results.append((to, "imap_fail", {}))
rc = 1
# optional notify alert recipients if any non-inbox
alert_to = _env("MAIL_DELIV_PROBE_ALERT_TO") or _env("MAIL_DELIV_ALERT_TO")
bad = [r for r in results if r[1] not in ("inbox",)]
if alert_to and bad:
summary = "\n".join(f"- {t}: {k} {h}" for t, k, h in results)
for ato in alert_to.split():
try:
smtp_send(
host,
port,
frm,
pw_from,
ato,
f"[taler-mon probe] spam/miss {token[:12]}",
f"Probe issues:\n{summary}\n",
f"<{uuid.uuid4().hex}@taler-monitoring.alert>",
)
print(f"ALERT → {ato}")
except Exception as e:
print(f"ALERT_FAIL → {ato} {e}")
# stamp state
state = Path(
_env(
"TALER_MONITORD_STATE_DIR",
str(Path.home() / ".local/state/taler-monitord"),
)
)
state.mkdir(parents=True, exist_ok=True)
(state / "mail-deliv-probe.last").write_text(
f"ts={now}\ntoken={token}\nrc={rc}\n" + "\n".join(f"{t}\t{k}\t{h}" for t, k, h in results) + "\n",
encoding="utf-8",
)
return rc
if __name__ == "__main__":
raise SystemExit(main())