Compare commits

..

2 Commits

3 changed files with 118 additions and 82 deletions

View File

@ -1 +1 @@
v20260112-01-synology-abb-partial-warning v20260112-02-synology-abb-subject-partial-warning

View File

@ -173,40 +173,33 @@ def _extract_totals(text: str) -> Tuple[int, int, int]:
_ABB_SUBJECT_RE = re.compile(r"\bactive\s+backup\s+for\s+business\b", re.I) _ABB_SUBJECT_RE = re.compile(r"\bactive\s+backup\s+for\s+business\b", re.I)
# Example (NL): # Examples (NL):
# "De back-uptaak vSphere-Task-1 op KANTOOR-NEW is voltooid." # "De back-uptaak vSphere-Task-1 op KANTOOR-NEW is voltooid."
# Example (EN): # "Virtuele machine back-uptaak vSphere-Task-1 op KANTOOR-NEW is gedeeltelijk voltooid."
# Examples (EN):
# "The backup task vSphere-Task-1 on KANTOOR-NEW has completed." # "The backup task vSphere-Task-1 on KANTOOR-NEW has completed."
# "Virtual machine backup task vSphere-Task-1 on KANTOOR-NEW partially completed."
_ABB_COMPLETED_RE = re.compile( _ABB_COMPLETED_RE = re.compile(
# NL examples: r"\b(?:virtuele\s+machine\s+)?(?:de\s+)?back-?up\s*taak\s+(?P<job>.+?)\s+op\s+(?P<host>[A-Za-z0-9._-]+)\s+is\s+(?P<status>voltooid|gedeeltelijk\s+voltooid)\b"
# "De back-uptaak <job> op <host> is voltooid." r"|\b(?:virtual\s+machine\s+)?(?:the\s+)?back-?up\s+task\s+(?P<job_en>.+?)\s+on\s+(?P<host_en>[A-Za-z0-9._-]+)\s+(?:is\s+)?(?P<status_en>completed|finished|has\s+completed|partially\s+completed)\b",
# "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, re.I,
) )
_ABB_FAILED_RE = re.compile( _ABB_FAILED_RE = re.compile(
r"\b(?:de\s+)?back-?up\s*taak\s+.+?\s+op\s+.+?\s+is\s+mislukt\b" r"\b(?:virtuele\s+machine\s+)?(?:de\s+)?back-?up\s*taak\s+.+?\s+op\s+.+?\s+is\s+mislukt\b"
r"|\b(?:the\s+)?back-?up\s+task\s+.+?\s+on\s+.+?\s+(?:has\s+)?failed\b", r"|\b(?:virtual\s+machine\s+)?(?:the\s+)?back-?up\s+task\s+.+?\s+on\s+.+?\s+(?:has\s+)?failed\b",
re.I, re.I,
) )
_ABB_DEVICE_LIST_RE = re.compile(r"^\s*(?:Apparaatlijst|Device\s+list)\s*:\s*(?P<list>.+?)\s*$", re.I) # Device list lines in body, e.g.
_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) # "Apparaatlijst (back-up gelukt): DC01, SQL01"
_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) # "Apparaatlijst (back-up mislukt): FS01"
# "Device list (backup succeeded): DC01, SQL01"
# "Device list (backup failed): FS01"
_ABB_DEVICE_LIST_RE = re.compile(
r"^\s*(?:Apparaatlijst|Device\s+list)\s*(?:\((?P<kind>[^)]+)\))?\s*:\s*(?P<list>.+?)\s*$",
re.I,
)
def _is_synology_active_backup_for_business(subject: str, text: str) -> bool: def _is_synology_active_backup_for_business(subject: str, text: str) -> bool:
@ -219,78 +212,50 @@ 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]]: def _parse_active_backup_for_business(subject: str, text: str) -> Tuple[bool, Dict, List[Dict]]:
haystack = f"{subject}\n{text}" 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: if not m:
# Not our ABB format # Not our ABB format
return False, {}, [] return False, {}, []
job_name = (m.groupdict().get("job") or m.groupdict().get("job_en") or "").strip() job_name = (m.group("job") or m.group("job_en") or "").strip()
host = (m.groupdict().get("host") or m.groupdict().get("host_en") or "").strip() host = (m.group("host") or m.group("host_en") or "").strip()
if not host:
m_host = re.search(r"\b(?:Van|From)\s+(?P<host>[^\r\n<]+)", text or "", re.I) # Determine overall status based on completion type and failure markers
if m_host: status_raw = (m.group("status") or m.group("status_en") or "").lower()
host = (m_host.group("host") or "").strip()
# Determine overall status:
# - Failed -> Error
# - Partially completed -> Warning
# - Otherwise -> Success
overall_status = "Success" overall_status = "Success"
overall_message = "Success" overall_message = "Success"
is_failed = _ABB_FAILED_RE.search(haystack) is not None # "gedeeltelijk voltooid" / "partially completed" should be treated as Warning
is_partial = _ABB_PARTIAL_RE.search(haystack) is not None if "gedeeltelijk" in status_raw or "partially" in status_raw:
if is_failed:
overall_status = "Error"
overall_message = "Failed"
elif is_partial:
overall_status = "Warning" overall_status = "Warning"
overall_message = "Partially completed" overall_message = "Partially completed"
# Parse device lists (newer ABB mail templates). # Explicit failure wording overrides everything
# NL examples: if _ABB_FAILED_RE.search(haystack):
# "Lijst met apparaten (back-up gelukt): SQL01, DC01" overall_status = "Error"
# "Lijst met apparaten (back-up mislukt): DC02" overall_message = "Failed"
# EN examples:
# "List of devices (backup succeeded): ..."
# "List of devices (backup failed): ..."
objects: List[Dict] = [] 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(): for line in (text or "").splitlines():
mm_ok = _ABB_DEVICE_SUCCESS_RE.match(line.strip()) mm = _ABB_DEVICE_LIST_RE.match(line.strip())
if mm_ok: if not mm:
_add_devices(mm_ok.group("list"), "Success") continue
raw_list = (mm.group("list") or "").strip()
mm_fail = _ABB_DEVICE_FAILED_RE.match(line.strip()) kind = (mm.group("kind") or "").lower()
if mm_fail: line_status = overall_status
_add_devices(mm_fail.group("list"), "Error") if "gelukt" in kind or "succeeded" in kind or "success" in kind:
line_status = "Success"
elif "mislukt" in kind or "failed" in kind or "error" in kind:
line_status = "Error"
# Fallback: generic "Apparaatlijst:" / "Device list:" (older templates) # "DC01, SQL01"
if not objects: for name in [p.strip() for p in raw_list.split(",")]:
for line in (text or "").splitlines(): if name:
mm = _ABB_DEVICE_LIST_RE.match(line.strip()) objects.append({"name": name, "status": line_status})
if not mm:
continue
_add_devices(mm.group("list"), overall_status)
result: Dict = { result = {
"backup_software": "Synology", "backup_software": "Synology",
"backup_type": "Active Backup for Business", "backup_type": "Active Backup for Business",
"job_name": job_name, "job_name": job_name,
@ -299,11 +264,73 @@ def _parse_active_backup_for_business(subject: str, text: str) -> Tuple[bool, Di
} }
# Provide a slightly nicer overall message when host is available # Provide a slightly nicer overall message when host is available
if host and overall_message in ("Success", "Failed", "Partially completed"): if host and overall_message in ("Success", "Failed"):
result["overall_message"] = f"{overall_message} ({host})" result["overall_message"] = f"{overall_message} ({host})"
return True, result, objects 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: def _is_synology_rsync(subject: str, text: str) -> bool:

View File

@ -116,6 +116,15 @@
- Improved host detection by extracting the system name from the “From” header when not present in the subject/body. - 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. - 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. - Fixed the ABB-specific regular expression to prevent parser failures on valid warning mails.
---
## v20260112-02-synology-abb-subject-partial-warning
- Updated the Synology Active Backup for Business subject matching to also recognize “Virtual machine backup task …” variants.
- Added subject-based detection for “partially completed / gedeeltelijk voltooid” and map this to Overall status: Warning.
- Fixed the ABB completion regex (it previously failed to match valid subjects).
- Improved per-device object parsing by detecting “backup succeeded/failed” device-list variants and mapping them to Success/Error accordingly.
================================================================================================================================================ ================================================================================================================================================
## v0.1.19 ## 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. 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.