Compare commits

...

2 Commits

3 changed files with 100 additions and 80 deletions

View File

@ -1 +1 @@
v20260109-13-ntfs-audit-jobname-prefix-flex
v20260112-01-synology-abb-partial-warning

View File

@ -178,8 +178,23 @@ _ABB_SUBJECT_RE = re.compile(r"\bactive\s+backup\s+for\s+business\b", re.I)
# Example (EN):
# "The backup task vSphere-Task-1 on KANTOOR-NEW has completed."
_ABB_COMPLETED_RE = re.compile(
r"\b(?:de\s+)?back-?up\s*taak\s+(?P<job>.+?)\s+op\s+(?P<host>.+?)\s+is\s+voltooid\b"
r"|\b(?:the\s+)?back-?up\s+task\s+(?P<job_en>.+?)\s+on\s+(?P<host_en>.+?)\s+(?:is\s+)?(?:completed|finished|has\s+completed)\b",
# NL examples:
# "De back-uptaak <job> op <host> is voltooid."
# "De virtuele machine back-uptaak <job> is voltooid."
# EN examples:
# "The backup task <job> on <host> has completed."
r"\b(?:de\s+)?(?:virtuele\s+machine\s+)?back-?up\s*taak\s+(?P<job>.+?)(?:\s+op\s+(?P<host>.+?))?\s+is\s+voltooid\b"
r"|\b(?:the\s+)?back-?up\s+task\s+(?P<job_en>.+?)(?:\s+on\s+(?P<host_en>.+?))?\s+(?:is\s+)?(?:completed|finished|has\s+completed)\b",
re.I,
)
_ABB_PARTIAL_RE = re.compile(
# NL examples:
# " ... is gedeeltelijk voltooid."
# EN examples:
# " ... is/has partially completed."
r"\b(?:de\s+)?(?:virtuele\s+machine\s+)?back-?up\s*taak\s+(?P<job>.+?)(?:\s+op\s+(?P<host>.+?))?\s+is\s+gedeeltelijk\s+voltooid\b"
r"|\b(?:the\s+)?back-?up\s+task\s+(?P<job_en>.+?)(?:\s+on\s+(?P<host_en>.+?))?\s+(?:has\s+)?(?:partially\s+completed|completed\s+partially|is\s+partially\s+completed)\b",
re.I,
)
@ -190,6 +205,8 @@ _ABB_FAILED_RE = re.compile(
)
_ABB_DEVICE_LIST_RE = re.compile(r"^\s*(?:Apparaatlijst|Device\s+list)\s*:\s*(?P<list>.+?)\s*$", re.I)
_ABB_DEVICE_SUCCESS_RE = re.compile(r"^\s*(?:Lijst\s+met\s+apparaten\s*\(back-?up\s+gelukt\)|List\s+of\s+devices\s*\(backup\s+succeeded\)|Device\s+list\s*\(backup\s+succeeded\))\s*:\s*(?P<list>.*)\s*$", re.I)
_ABB_DEVICE_FAILED_RE = re.compile(r"^\s*(?:Lijst\s+met\s+apparaten\s*\(back-?up\s+mislukt\)|List\s+of\s+devices\s*\(backup\s+failed\)|Device\s+list\s*\(backup\s+failed\))\s*:\s*(?P<list>.*)\s*$", re.I)
def _is_synology_active_backup_for_business(subject: str, text: str) -> bool:
@ -202,32 +219,78 @@ def _is_synology_active_backup_for_business(subject: str, text: str) -> bool:
def _parse_active_backup_for_business(subject: str, text: str) -> Tuple[bool, Dict, List[Dict]]:
haystack = f"{subject}\n{text}"
m = _ABB_COMPLETED_RE.search(haystack)
# ABB mails always contain a completion marker (completed / partially completed) in subject or body.
m = _ABB_COMPLETED_RE.search(haystack) or _ABB_PARTIAL_RE.search(haystack)
if not m:
# Not our ABB format
return False, {}, []
job_name = (m.group("job") or m.group("job_en") or "").strip()
host = (m.group("host") or m.group("host_en") or "").strip()
job_name = (m.groupdict().get("job") or m.groupdict().get("job_en") or "").strip()
host = (m.groupdict().get("host") or m.groupdict().get("host_en") or "").strip()
if not host:
m_host = re.search(r"\b(?:Van|From)\s+(?P<host>[^\r\n<]+)", text or "", re.I)
if m_host:
host = (m_host.group("host") or "").strip()
# Determine overall status:
# - Failed -> Error
# - Partially completed -> Warning
# - Otherwise -> Success
overall_status = "Success"
overall_message = "Success"
if _ABB_FAILED_RE.search(haystack):
is_failed = _ABB_FAILED_RE.search(haystack) is not None
is_partial = _ABB_PARTIAL_RE.search(haystack) is not None
if is_failed:
overall_status = "Error"
overall_message = "Failed"
elif is_partial:
overall_status = "Warning"
overall_message = "Partially completed"
# Parse device lists (newer ABB mail templates).
# NL examples:
# "Lijst met apparaten (back-up gelukt): SQL01, DC01"
# "Lijst met apparaten (back-up mislukt): DC02"
# EN examples:
# "List of devices (backup succeeded): ..."
# "List of devices (backup failed): ..."
objects: List[Dict] = []
seen: set[str] = set()
def _add_devices(raw: str, status: str) -> None:
if raw is None:
return
raw = raw.strip()
if not raw:
return
for name in [p.strip() for p in raw.split(",")]:
if not name or name in seen:
continue
seen.add(name)
objects.append({"name": name, "status": status})
# First parse explicit succeeded/failed device lists when present.
for line in (text or "").splitlines():
mm_ok = _ABB_DEVICE_SUCCESS_RE.match(line.strip())
if mm_ok:
_add_devices(mm_ok.group("list"), "Success")
mm_fail = _ABB_DEVICE_FAILED_RE.match(line.strip())
if mm_fail:
_add_devices(mm_fail.group("list"), "Error")
# Fallback: generic "Apparaatlijst:" / "Device list:" (older templates)
if not objects:
for line in (text or "").splitlines():
mm = _ABB_DEVICE_LIST_RE.match(line.strip())
if not mm:
continue
raw_list = (mm.group("list") or "").strip()
# "DC01, SQL01"
for name in [p.strip() for p in raw_list.split(",")]:
if name:
objects.append({"name": name, "status": overall_status})
_add_devices(mm.group("list"), overall_status)
result = {
result: Dict = {
"backup_software": "Synology",
"backup_type": "Active Backup for Business",
"job_name": job_name,
@ -236,73 +299,11 @@ def _parse_active_backup_for_business(subject: str, text: str) -> Tuple[bool, Di
}
# Provide a slightly nicer overall message when host is available
if host and overall_message in ("Success", "Failed"):
if host and overall_message in ("Success", "Failed", "Partially completed"):
result["overall_message"] = f"{overall_message} ({host})"
return True, result, objects
def _is_synology_active_backup(subject: str, text: str) -> bool:
# Keep matching conservative to avoid false positives.
subj = (subject or "").lower()
if "active backup" in subj:
return True
# Fallback for senders that don't include it in the subject
t = (text or "").lower()
return "active backup" in t and ("adminconsole" in t or "back-up" in t or "backup" in t)
def _is_synology_hyper_backup(subject: str, text: str) -> bool:
# Subject often does not mention Hyper Backup; body typically contains it.
s = (subject or "").lower()
t = (text or "").lower()
if "hyper backup" in s:
return True
# Dutch/English variants that appear in Hyper Backup task notifications.
if ("hyper backup" in t) and (
("taaknaam:" in t)
or ("task name:" in t)
or ("gegevensback-uptaak" in t)
or ("data backup task" in t)
):
return True
# Newer task notification variant (often used for cloud destinations like HiDrive)
# does not always include "Hyper Backup" in the subject/body but contains these fields.
if ("backup task:" in t) and ("backup destination:" in t):
return True
# Dutch task notification variant (e.g. "Uw back-uptaak ... is nu voltooid")
return ("back-uptaak:" in t) and ("back-updoel:" in t)
_HB_TASKNAME_RE = re.compile(
r"^(?:Taaknaam|Task name|Back-uptaak|Backup Task|Backup task)\s*:\s*(?P<name>.+)$",
re.I | re.M,
)
_HB_BACKUP_TASK_RE = re.compile(r"^Backup Task\s*:\s*(?P<name>.+)$", re.I | re.M)
_HB_FAILED_RE = re.compile(r"\bis\s+mislukt\b|\bhas\s+failed\b|\bfailed\b", re.I)
_HB_SUCCESS_RE = re.compile(
r"\bis\s+(?:nu\s+)?voltooid\b|\bhas\s+completed\b|\bsuccessful\b|\bgeslaagd\b",
re.I,
)
_HB_WARNING_RE = re.compile(r"\bgedeeltelijk\s+voltooid\b|\bpartially\s+completed\b|\bwarning\b|\bwaarschuwing\b", re.I)
# Synology Network Backup / R-Sync task notifications
# Example (NL):
# "Uw back-uptaak R-Sync ASP-NAS02 is nu voltooid."
# "Back-uptaak: R-Sync ASP-NAS02"
# Example (EN):
# "Your backup task R-Sync ASP-NAS02 has completed."
# "Backup task: R-Sync ASP-NAS02"
_RSYNC_MARKER_RE = re.compile(r"\br-?sync\b", re.I)
_RSYNC_TASK_RE = re.compile(r"^(?:Back-uptaak|Backup\s+task)\s*:\s*(?P<name>.+)$", re.I | re.M)
_RSYNC_FAILED_RE = re.compile(r"\bis\s+mislukt\b|\bhas\s+failed\b|\bfailed\b", re.I)
_RSYNC_WARNING_RE = re.compile(r"\bgedeeltelijk\s+voltooid\b|\bpartially\s+completed\b|\bwarning\b|\bwaarschuwing\b", re.I)
_RSYNC_SUCCESS_RE = re.compile(r"\bis\s+(?:nu\s+)?voltooid\b|\bhas\s+completed\b|\bcompleted\b|\bsuccessful\b|\bgeslaagd\b", re.I)
def _is_synology_rsync(subject: str, text: str) -> bool:

View File

@ -97,6 +97,25 @@
- Updated NTFS Auditing (Audit) subject parsing to extract the hostname/FQDN directly from the start of the subject before "file audits".
- Removed the dependency on a fixed "Bouter <host>" subject prefix, so hosts like dc01.totall-it.local and fs01.totall-it.local are recognized.
---
## v20260109-13-ntfs-audit-jobname-prefix-flex
- Updated NTFS Auditing (Audit) subject parsing to support both formats:
- Subjects starting with "Bouter <host> file audits"
- Subjects starting directly with "<host> file audits"
- Ensures both btr-dc001/btr-dc002 and dc01/fs01 style hostnames are recognized consistently.
---
## v20260112-01-synology-abb-partial-warning
- Updated the Synology Active Backup for Business parser to detect “partially completed” jobs as Warning instead of ignoring them.
- Added support for localized variants of the completion message (including cases without an explicit host in the first line).
- Improved host detection by extracting the system name from the “From” header when not present in the subject/body.
- Extended object parsing to recognize per-device backup results and correctly classify them as Success or Error.
- Fixed the ABB-specific regular expression to prevent parser failures on valid warning mails.
================================================================================================================================================
## v0.1.19
This release delivers a broad set of improvements focused on reliability, transparency, and operational control across mail processing, administrative auditing, and Run Checks workflows. The changes aim to make message handling more robust, provide better insight for administrators, and give operators clearer and more flexible control when reviewing backup runs.