mirror of
https://github.com/domainaware/parsedmarc.git
synced 2026-08-15 19:53:18 +00:00
* Upgrade ruff to 0.16.0 and pyright to 1.1.411; convert to f-strings ruff 0.16.0 expanded the default lint rule selection well beyond the long-standing E4/E7/E9/F, so [tool.ruff.lint] now selects the rule set explicitly: the pre-0.16 defaults plus the modern-type-hint UP rules and the two f-string rules (UP030/UP032). All 352 UP030/UP032 findings were auto-fixed; conversions requiring Python 3.12 f-string quote reuse were conservatively left as .format() by ruff (verified: the whole package and test suite byte-compile under CPython 3.10.20, the oldest CI version). Adopting the other newly-default rule families (BLE, SIM, C4, DTZ, I, ...) is deferred as a deliberate per-family decision. ruff format with 0.16.0 also now formats Python code fences in Markdown, which reformatted one block in parsedmarc/resources/maps/AGENTS.md. ruff check, ruff format --check, pyright (0 errors/warnings), and the full test suite (775 passed) are green on the new versions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address Copilot review findings on the f-string conversion - Rewrite messages that used backslash line continuations inside string literals, which embedded the source indentation as literal whitespace in the logged/raised text: the duplicate search-error messages in the Elasticsearch and OpenSearch outputs, the 'since'-option warning (which also implicitly concatenated "24hrs" and "SMTP" with no separator) and the IMAP 'since' debug line, and the missing-org_name KeyError message. - Build the Splunk HEC newline-delimited payloads by appending to a list and joining once instead of quadratic string concatenation in a loop. Declined: switching the webhook output's logger.error to logger.exception — the single-line ERROR without a traceback is the deliberate house pattern for batch-resilient sinks, and changing log verbosity is out of scope for this refactor PR. ruff check/format, pyright (0 errors/warnings), 775 tests, and a CPython 3.10 compileall all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
74 lines
2.5 KiB
Python
74 lines
2.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from parsedmarc import logger
|
|
from parsedmarc.constants import USER_AGENT
|
|
|
|
|
|
class WebhookClient(object):
|
|
"""A client for webhooks"""
|
|
|
|
def __init__(
|
|
self,
|
|
aggregate_url: str,
|
|
failure_url: str,
|
|
smtp_tls_url: str,
|
|
timeout: int | None = 60,
|
|
):
|
|
"""
|
|
Initializes the WebhookClient
|
|
Args:
|
|
aggregate_url (str): The aggregate report webhook url
|
|
failure_url (str): The failure report webhook url
|
|
smtp_tls_url (str): The smtp_tls report webhook url
|
|
timeout (int): The timeout to use when calling the webhooks
|
|
"""
|
|
self.aggregate_url = aggregate_url
|
|
self.failure_url = failure_url
|
|
self.smtp_tls_url = smtp_tls_url
|
|
self.timeout = timeout
|
|
self.session = httpx.Client(
|
|
headers={
|
|
"User-Agent": USER_AGENT,
|
|
"Content-Type": "application/json",
|
|
},
|
|
follow_redirects=True,
|
|
)
|
|
|
|
def save_failure_report_to_webhook(self, report: str):
|
|
self._send_to_webhook(self.failure_url, report)
|
|
|
|
def save_smtp_tls_report_to_webhook(self, report: str):
|
|
self._send_to_webhook(self.smtp_tls_url, report)
|
|
|
|
def save_aggregate_report_to_webhook(self, report: str):
|
|
self._send_to_webhook(self.aggregate_url, report)
|
|
|
|
def _send_to_webhook(self, webhook_url: str, payload: bytes | str | dict[str, Any]):
|
|
# All HTTP / network errors are swallowed and logged: a failing
|
|
# webhook should never abort the surrounding parse-and-output
|
|
# batch. The outer save_* methods previously wrapped this in a
|
|
# redundant try/except — removed because _send_to_webhook
|
|
# already catches every Exception itself.
|
|
try:
|
|
if isinstance(payload, dict):
|
|
# requests form-encoded dict payloads via data=; httpx does
|
|
# the same only via data=
|
|
self.session.post(webhook_url, data=payload, timeout=self.timeout)
|
|
else:
|
|
self.session.post(webhook_url, content=payload, timeout=self.timeout)
|
|
except Exception as error_:
|
|
logger.error(f"Webhook Error: {error_.__str__()}")
|
|
|
|
def close(self):
|
|
"""Close the underlying HTTP session."""
|
|
self.session.close()
|
|
|
|
# Backward-compatible alias
|
|
save_forensic_report_to_webhook = save_failure_report_to_webhook
|