|
|
|
@ -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():
|
|
|
|
|
|
|
|
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():
|
|
|
|
for line in (text or "").splitlines():
|
|
|
|
mm = _ABB_DEVICE_LIST_RE.match(line.strip())
|
|
|
|
mm = _ABB_DEVICE_LIST_RE.match(line.strip())
|
|
|
|
if not mm:
|
|
|
|
if not mm:
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
_add_devices(mm.group("list"), overall_status)
|
|
|
|
raw_list = (mm.group("list") or "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
result: Dict = {
|
|
|
|
kind = (mm.group("kind") or "").lower()
|
|
|
|
|
|
|
|
line_status = overall_status
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# "DC01, SQL01"
|
|
|
|
|
|
|
|
for name in [p.strip() for p in raw_list.split(",")]:
|
|
|
|
|
|
|
|
if name:
|
|
|
|
|
|
|
|
objects.append({"name": name, "status": line_status})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|