diff --git a/_modules/parsedmarc.html b/_modules/parsedmarc.html
index 4d27bef2..3c480c8d 100644
--- a/_modules/parsedmarc.html
+++ b/_modules/parsedmarc.html
@@ -5,14 +5,14 @@
- parsedmarc — parsedmarc 10.3.0 documentation
+ parsedmarc — parsedmarc 10.4.0 documentation
-
+
@@ -88,6 +88,7 @@
importbinasciiimportemailimportemail.utils
+importfunctoolsimportjsonimportloggingimportmailbox
@@ -100,10 +101,11 @@
importzipfileimportzlibfrombase64importb64decode
+fromcollectionsimportdeque
+fromcollections.abcimportCallable,SequencefromcsvimportDictWriterfromdatetimeimportdate,datetime,timedelta,timezone,tzinfofromioimportBytesIO,StringIO
-fromcollections.abcimportCallable,Sequencefromtypingimport(Any,BinaryIO,
@@ -117,6 +119,12 @@
frommailsuite.smtpimportsend_emailfromtqdmimporttqdm
+fromparsedmarc.configimport(
+ IP_ADDRESS_CACHE,
+ REVERSE_DNS_MAP,
+ SEEN_AGGREGATE_REPORT_IDS,
+ ParserConfig,
+)fromparsedmarc.constantsimport(DEFAULT_DNS_MAX_RETRIES,DEFAULT_DNS_TIMEOUT,
@@ -136,6 +144,7 @@
ForensicReportasForensicReport,ParsedReport,ParsingResults,
+ ReportType,SMTPTLSReport,)fromparsedmarc.utilsimport(
@@ -181,7 +190,31 @@
MAGIC_GZIP=b"\x1f\x8b"MAGIC_XML=b"\x3c\x3f\x78\x6d\x6c\x20"MAGIC_XML_TAG=b"\x3c"# '<' - XML starting with an element tag (no declaration)
-MAGIC_JSON=b"\7b"
+# 0x7B, "{" -- a JSON text that is an object begins with it (RFC 8259).
+# Previously written as b"\7b", which Python reads as the octal escape
+# \7 (BEL) followed by a literal "b", so the branch never matched real
+# JSON; every in-tree caller happened to pre-guard with its own zip/gzip
+# or "{" check, which masked it.
+MAGIC_JSON=b"\x7b"
+
+# Per-message count of consecutive failed saves, keyed on
+# ``(reports_folder, str(message_uid))``. Populated only when
+# ``get_dmarc_reports_from_mailbox()`` is given a ``save_callback`` that
+# reports a batch as unsaved; a message whose count exceeds
+# ``max_unsaved_retries`` is moved to ``{archive_folder}/Unsaved`` instead
+# of being retried forever, and its entry is dropped. A successful save
+# clears the entries for that batch's messages.
+#
+# Process-local and deliberately not persisted: a restart re-attempts every
+# message still in the reports folder, which is the safe direction (retry
+# rather than shelve). The key carries no connection identity, so a library
+# caller processing two connections that share a folder name (two IMAP
+# servers, both "INBOX") in one process could collide counters if UIDs
+# happen to match; the CLI uses one connection per process. It is not a
+# ``ParserConfig`` field for the same reason ``batch_size`` and the
+# ``delete`` flags are not -- it governs mailbox orchestration, not
+# parsing.
+_FAILED_SAVE_ATTEMPTS:dict[tuple[str,str],int]={}EMAIL_SAMPLE_CONTENT_TYPES=("text/rfc822",
@@ -194,10 +227,6 @@
"message/rfc-822-headers",)
-IP_ADDRESS_CACHE=ExpiringDict(max_len=10000,max_age_seconds=14400)
-SEEN_AGGREGATE_REPORT_IDS=ExpiringDict(max_len=100000000,max_age_seconds=3600)
-REVERSE_DNS_MAP=dict()
-
[docs]
@@ -238,6 +267,66 @@
InvalidForensicReport=InvalidFailureReport
+def_resolve_config(
+ config:ParserConfig|None,
+ *,
+ offline:bool=False,
+ ip_db_path:str|None=None,
+ always_use_local_files:bool=False,
+ reverse_dns_map_path:str|None=None,
+ reverse_dns_map_url:str|None=None,
+ nameservers:list[str]|None=None,
+ dns_timeout:float=DEFAULT_DNS_TIMEOUT,
+ dns_retries:int=DEFAULT_DNS_MAX_RETRIES,
+ strip_attachment_payloads:bool=False,
+ normalize_timespan_threshold_hours:float=24.0,
+)->ParserConfig:
+"""Resolve the effective :class:`~parsedmarc.config.ParserConfig` for a
+ public parsing call, from either an explicit ``config`` or the caller's
+ individual option keyword arguments.
+
+ If ``config`` is not ``None``, it is returned unchanged: per the
+ documented ``config=`` contract, the individual option keyword arguments
+ are ignored in favor of the config's own values, so no merging happens
+ here.
+
+ Otherwise, a new ``ParserConfig`` is built from the given keyword
+ arguments. Its three cache fields are deliberately *not* left to their
+ ``default_factory`` -- doing so would hand back a config with brand new,
+ empty caches on every call, silently defeating cross-call IP-info
+ caching and aggregate-report dedup. Instead, the module-default caches
+ (:data:`IP_ADDRESS_CACHE`, :data:`SEEN_AGGREGATE_REPORT_IDS`,
+ :data:`REVERSE_DNS_MAP`) are injected explicitly, so kwargs-style calls
+ keep observing and mutating the same shared caches they always have.
+
+ ``dataclasses.replace`` is deliberately not used here: it would need an
+ existing ``ParserConfig`` to start from, and there isn't one on the
+ kwargs path -- this function's job is to build the first one.
+
+ ``psl_overrides_path`` / ``psl_overrides_url`` have no corresponding
+ keyword arguments on the public functions (see AGENTS.md's guidance on
+ justifying new config options), so they stay ``None`` on this path; they
+ are only ever set via an explicitly constructed ``config=``.
+ """
+ ifconfigisnotNone:
+ returnconfig
+ returnParserConfig(
+ offline=offline,
+ ip_db_path=ip_db_path,
+ always_use_local_files=always_use_local_files,
+ reverse_dns_map_path=reverse_dns_map_path,
+ reverse_dns_map_url=reverse_dns_map_url,
+ nameservers=nameservers,
+ dns_timeout=dns_timeout,
+ dns_retries=dns_retries,
+ strip_attachment_payloads=strip_attachment_payloads,
+ normalize_timespan_threshold_hours=float(normalize_timespan_threshold_hours),
+ ip_address_cache=IP_ADDRESS_CACHE,
+ seen_aggregate_report_ids=SEEN_AGGREGATE_REPORT_IDS,
+ reverse_dns_map=REVERSE_DNS_MAP,
+ )
+
+
def_exc_origin(error:BaseException)->str:"""Returns a ``" (raised at <file>:<line>)"`` suffix pointing at where an unexpected exception actually originated, but only when the parsedmarc
@@ -471,14 +560,7 @@
def_parse_report_record(record:dict[str,Any],*,
- ip_db_path:str|None=None,
- always_use_local_files:bool=False,
- reverse_dns_map_path:str|None=None,
- reverse_dns_map_url:str|None=None,
- offline:bool=False,
- nameservers:list[str]|None=None,
- dns_timeout:float=DEFAULT_DNS_TIMEOUT,
- dns_retries:int=DEFAULT_DNS_MAX_RETRIES,
+ config:ParserConfig,is_rfc_9990:bool=False,)->dict[str,Any]:"""
@@ -487,16 +569,9 @@
Args: record (dict): The record to convert
- always_use_local_files (bool): Do not download files
- reverse_dns_map_path (str): Path to a reverse DNS map file
- reverse_dns_map_url (str): URL to a reverse DNS map file
- ip_db_path (str): Path to a MMDB file from IPinfo, MaxMind, or DBIP
- offline (bool): Do not query online for geolocation or DNS
- nameservers (list): A list of one or more nameservers to use
- (Cloudflare's public DNS resolvers by default)
- dns_timeout (float): Sets the DNS timeout in seconds
- dns_retries (int): Number of times to retry DNS queries on timeout
- or other transient errors
+ config (ParserConfig): Parsing and enrichment options, plus caches
+ is_rfc_9990 (bool): Whether the enclosing report was detected as
+ RFC 9990-shaped, for RFC 9990-aware validation warnings Returns: dict: The converted record
@@ -507,16 +582,18 @@
raiseValueError("Source IP address is empty")new_record_source=get_ip_address_info(record["row"]["source_ip"],
- cache=IP_ADDRESS_CACHE,
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- reverse_dns_map=REVERSE_DNS_MAP,
- offline=offline,
- nameservers=nameservers,
- timeout=dns_timeout,
- retries=dns_retries,
+ cache=config.ip_address_cache,
+ ip_db_path=config.ip_db_path,
+ always_use_local_files=config.always_use_local_files,
+ reverse_dns_map_path=config.reverse_dns_map_path,
+ reverse_dns_map_url=config.reverse_dns_map_url,
+ reverse_dns_map=config.reverse_dns_map,
+ offline=config.offline,
+ nameservers=config.nameservers,
+ timeout=config.dns_timeout,
+ retries=config.dns_retries,
+ psl_overrides_path=config.psl_overrides_path,
+ psl_overrides_url=config.psl_overrides_url,)new_record["source"]=new_record_sourcenew_record["count"]=int(record["row"]["count"])
@@ -887,6 +964,7 @@
retries:int=DEFAULT_DNS_MAX_RETRIES,keep_alive:Callable|None=None,normalize_timespan_threshold_hours:float=24.0,
+ config:ParserConfig|None=None,)->AggregateReport:"""Parses a DMARC XML report string and returns a consistent dict
@@ -903,12 +981,29 @@
timeout (float): Sets the DNS timeout in seconds retries (int): Number of times to retry DNS queries on timeout or other transient errors
- keep_alive (callable): Keep alive function
+ keep_alive (callable): Keep alive function. Not part of ``config``;
+ always applies. normalize_timespan_threshold_hours (float): Normalize timespans beyond this
+ config (ParserConfig): a single object carrying all parsing and
+ enrichment options plus the caches; when provided, the
+ individual option keyword arguments listed above are ignored in
+ favor of the config's values. Returns: dict: The parsed aggregate DMARC report """
+ cfg=_resolve_config(
+ config,
+ offline=offline,
+ ip_db_path=ip_db_path,
+ always_use_local_files=always_use_local_files,
+ reverse_dns_map_path=reverse_dns_map_path,
+ reverse_dns_map_url=reverse_dns_map_url,
+ nameservers=nameservers,
+ dns_timeout=timeout,
+ dns_retries=retries,
+ normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
+ )errors=[]# Parse XML and recover from errorsifisinstance(xml,bytes):
@@ -1000,7 +1095,9 @@
end_ts=int(date_range["end"].split(".")[0])span_seconds=end_ts-begin_ts
- normalize_timespan=span_seconds>normalize_timespan_threshold_hours*3600
+ normalize_timespan=(
+ span_seconds>cfg.normalize_timespan_threshold_hours*3600
+ )date_range["begin"]=timestamp_to_human(begin_ts)date_range["end"]=timestamp_to_human(end_ts)
@@ -1110,14 +1207,7 @@
try:report_record=_parse_report_record(report["record"][i],
- ip_db_path=ip_db_path,
- offline=offline,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- nameservers=nameservers,
- dns_timeout=timeout,
- dns_retries=retries,
+ config=cfg,is_rfc_9990=is_rfc_9990,)_append_parsed_record(
@@ -1133,14 +1223,7 @@
else:report_record=_parse_report_record(report["record"],
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- offline=offline,
- nameservers=nameservers,
- dns_timeout=timeout,
- dns_retries=retries,
+ config=cfg,is_rfc_9990=is_rfc_9990,)_append_parsed_record(
@@ -1288,6 +1371,7 @@
dns_retries:int=DEFAULT_DNS_MAX_RETRIES,keep_alive:Callable|None=None,normalize_timespan_threshold_hours:float=24.0,
+ config:ParserConfig|None=None,)->AggregateReport:"""Parses a file at the given path, a file-like object. or bytes as an aggregate DMARC report
@@ -1304,12 +1388,29 @@
dns_timeout (float): Sets the DNS timeout in seconds dns_retries (int): Number of times to retry DNS queries on timeout or other transient errors
- keep_alive (callable): Keep alive function
+ keep_alive (callable): Keep alive function. Not part of ``config``;
+ always applies. normalize_timespan_threshold_hours (float): Normalize timespans beyond this
+ config (ParserConfig): a single object carrying all parsing and
+ enrichment options plus the caches; when provided, the
+ individual option keyword arguments listed above are ignored in
+ favor of the config's values. Returns: dict: The parsed DMARC aggregate report """
+ cfg=_resolve_config(
+ config,
+ offline=offline,
+ ip_db_path=ip_db_path,
+ always_use_local_files=always_use_local_files,
+ reverse_dns_map_path=reverse_dns_map_path,
+ reverse_dns_map_url=reverse_dns_map_url,
+ nameservers=nameservers,
+ dns_timeout=dns_timeout,
+ dns_retries=dns_retries,
+ normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
+ )try:xml=extract_report(_input)
@@ -1318,16 +1419,8 @@
returnparse_aggregate_report_xml(xml,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- ip_db_path=ip_db_path,
- offline=offline,
- nameservers=nameservers,
- timeout=dns_timeout,
- retries=dns_retries,
+ config=cfg,keep_alive=keep_alive,
- normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,)
@@ -1563,6 +1656,7 @@
dns_timeout:float=DEFAULT_DNS_TIMEOUT,dns_retries:int=DEFAULT_DNS_MAX_RETRIES,strip_attachment_payloads:bool=False,
+ config:ParserConfig|None=None,)->FailureReport:""" Converts a DMARC failure report and sample to a dict
@@ -1583,10 +1677,26 @@
or other transient errors strip_attachment_payloads (bool): Remove attachment payloads from failure report results
+ config (ParserConfig): a single object carrying all parsing and
+ enrichment options plus the caches; when provided, the
+ individual option keyword arguments listed above are ignored in
+ favor of the config's values. Returns: dict: A parsed report and sample """
+ cfg=_resolve_config(
+ config,
+ offline=offline,
+ ip_db_path=ip_db_path,
+ always_use_local_files=always_use_local_files,
+ reverse_dns_map_path=reverse_dns_map_path,
+ reverse_dns_map_url=reverse_dns_map_url,
+ nameservers=nameservers,
+ dns_timeout=dns_timeout,
+ dns_retries=dns_retries,
+ strip_attachment_payloads=strip_attachment_payloads,
+ )delivery_results=["delivered","spam","policy","reject","other"]try:
@@ -1626,16 +1736,18 @@
ip_address=re.split(r"\s",parsed_report["source_ip"]).pop(0)parsed_report_source=get_ip_address_info(ip_address,
- cache=IP_ADDRESS_CACHE,
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- reverse_dns_map=REVERSE_DNS_MAP,
- offline=offline,
- nameservers=nameservers,
- timeout=dns_timeout,
- retries=dns_retries,
+ cache=cfg.ip_address_cache,
+ ip_db_path=cfg.ip_db_path,
+ always_use_local_files=cfg.always_use_local_files,
+ reverse_dns_map_path=cfg.reverse_dns_map_path,
+ reverse_dns_map_url=cfg.reverse_dns_map_url,
+ reverse_dns_map=cfg.reverse_dns_map,
+ offline=cfg.offline,
+ nameservers=cfg.nameservers,
+ timeout=cfg.dns_timeout,
+ retries=cfg.dns_retries,
+ psl_overrides_path=cfg.psl_overrides_path,
+ psl_overrides_url=cfg.psl_overrides_url,)parsed_report["source"]=parsed_report_sourcedelparsed_report["source_ip"]
@@ -1704,7 +1816,7 @@
parsed_report[optional_field]=Noneparsed_sample=parse_email(
- sample,strip_attachment_payloads=strip_attachment_payloads
+ sample,strip_attachment_payloads=cfg.strip_attachment_payloads)if"reported_domain"notinparsed_report:
@@ -1854,6 +1966,7 @@
strip_attachment_payloads:bool=False,keep_alive:Callable|None=None,normalize_timespan_threshold_hours:float=24.0,
+ config:ParserConfig|None=None,)->ParsedReport:""" Parses a DMARC report from an email
@@ -1871,14 +1984,32 @@
or other transient errors strip_attachment_payloads (bool): Remove attachment payloads from failure report results
- keep_alive (callable): keep alive function
+ keep_alive (callable): keep alive function. Not part of ``config``;
+ always applies. normalize_timespan_threshold_hours (float): Normalize timespans beyond this
+ config (ParserConfig): a single object carrying all parsing and
+ enrichment options plus the caches; when provided, the
+ individual option keyword arguments listed above are ignored in
+ favor of the config's values. Returns: dict: * ``report_type``: ``aggregate`` or ``failure`` * ``report``: The parsed report """
+ cfg=_resolve_config(
+ config,
+ offline=offline,
+ ip_db_path=ip_db_path,
+ always_use_local_files=always_use_local_files,
+ reverse_dns_map_path=reverse_dns_map_path,
+ reverse_dns_map_url=reverse_dns_map_url,
+ nameservers=nameservers,
+ dns_timeout=dns_timeout,
+ dns_retries=dns_retries,
+ strip_attachment_payloads=strip_attachment_payloads,
+ normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
+ )result:ParsedReport|None=Nonemsg_date:datetime=datetime.now(timezone.utc)
@@ -1986,16 +2117,8 @@
elifpayload_text.strip().startswith("<"):aggregate_report=parse_aggregate_report_xml(payload_text,
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- offline=offline,
- nameservers=nameservers,
- timeout=dns_timeout,
- retries=dns_retries,
+ config=cfg,keep_alive=keep_alive,
- normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,)result={"report_type":"aggregate","report":aggregate_report}
@@ -2020,15 +2143,7 @@
feedback_report,sample,msg_date,
- offline=offline,
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- nameservers=nameservers,
- dns_timeout=dns_timeout,
- dns_retries=dns_retries,
- strip_attachment_payloads=strip_attachment_payloads,
+ config=cfg,)exceptInvalidFailureReportase:error=(
@@ -2112,7 +2227,8 @@
reverse_dns_map_url:str|None=None,offline:bool=False,keep_alive:Callable|None=None,
- normalize_timespan_threshold_hours:float=24,
+ normalize_timespan_threshold_hours:float=24.0,
+ config:ParserConfig|None=None,)->ParsedReport:"""Parses a DMARC aggregate or failure file at the given path, a file-like object. or bytes
@@ -2132,11 +2248,30 @@
reverse_dns_map_path (str): Path to a reverse DNS map reverse_dns_map_url (str): URL to a reverse DNS map offline (bool): Do not make online queries for geolocation or DNS
- keep_alive (callable): Keep alive function
+ keep_alive (callable): Keep alive function. Not part of ``config``;
+ always applies.
+ normalize_timespan_threshold_hours (float): Normalize timespans beyond this
+ config (ParserConfig): a single object carrying all parsing and
+ enrichment options plus the caches; when provided, the
+ individual option keyword arguments listed above are ignored in
+ favor of the config's values. Returns: dict: The parsed DMARC report """
+ cfg=_resolve_config(
+ config,
+ offline=offline,
+ ip_db_path=ip_db_path,
+ always_use_local_files=always_use_local_files,
+ reverse_dns_map_path=reverse_dns_map_path,
+ reverse_dns_map_url=reverse_dns_map_url,
+ nameservers=nameservers,
+ dns_timeout=dns_timeout,
+ dns_retries=dns_retries,
+ strip_attachment_payloads=strip_attachment_payloads,
+ normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
+ )file_object:BinaryIOifisinstance(input_,(str,os.PathLike)):file_path=os.fspath(input_)
@@ -2161,16 +2296,8 @@
try:report=parse_aggregate_report_file(content,
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- offline=offline,
- nameservers=nameservers,
- dns_timeout=dns_timeout,
- dns_retries=dns_retries,
+ config=cfg,keep_alive=keep_alive,
- normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,)results={"report_type":"aggregate","report":report}exceptInvalidAggregateReportasaggregate_error:
@@ -2181,17 +2308,8 @@
try:results=parse_report_email(content,
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- offline=offline,
- nameservers=nameservers,
- dns_timeout=dns_timeout,
- dns_retries=dns_retries,
- strip_attachment_payloads=strip_attachment_payloads,
+ config=cfg,keep_alive=keep_alive,
- normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,)exceptInvalidDMARCReportasemail_error:raiseParserError(
@@ -2206,6 +2324,123 @@
+def_classify_parsed_email(
+ parsed_email:ParsedReport,
+ aggregate_reports:list[AggregateReport],
+ failure_reports:list[FailureReport],
+ smtp_tls_reports:list[SMTPTLSReport],
+ *,
+ seen_aggregate_report_ids:ExpiringDict,
+ pending_aggregate_keys:set[str]|None=None,
+)->ReportType:
+"""Classify a parsed report email, appending it to the matching list.
+
+ Owns the seen-aggregate-report-ID dedup check against
+ ``seen_aggregate_report_ids``: an aggregate report already seen (keyed
+ on ``{org_name}_{report_id}``) is logged and dropped instead of
+ appended. Shared, unmodified, by the sequential and parallel branches
+ of ``get_dmarc_reports_from_mbox`` and ``get_dmarc_reports_from_mailbox``
+ so both dedup identically -- callers pass the config's
+ ``seen_aggregate_report_ids`` cache so dedup state stays scoped to
+ whichever ``ParserConfig`` (explicit or module-default) is in effect.
+
+ ``pending_aggregate_keys`` lets a caller *defer* the dedup-cache write:
+ when a set is supplied, a newly seen key is staged in that set instead
+ of being written to ``seen_aggregate_report_ids``, and the dedup check
+ consults both. The caller then folds the staged keys into the cache
+ once the batch is known to have been saved (see
+ ``get_dmarc_reports_from_mailbox``), or drops them so a retry reparses
+ the same reports rather than silently skipping them as duplicates.
+ ``None`` (the default) keeps the immediate-write behavior.
+
+ Returns the report type so mailbox callers know which UID list to
+ append the source message's UID to.
+ """
+ report_type=parsed_email["report_type"]
+ # Compare against parsed_email["report_type"] directly (not the
+ # report_type local above) in each branch so pyright's TypedDict
+ # discriminated-union narrowing applies to parsed_email["report"].
+ ifparsed_email["report_type"]=="aggregate":
+ report_org=parsed_email["report"]["report_metadata"]["org_name"]
+ report_id=parsed_email["report"]["report_metadata"]["report_id"]
+ report_key=f"{report_org}_{report_id}"
+ already_seen=report_keyinseen_aggregate_report_idsor(
+ pending_aggregate_keysisnotNoneandreport_keyinpending_aggregate_keys
+ )
+ ifnotalready_seen:
+ ifpending_aggregate_keysisNone:
+ seen_aggregate_report_ids[report_key]=True
+ else:
+ pending_aggregate_keys.add(report_key)
+ aggregate_reports.append(parsed_email["report"])
+ else:
+ logger.debug(
+ f"Skipping duplicate aggregate report from {report_org} "
+ f"with ID: {report_id}"
+ )
+ elifparsed_email["report_type"]=="failure":
+ failure_reports.append(parsed_email["report"])
+ elifparsed_email["report_type"]=="smtp_tls":
+ smtp_tls_reports.append(parsed_email["report"])
+ returnreport_type
+
+
+def_fetch_mailbox_message(
+ connection:MailboxConnection,msg_uid:Any,test:bool
+)->tuple[int|str,str]:
+"""Fetch one message from ``connection`` by UID, casting the UID to the
+ type each backend's ``fetch_message`` expects.
+
+ Shared, unmodified, by the sequential and parallel branches of
+ ``get_dmarc_reports_from_mailbox`` so both fetch identically.
+
+ Returns ``(message_id, msg_content)``; ``message_id`` is the
+ backend-appropriate id to use for later move/delete calls.
+ """
+ message_id:int|str
+ ifisinstance(connection,IMAPConnection):
+ message_id=int(msg_uid)
+ msg_content=connection.fetch_message(message_id)
+ elifisinstance(connection,MSGraphConnection):
+ message_id=str(msg_uid)
+ msg_content=connection.fetch_message(message_id,mark_read=nottest)
+ elifisinstance(connection,MaildirConnection):
+ message_id=str(msg_uid)ifnotisinstance(msg_uid,str)elsemsg_uid
+ msg_content=connection.fetch_message(message_id,mark_read=nottest)
+ else:
+ message_id=str(msg_uid)ifnotisinstance(msg_uid,str)elsemsg_uid
+ msg_content=connection.fetch_message(message_id)
+ returnmessage_id,msg_content
+
+
+def_dispose_invalid_message(
+ connection:MailboxConnection,
+ message_id:int|str,
+ delete:bool,
+ invalid_reports_folder:str,
+)->None:
+"""Delete or move an unparseable message, per ``delete``.
+
+ Callers pass their effective ``delete_invalid`` value as ``delete``.
+
+ Shared, unmodified, by the sequential and parallel branches of
+ ``get_dmarc_reports_from_mailbox`` so both dispose of invalid messages
+ identically.
+ """
+ ifdelete:
+ logger.debug(f"Deleting message UID {message_id}")
+ ifisinstance(connection,IMAPConnection):
+ connection.delete_message(int(message_id))
+ else:
+ connection.delete_message(str(message_id))
+ else:
+ logger.debug(f"Moving message UID {message_id} to {invalid_reports_folder}")
+ ifisinstance(connection,IMAPConnection):
+ connection.move_message(int(message_id),invalid_reports_folder)
+ else:
+ connection.move_message(str(message_id),invalid_reports_folder)
+
+
[docs]defget_dmarc_reports_from_mbox(
@@ -2221,6 +2456,8 @@
reverse_dns_map_url:str|None=None,offline:bool=False,normalize_timespan_threshold_hours:float=24.0,
+ n_procs:int=1,
+ config:ParserConfig|None=None,)->ParsingResults:"""Parses a mailbox in mbox format containing e-mails with attached DMARC reports
@@ -2240,11 +2477,32 @@
ip_db_path (str): Path to a MMDB file from IPinfo, MaxMind, or DBIP offline (bool): Do not make online queries for geolocation or DNS normalize_timespan_threshold_hours (float): Normalize timespans beyond this
+ n_procs (int): Number of processes to use for parsing messages in
+ parallel. Message reading, deduplication, and result assembly
+ stay in the calling process; only parsing is parallelized. Not
+ part of ``config``; always applies.
+ config (ParserConfig): a single object carrying all parsing and
+ enrichment options plus the caches; when provided, the
+ individual option keyword arguments listed above are ignored in
+ favor of the config's values. Returns: dict: Lists of ``aggregate_reports``, ``failure_reports``, and ``smtp_tls_reports`` """
+ cfg=_resolve_config(
+ config,
+ offline=offline,
+ ip_db_path=ip_db_path,
+ always_use_local_files=always_use_local_files,
+ reverse_dns_map_path=reverse_dns_map_path,
+ reverse_dns_map_url=reverse_dns_map_url,
+ nameservers=nameservers,
+ dns_timeout=dns_timeout,
+ dns_retries=dns_retries,
+ strip_attachment_payloads=strip_attachment_payloads,
+ normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
+ )aggregate_reports:list[AggregateReport]=[]failure_reports:list[FailureReport]=[]smtp_tls_reports:list[SMTPTLSReport]=[]
@@ -2253,43 +2511,51 @@
message_keys=mbox.keys()total_messages=len(message_keys)logger.debug(f"Found {total_messages} messages in {input_}")
- foriintqdm(range(total_messages),disable=None):
- message_key=message_keys[i]
- logger.info(f"Processing message {i+1} of {total_messages}")
- msg_content=mbox.get_string(message_key)
- try:
- sa=strip_attachment_payloads
- parsed_email=parse_report_email(
- msg_content,
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- offline=offline,
- nameservers=nameservers,
- dns_timeout=dns_timeout,
- dns_retries=dns_retries,
- strip_attachment_payloads=sa,
- normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
- )
- ifparsed_email["report_type"]=="aggregate":
- report_org=parsed_email["report"]["report_metadata"]["org_name"]
- report_id=parsed_email["report"]["report_metadata"]["report_id"]
- report_key=f"{report_org}_{report_id}"
- ifreport_keynotinSEEN_AGGREGATE_REPORT_IDS:
- SEEN_AGGREGATE_REPORT_IDS[report_key]=True
- aggregate_reports.append(parsed_email["report"])
- else:
- logger.debug(
- "Skipping duplicate aggregate report "
- f"from {report_org} with ID: {report_id}"
- )
- elifparsed_email["report_type"]=="failure":
- failure_reports.append(parsed_email["report"])
- elifparsed_email["report_type"]=="smtp_tls":
- smtp_tls_reports.append(parsed_email["report"])
- exceptInvalidDMARCReportaserror:
- logger.warning(error.__str__())
+
+ ifn_procs>1andtotal_messages>1:
+ fromparsedmarc.parallelimport_parse_report_email_job,parallel_map
+
+ func=functools.partial(_parse_report_email_job,config=cfg)
+
+ def_jobs():
+ foriinrange(total_messages):
+ message_key=message_keys[i]
+ logger.info(f"Processing message {i+1} of {total_messages}")
+ yieldmbox.get_string(message_key)
+
+ forresultintqdm(
+ parallel_map(func,_jobs(),n_procs),
+ total=total_messages,
+ disable=None,
+ ):
+ ifisinstance(result,InvalidDMARCReport):
+ logger.warning(str(result))
+ elifisinstance(result,ParserError):
+ raiseresult
+ else:
+ _classify_parsed_email(
+ result,
+ aggregate_reports,
+ failure_reports,
+ smtp_tls_reports,
+ seen_aggregate_report_ids=cfg.seen_aggregate_report_ids,
+ )
+ else:
+ foriintqdm(range(total_messages),disable=None):
+ message_key=message_keys[i]
+ logger.info(f"Processing message {i+1} of {total_messages}")
+ msg_content=mbox.get_string(message_key)
+ try:
+ parsed_email=parse_report_email(msg_content,config=cfg)
+ _classify_parsed_email(
+ parsed_email,
+ aggregate_reports,
+ failure_reports,
+ smtp_tls_reports,
+ seen_aggregate_report_ids=cfg.seen_aggregate_report_ids,
+ )
+ exceptInvalidDMARCReportaserror:
+ logger.warning(error.__str__())exceptmailbox.NoSuchMailboxError:raiseInvalidDMARCReport(f"Mailbox {input_} does not exist")return{
@@ -2338,6 +2604,28 @@
)
+def_ensure_folder(connection:MailboxConnection,folder:str)->None:
+"""Best-effort create ``folder`` if the backend says it is missing.
+
+ ``get_dmarc_reports_from_mailbox()`` creates its destination folders up
+ front, but only when ``create_folders`` is set -- watch mode calls it
+ with ``create_folders=False``, and the ``Unsaved`` holding folder is
+ only created up front when a ``save_callback`` was supplied. This is the
+ defensive check immediately before a message is moved there.
+
+ Like ``_migrate_forensic_archive_folder``, it never raises: a backend
+ that cannot report on or create the folder is logged and skipped, and
+ the move that follows either succeeds anyway (the folder already
+ existed) or fails and is logged by its own error handler -- warn, don't
+ crash.
+ """
+ try:
+ ifnotconnection.folder_exists(folder):
+ connection.create_folder(folder)
+ exceptExceptionaserror:
+ logger.warning(f"Could not create folder {folder}: {error}")
+
+
[docs]defget_dmarc_reports_from_mailbox(
@@ -2346,6 +2634,10 @@
reports_folder:str="INBOX",archive_folder:str="Archive",delete:bool=False,
+ delete_aggregate:bool|None=None,
+ delete_failure:bool|None=None,
+ delete_smtp_tls:bool|None=None,
+ delete_invalid:bool|None=None,test:bool=False,ip_db_path:str|None=None,always_use_local_files:bool=False,
@@ -2353,14 +2645,18 @@
reverse_dns_map_url:str|None=None,offline:bool=False,nameservers:list[str]|None=None,
- dns_timeout:float=6.0,
+ dns_timeout:float=DEFAULT_DNS_TIMEOUT,dns_retries:int=DEFAULT_DNS_MAX_RETRIES,strip_attachment_payloads:bool=False,results:ParsingResults|None=None,batch_size:int=10,since:datetime|date|str|None=None,create_folders:bool=True,
- normalize_timespan_threshold_hours:float=24,
+ normalize_timespan_threshold_hours:float=24.0,
+ n_procs:int=1,
+ save_callback:Callable[[ParsingResults],bool|None]|None=None,
+ max_unsaved_retries:int=2,
+ config:ParserConfig|None=None,)->ParsingResults:""" Fetches and parses DMARC reports from a mailbox
@@ -2369,7 +2665,23 @@
connection: A Mailbox connection object reports_folder (str): The folder where reports can be found archive_folder (str): The folder to move processed mail to
- delete (bool): Delete messages after processing them
+ delete (bool): Delete messages after processing them
+ delete_aggregate (bool | None): Delete aggregate report messages
+ after processing them, instead of moving them to the
+ ``Aggregate`` archive subfolder; ``None`` (the default) inherits
+ the value of ``delete``
+ delete_failure (bool | None): Delete failure report messages after
+ processing them, instead of moving them to the ``Failure``
+ archive subfolder; ``None`` (the default) inherits the value of
+ ``delete``
+ delete_smtp_tls (bool | None): Delete SMTP TLS report messages after
+ processing them, instead of moving them to the ``SMTP-TLS``
+ archive subfolder; ``None`` (the default) inherits the value of
+ ``delete``
+ delete_invalid (bool | None): Delete unparseable messages, instead
+ of moving them to the ``Invalid`` archive subfolder where they
+ can be inspected for debugging; ``None`` (the default) inherits
+ the value of ``delete`` test (bool): Do not move or delete messages after processing them ip_db_path (str): Path to a MMDB file from IPinfo, MaxMind, or DBIP always_use_local_files (bool): Do not download files
@@ -2390,22 +2702,124 @@
create_folders (bool): Whether to create the destination folders (not used in watch) normalize_timespan_threshold_hours (float): Normalize timespans beyond this
+ n_procs (int): Number of processes to use for parsing messages in
+ parallel. Fetching, archiving, and deduplication remain
+ sequential in the calling process; only parsing is
+ parallelized. With ``n_procs > 1``, invalid-message disposition
+ happens after the parsing phase completes, rather than
+ interleaved message-by-message as it is when ``n_procs`` is 1.
+ Not part of ``config``; always applies.
+ save_callback: An optional callable invoked once per fetched batch
+ with a ``ParsingResults`` dict holding only that batch's newly
+ parsed reports, after parsing but before any of the batch's
+ messages are deleted or moved out of ``reports_folder``. It
+ tells this function whether the batch was actually persisted:
+
+ * Returning ``False`` means "not saved": the batch's messages
+ are left in ``reports_folder`` for retry on the next run (or
+ moved to ``{archive_folder}/Unsaved`` once they have failed
+ ``max_unsaved_retries`` retries -- see below), and the
+ aggregate-report dedup cache is not updated for that batch, so
+ a retry reparses the same reports instead of skipping them as
+ duplicates.
+ * Raising counts as "not saved" too: the same bookkeeping runs
+ (the batch's messages are held back or moved to ``Unsaved`` at
+ the cap, and the failed attempt counts toward
+ ``max_unsaved_retries``), and the exception is then re-raised
+ to the caller.
+ * Any other return value, including ``None``, commits the batch:
+ the dedup cache is updated and the messages are deleted or
+ archived exactly as they are with no callback.
+
+ ``None`` (the default) commits every batch, preserving the prior
+ behavior. The callback is still invoked when ``test`` is
+ ``True``, so a test run exercises the full pipeline, but neither
+ the mailbox nor the retry counters are touched regardless of
+ what it returns.
+ max_unsaved_retries (int): How many times a message may be *retried*
+ after ``save_callback`` first reported its batch unsaved, before
+ it is moved to the ``Unsaved`` archive subfolder instead of
+ being retried again (default 2, i.e. the initial attempt plus
+ two retries -- at most three deliveries to any output
+ destination that does not deduplicate). ``0`` moves a message on
+ the first failed save; negative values raise ``ValueError``.
+ Messages moved to ``Unsaved`` are never deleted, whatever the
+ ``delete`` options say; recover them by fixing the output
+ destination and moving them back into ``reports_folder``.
+ Counts are kept in memory, per message and
+ per process, are reset by a successful save, and are only kept
+ when a ``save_callback`` is supplied -- so the cap applies
+ across repeated calls within one process (``watch_inbox()``'s
+ checks), not across separate one-shot processes, each of which
+ starts a message's count over. Mailbox orchestration, so like
+ ``batch_size`` it is not part of ``config``.
+ config (ParserConfig): a single object carrying all parsing and
+ enrichment options plus the caches; when provided, it replaces
+ the individual parsing and enrichment option keyword arguments
+ listed above (DNS, GeoIP, offline mode, attachment payload
+ stripping, timespan normalization), whose values are then
+ ignored. The remaining keyword arguments control mailbox
+ handling and orchestration rather than parsing (the folder
+ names, the ``delete`` options, ``test``, ``since``,
+ ``batch_size``, ``save_callback``, ``max_unsaved_retries``);
+ they are not part of ``config`` and always apply. Returns: dict: Lists of ``aggregate_reports``, ``failure_reports``, and ``smtp_tls_reports`` """
- ifdeleteandtest:
- raiseValueError("delete and test options are mutually exclusive")
+ # Each per-report-type flag inherits the overall ``delete`` value when it
+ # is left unset (``None``). Resolve once, up front: every decision below
+ # reads only these four resolved flags. The raw ``delete`` parameter is
+ # still forwarded verbatim to the recursive self-call at the end of this
+ # function, where it is inert because all four resolved flags accompany
+ # it.
+ delete_aggregate=deleteifdelete_aggregateisNoneelsedelete_aggregate
+ delete_failure=deleteifdelete_failureisNoneelsedelete_failure
+ delete_smtp_tls=deleteifdelete_smtp_tlsisNoneelsedelete_smtp_tls
+ delete_invalid=deleteifdelete_invalidisNoneelsedelete_invalid
+
+ iftestand(
+ delete_aggregateordelete_failureordelete_smtp_tlsordelete_invalid
+ ):
+ raiseValueError("delete options and test are mutually exclusive")
+
+ ifmax_unsaved_retries<0:
+ raiseValueError("max_unsaved_retries must be >= 0")ifconnectionisNone:raiseValueError("Must supply a connection")
+ cfg=_resolve_config(
+ config,
+ offline=offline,
+ ip_db_path=ip_db_path,
+ always_use_local_files=always_use_local_files,
+ reverse_dns_map_path=reverse_dns_map_path,
+ reverse_dns_map_url=reverse_dns_map_url,
+ nameservers=nameservers,
+ dns_timeout=dns_timeout,
+ dns_retries=dns_retries,
+ strip_attachment_payloads=strip_attachment_payloads,
+ normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
+ )
+
# current_time useful to fetch_messages later in the programcurrent_time:datetime|date|str|None=Noneaggregate_reports:list[AggregateReport]=[]failure_reports:list[FailureReport]=[]smtp_tls_reports:list[SMTPTLSReport]=[]
+ # This call's own reports, kept separate from the accumulated lists
+ # above (which carry earlier batches' reports in via ``results``) so the
+ # save callback is handed only what this batch parsed.
+ batch_aggregate_reports:list[AggregateReport]=[]
+ batch_failure_reports:list[FailureReport]=[]
+ batch_smtp_tls_reports:list[SMTPTLSReport]=[]
+ # Aggregate dedup keys are staged here and only written to the shared
+ # cache once the batch is known to be saved, so an unsaved batch (or a
+ # mid-batch crash) leaves the cache clean and the reports are reparsed
+ # on the retry instead of being dropped as duplicates.
+ pending_aggregate_keys:set[str]=set()aggregate_report_msg_uids=[]failure_report_msg_uids=[]smtp_tls_msg_uids=[]
@@ -2413,6 +2827,7 @@
failure_reports_folder=f"{archive_folder}/Failure"smtp_tls_reports_folder=f"{archive_folder}/SMTP-TLS"invalid_reports_folder=f"{archive_folder}/Invalid"
+ unsaved_reports_folder=f"{archive_folder}/Unsaved"ifresults:aggregate_reports=results["aggregate_reports"].copy()
@@ -2426,6 +2841,11 @@
connection.create_folder(failure_reports_folder)connection.create_folder(smtp_tls_reports_folder)connection.create_folder(invalid_reports_folder)
+ ifsave_callbackisnotNone:
+ # Only reachable when a callback can report a batch unsaved;
+ # without one no message is ever held back, so the folder would
+ # sit empty in every mailbox.
+ connection.create_folder(unsaved_reports_folder)ifsinceandisinstance(since,str):_since=1440# default one day
@@ -2481,138 +2901,246 @@
logger.debug(f"Processing {message_limit} messages")
- foriinrange(message_limit):
- msg_uid=messages[i]
- logger.debug(f"Processing message {i+1} of {message_limit}: UID {msg_uid}")
- message_id:int|str
- ifisinstance(connection,IMAPConnection):
- message_id=int(msg_uid)
- msg_content=connection.fetch_message(message_id)
- elifisinstance(connection,MSGraphConnection):
- message_id=str(msg_uid)
- msg_content=connection.fetch_message(message_id,mark_read=nottest)
- elifisinstance(connection,MaildirConnection):
- message_id=str(msg_uid)ifnotisinstance(msg_uid,str)elsemsg_uid
- msg_content=connection.fetch_message(message_id,mark_read=nottest)
- else:
- message_id=str(msg_uid)ifnotisinstance(msg_uid,str)elsemsg_uid
- msg_content=connection.fetch_message(message_id)
+ ifn_procs>1andmessage_limit>1:
+ fromparsedmarc.parallelimport_parse_report_email_job,parallel_map
+
+ # The config's caches (ip_address_cache, seen_aggregate_report_ids,
+ # reverse_dns_map) never cross the process boundary --
+ # ParserConfig.__getstate__ drops them, and each worker accumulates
+ # its own via the module defaults it rebinds to on unpickling (see
+ # ParserConfig.__setstate__). keep_alive is a bound method of the
+ # live connection object and is not a ParserConfig field, so it is
+ # never submitted to the pool either; the heartbeat passed to
+ # parallel_map below keeps the connection alive instead.
+ func=functools.partial(_parse_report_email_job,config=cfg)
+
+ # parallel_map yields results in submission order, so the oldest
+ # queued id always belongs to the next yielded result; popping as
+ # results arrive keeps this queue no larger than the in-flight
+ # submission window.
+ fetched_ids:deque[int|str]=deque()
+ invalid_msg_ids:list[int|str]=[]
+
+ def_jobs():
+ foriinrange(message_limit):
+ msg_uid=messages[i]
+ logger.debug(
+ f"Processing message {i+1} of {message_limit}: UID {msg_uid}"
+ )
+ message_id,msg_content=_fetch_mailbox_message(
+ connection,msg_uid,test
+ )
+ fetched_ids.append(message_id)
+ yieldmsg_content
+
+ forresultinparallel_map(
+ func,_jobs(),n_procs,heartbeat=connection.keepalive
+ ):
+ message_id=fetched_ids.popleft()
+ ifisinstance(result,ParserError):
+ logger.warning(str(result))
+ invalid_msg_ids.append(message_id)
+ else:
+ report_type=_classify_parsed_email(
+ result,
+ batch_aggregate_reports,
+ batch_failure_reports,
+ batch_smtp_tls_reports,
+ seen_aggregate_report_ids=cfg.seen_aggregate_report_ids,
+ pending_aggregate_keys=pending_aggregate_keys,
+ )
+ ifreport_type=="aggregate":
+ aggregate_report_msg_uids.append(message_id)
+ elifreport_type=="failure":
+ failure_report_msg_uids.append(message_id)
+ elifreport_type=="smtp_tls":
+ smtp_tls_msg_uids.append(message_id)
+
+ ifnottest:
+ forinvalid_message_idininvalid_msg_ids:
+ _dispose_invalid_message(
+ connection,
+ invalid_message_id,
+ delete_invalid,
+ invalid_reports_folder,
+ )
+ else:
+ foriinrange(message_limit):
+ msg_uid=messages[i]
+ logger.debug(
+ f"Processing message {i+1} of {message_limit}: UID {msg_uid}"
+ )
+ message_id,msg_content=_fetch_mailbox_message(connection,msg_uid,test)
+ try:
+ parsed_email=parse_report_email(
+ msg_content,
+ config=cfg,
+ keep_alive=connection.keepalive,
+ )
+ report_type=_classify_parsed_email(
+ parsed_email,
+ batch_aggregate_reports,
+ batch_failure_reports,
+ batch_smtp_tls_reports,
+ seen_aggregate_report_ids=cfg.seen_aggregate_report_ids,
+ pending_aggregate_keys=pending_aggregate_keys,
+ )
+ ifreport_type=="aggregate":
+ aggregate_report_msg_uids.append(message_id)
+ elifreport_type=="failure":
+ failure_report_msg_uids.append(message_id)
+ elifreport_type=="smtp_tls":
+ smtp_tls_msg_uids.append(message_id)
+ exceptParserErroraserror:
+ logger.warning(error.__str__())
+ ifnottest:
+ _dispose_invalid_message(
+ connection,message_id,delete_invalid,invalid_reports_folder
+ )
+
+ # Ask the caller whether this batch actually made it to its output
+ # destinations before touching a single message. A callback that says
+ # otherwise (or raises) means the reports exist nowhere else yet, so no
+ # message may be archived or deleted -- only retained for retry, or moved
+ # intact to the Unsaved folder once retries run out (#242).
+ batch_results:ParsingResults={
+ "aggregate_reports":batch_aggregate_reports,
+ "failure_reports":batch_failure_reports,
+ "smtp_tls_reports":batch_smtp_tls_reports,
+ }
+ persisted=True
+ callback_error:Exception|None=None
+ ifsave_callbackisnotNone:try:
- sa=strip_attachment_payloads
- parsed_email=parse_report_email(
- msg_content,
- nameservers=nameservers,
- dns_timeout=dns_timeout,
- dns_retries=dns_retries,
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- offline=offline,
- strip_attachment_payloads=sa,
- keep_alive=connection.keepalive,
- normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
- )
- ifparsed_email["report_type"]=="aggregate":
- report_org=parsed_email["report"]["report_metadata"]["org_name"]
- report_id=parsed_email["report"]["report_metadata"]["report_id"]
- report_key=f"{report_org}_{report_id}"
- ifreport_keynotinSEEN_AGGREGATE_REPORT_IDS:
- SEEN_AGGREGATE_REPORT_IDS[report_key]=True
- aggregate_reports.append(parsed_email["report"])
- else:
- logger.debug(
- f"Skipping duplicate aggregate report with ID: {report_id}"
- )
- aggregate_report_msg_uids.append(message_id)
- elifparsed_email["report_type"]=="failure":
- failure_reports.append(parsed_email["report"])
- failure_report_msg_uids.append(message_id)
- elifparsed_email["report_type"]=="smtp_tls":
- smtp_tls_reports.append(parsed_email["report"])
- smtp_tls_msg_uids.append(message_id)
- exceptParserErroraserror:
- logger.warning(error.__str__())
- ifnottest:
- ifdelete:
- logger.debug(f"Deleting message UID {msg_uid}")
- ifisinstance(connection,IMAPConnection):
- connection.delete_message(int(message_id))
- else:
- connection.delete_message(str(message_id))
- else:
- logger.debug(
- f"Moving message UID {msg_uid} to {invalid_reports_folder}"
- )
- ifisinstance(connection,IMAPConnection):
- connection.move_message(int(message_id),invalid_reports_folder)
- else:
- connection.move_message(str(message_id),invalid_reports_folder)
+ persisted=save_callback(batch_results)isnotFalse
+ exceptExceptionaserror:
+ # A raising callback is a failed save too: the failure
+ # bookkeeping below still runs (count the attempt, hold or shelve
+ # the messages) before the exception is re-raised, so a callback
+ # that always raises -- e.g. the CLI's under
+ # ``fail_on_output_error`` -- is still bounded by
+ # ``max_unsaved_retries`` even when the caller's watch loop
+ # swallows the exception and keeps checking, as mailsuite's IMAP
+ # and Maildir backends do.
+ persisted=False
+ callback_error=error
- ifnottest:
- ifdelete:
- processed_messages=(
- aggregate_report_msg_uids+failure_report_msg_uids+smtp_tls_msg_uids
- )
+ batch_msg_uids=(
+ aggregate_report_msg_uids+failure_report_msg_uids+smtp_tls_msg_uids
+ )
- number_of_processed_msgs=len(processed_messages)
- foriinrange(number_of_processed_msgs):
- msg_uid=processed_messages[i]
- logger.debug(
- f"Deleting message {i+1} of {number_of_processed_msgs}: UID {msg_uid}"
- )
+ ifpersisted:
+ # Committing: the reports are safely stored elsewhere, so record
+ # their dedup keys and forget any earlier failures for these
+ # messages.
+ forreport_keyinpending_aggregate_keys:
+ cfg.seen_aggregate_report_ids[report_key]=True
+ ifnottest:
+ formsg_uidinbatch_msg_uids:
+ _FAILED_SAVE_ATTEMPTS.pop((reports_folder,str(msg_uid)),None)
+ elifnottest:
+ # Held back for retry. Messages that have now failed the initial
+ # attempt plus ``max_unsaved_retries`` retries stop being retried and
+ # move to the Unsaved folder, bounding duplicate delivery to output
+ # destinations that do not deduplicate; the rest stay put.
+ retained_uids:list[int|str]=[]
+ over_cap_uids:list[int|str]=[]
+ highest_attempt=0
+ formsg_uidinbatch_msg_uids:
+ attempts_key=(reports_folder,str(msg_uid))
+ attempts=_FAILED_SAVE_ATTEMPTS.get(attempts_key,0)+1
+ _FAILED_SAVE_ATTEMPTS[attempts_key]=attempts
+ ifattempts>max_unsaved_retries:
+ over_cap_uids.append(msg_uid)
+ else:
+ retained_uids.append(msg_uid)
+ highest_attempt=max(highest_attempt,attempts)
+ ifretained_uids:
+ logger.error(
+ f"Reports were not saved: leaving {len(retained_uids)} "
+ f"message(s) in {reports_folder} to retry on the next run "
+ f"or check (failed attempt {highest_attempt} of "
+ f"{max_unsaved_retries+1})"
+ )
+ ifover_cap_uids:
+ logger.error(
+ f"Reports were not saved after {max_unsaved_retries+1} "
+ f"attempt(s): moving {len(over_cap_uids)} message(s) from "
+ f"{reports_folder} to {unsaved_reports_folder} instead of "
+ "retrying them further. They are never deleted -- fix the "
+ f"output destination, then move them back to {reports_folder}"
+ )
+ _ensure_folder(connection,unsaved_reports_folder)
+ formsg_uidinover_cap_uids:try:
- connection.delete_message(msg_uid)
-
+ connection.move_message(msg_uid,unsaved_reports_folder)exceptExceptionase:
- message="Error deleting message UID"
- e=f"{message}{msg_uid}: {e}"
+ e=f"Error moving message UID {msg_uid}: {e}"logger.error(f"Mailbox error: {e}")
- else:
- iflen(aggregate_report_msg_uids)>0:
- log_message="Moving aggregate report messages from"
- logger.debug(
- f"{log_message}{reports_folder} to {aggregate_reports_folder}"
- )
- number_of_agg_report_msgs=len(aggregate_report_msg_uids)
- foriinrange(number_of_agg_report_msgs):
- msg_uid=aggregate_report_msg_uids[i]
+ else:
+ # Drop the counter only once the message is actually out
+ # of the retry loop. Clearing it before a failed move
+ # would hand the still-in-place message a fresh set of
+ # under-cap retries (and deliveries); keeping it means
+ # the next failed save classifies the message over-cap
+ # again and re-attempts the move instead.
+ _FAILED_SAVE_ATTEMPTS.pop((reports_folder,str(msg_uid)),None)
+
+ ifcallback_errorisnotNone:
+ raisecallback_error
+
+ aggregate_reports+=batch_aggregate_reports
+ failure_reports+=batch_failure_reports
+ smtp_tls_reports+=batch_smtp_tls_reports
+
+ ifpersistedandnottest:
+ # Each report type is disposed of according to its own effective
+ # delete flag: deleted outright, or moved to its archive subfolder.
+ formsg_uids,delete_type,destination_folder,labelin(
+ (
+ aggregate_report_msg_uids,
+ delete_aggregate,
+ aggregate_reports_folder,
+ "aggregate report",
+ ),
+ (
+ failure_report_msg_uids,
+ delete_failure,
+ failure_reports_folder,
+ "failure report",
+ ),
+ (
+ smtp_tls_msg_uids,
+ delete_smtp_tls,
+ smtp_tls_reports_folder,
+ "SMTP TLS report",
+ ),
+ ):
+ number_of_msgs=len(msg_uids)
+ ifnumber_of_msgs==0:
+ continue
+ ifnotdelete_type:
+ message=f"Moving {label} messages from"
+ logger.debug(f"{message}{reports_folder} to {destination_folder}")
+ foriinrange(number_of_msgs):
+ msg_uid=msg_uids[i]
+ ifdelete_type:logger.debug(
- f"Moving message {i+1} of {number_of_agg_report_msgs}: UID {msg_uid}"
+ f"Deleting message {i+1} of {number_of_msgs}: UID {msg_uid}")try:
- connection.move_message(msg_uid,aggregate_reports_folder)
+ connection.delete_message(msg_uid)exceptExceptionase:
- message="Error moving message UID"
+ message="Error deleting message UID"e=f"{message}{msg_uid}: {e}"logger.error(f"Mailbox error: {e}")
- iflen(failure_report_msg_uids)>0:
- message="Moving failure report messages from"
- logger.debug(f"{message}{reports_folder} to {failure_reports_folder}")
- number_of_failure_msgs=len(failure_report_msg_uids)
- foriinrange(number_of_failure_msgs):
- msg_uid=failure_report_msg_uids[i]
+ else:message="Moving message"logger.debug(
- f"{message}{i+1} of {number_of_failure_msgs}: UID {msg_uid}"
+ f"{message}{i+1} of {number_of_msgs}: UID {msg_uid}")try:
- connection.move_message(msg_uid,failure_reports_folder)
- exceptExceptionase:
- e=f"Error moving message UID {msg_uid}: {e}"
- logger.error(f"Mailbox error: {e}")
- iflen(smtp_tls_msg_uids)>0:
- message="Moving SMTP TLS report messages from"
- logger.debug(f"{message}{reports_folder} to {smtp_tls_reports_folder}")
- number_of_smtp_tls_uids=len(smtp_tls_msg_uids)
- foriinrange(number_of_smtp_tls_uids):
- msg_uid=smtp_tls_msg_uids[i]
- message="Moving message"
- logger.debug(
- f"{message}{i+1} of {number_of_smtp_tls_uids}: UID {msg_uid}"
- )
- try:
- connection.move_message(msg_uid,smtp_tls_reports_folder)
+ connection.move_message(msg_uid,destination_folder)exceptExceptionase:e=f"Error moving message UID {msg_uid}: {e}"logger.error(f"Mailbox error: {e}")
@@ -2622,7 +3150,11 @@
"smtp_tls_reports":smtp_tls_reports,}
- ifnottestandnotbatch_size:
+ # An unsaved batch left its messages in ``reports_folder``, so the
+ # re-check below would find them again and immediately reprocess the
+ # very messages that just failed -- burning through the retry cap in one
+ # call. Skip it and let the next run retry them.
+ ifpersistedandnottestandnotbatch_size:ifcurrent_time:total_messages=len(connection.fetch_messages(reports_folder,since=current_time)
@@ -2639,19 +3171,17 @@
reports_folder=reports_folder,archive_folder=archive_folder,delete=delete,
+ delete_aggregate=delete_aggregate,
+ delete_failure=delete_failure,
+ delete_smtp_tls=delete_smtp_tls,
+ delete_invalid=delete_invalid,test=test,
- nameservers=nameservers,
- dns_timeout=dns_timeout,
- dns_retries=dns_retries,
- strip_attachment_payloads=strip_attachment_payloads,results=results,
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- offline=offline,since=current_time,
- normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
+ n_procs=n_procs,
+ save_callback=save_callback,
+ max_unsaved_retries=max_unsaved_retries,
+ config=cfg,)returnresults
@@ -2667,6 +3197,10 @@
reports_folder:str="INBOX",archive_folder:str="Archive",delete:bool=False,
+ delete_aggregate:bool|None=None,
+ delete_failure:bool|None=None,
+ delete_smtp_tls:bool|None=None,
+ delete_invalid:bool|None=None,test:bool=False,check_timeout:int=30,ip_db_path:str|None=None,
@@ -2675,13 +3209,16 @@
reverse_dns_map_url:str|None=None,offline:bool=False,nameservers:list[str]|None=None,
- dns_timeout:float=6.0,
+ dns_timeout:float=DEFAULT_DNS_TIMEOUT,dns_retries:int=DEFAULT_DNS_MAX_RETRIES,strip_attachment_payloads:bool=False,batch_size:int=10,since:datetime|date|str|None=None,
- normalize_timespan_threshold_hours:float=24,
+ normalize_timespan_threshold_hours:float=24.0,config_reloading:Callable|None=None,
+ n_procs:int=1,
+ max_unsaved_retries:int=2,
+ config:ParserConfig|None=None,):""" Watches the mailbox for new messages and
@@ -2689,10 +3226,40 @@
Args: mailbox_connection: The mailbox connection object
- callback: The callback function to receive the parsing results
+ callback: The callback function to receive the parsing results.
+ Passed straight through to ``get_dmarc_reports_from_mailbox()``
+ as its ``save_callback``, so it now runs once per fetched batch,
+ with only that batch's reports, *before* those messages are
+ deleted or moved out of ``reports_folder`` -- rather than once
+ afterward with the whole check's accumulated results. Returning
+ ``False`` reports the batch as unsaved, leaving its messages in
+ place to be retried on the next check instead of archived or
+ deleted (see ``save_callback`` and ``max_unsaved_retries`` on
+ ``get_dmarc_reports_from_mailbox()``). Raising counts as an
+ unsaved batch too -- same retention and retry cap -- before the
+ exception reaches the mailbox backend's watch loop; what happens
+ then is backend-specific: the Microsoft Graph and Gmail backends
+ let it propagate and end the watch, while mailsuite's IMAP and
+ Maildir watch loops log it and keep checking. reports_folder (str): The IMAP folder where reports can be found archive_folder (str): The folder to move processed mail to
- delete (bool): Delete messages after processing them
+ delete (bool): Delete messages after processing them
+ delete_aggregate (bool | None): Delete aggregate report messages
+ after processing them, instead of moving them to the
+ ``Aggregate`` archive subfolder; ``None`` (the default) inherits
+ the value of ``delete``
+ delete_failure (bool | None): Delete failure report messages after
+ processing them, instead of moving them to the ``Failure``
+ archive subfolder; ``None`` (the default) inherits the value of
+ ``delete``
+ delete_smtp_tls (bool | None): Delete SMTP TLS report messages after
+ processing them, instead of moving them to the ``SMTP-TLS``
+ archive subfolder; ``None`` (the default) inherits the value of
+ ``delete``
+ delete_invalid (bool | None): Delete unparseable messages, instead
+ of moving them to the ``Invalid`` archive subfolder where they
+ can be inspected for debugging; ``None`` (the default) inherits
+ the value of ``delete`` test (bool): Do not move or delete messages after processing them check_timeout (int): Number of seconds to wait for a IMAP IDLE response or the number of seconds until the next mail check
@@ -2715,30 +3282,64 @@
reload (or shutdown) has been requested (e.g. via SIGHUP/SIGTERM). Polled by the mailbox backend between checks, including the IMAP IDLE loop, so the watcher exits cleanly at a safe boundary.
+ n_procs (int): Number of processes to use for parsing messages in
+ parallel. Passed through to ``get_dmarc_reports_from_mailbox``
+ on each check. Not part of ``config``; always applies.
+ max_unsaved_retries (int): How many times a message may be retried
+ after ``callback`` first reported its batch unsaved, before it is
+ moved to the ``Unsaved`` archive subfolder (default 2). Passed
+ through to ``get_dmarc_reports_from_mailbox``, where it is
+ documented in full. Not part of ``config``; always applies.
+ config (ParserConfig): a single object carrying all parsing and
+ enrichment options plus the caches; when provided, it replaces
+ the individual parsing and enrichment option keyword arguments
+ listed above (DNS, GeoIP, offline mode, attachment payload
+ stripping, timespan normalization), whose values are then
+ ignored. The remaining keyword arguments control mailbox
+ handling and orchestration rather than parsing (the folder
+ names, the ``delete`` options, ``test``, ``since``,
+ ``batch_size``, ``max_unsaved_retries``); they are not part of
+ ``config`` and always apply. """
+ # Validate before the watch loop starts: raised inside a check, this
+ # would be swallowed and endlessly retried by the IMAP and Maildir
+ # backends' per-check exception handling instead of surfacing.
+ ifmax_unsaved_retries<0:
+ raiseValueError("max_unsaved_retries must be >= 0")
+
+ cfg=_resolve_config(
+ config,
+ offline=offline,
+ ip_db_path=ip_db_path,
+ always_use_local_files=always_use_local_files,
+ reverse_dns_map_path=reverse_dns_map_path,
+ reverse_dns_map_url=reverse_dns_map_url,
+ nameservers=nameservers,
+ dns_timeout=dns_timeout,
+ dns_retries=dns_retries,
+ strip_attachment_payloads=strip_attachment_payloads,
+ normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
+ )defcheck_callback(connection):
- res=get_dmarc_reports_from_mailbox(
+ get_dmarc_reports_from_mailbox(connection=connection,reports_folder=reports_folder,archive_folder=archive_folder,delete=delete,
+ delete_aggregate=delete_aggregate,
+ delete_failure=delete_failure,
+ delete_smtp_tls=delete_smtp_tls,
+ delete_invalid=delete_invalid,test=test,
- ip_db_path=ip_db_path,
- always_use_local_files=always_use_local_files,
- reverse_dns_map_path=reverse_dns_map_path,
- reverse_dns_map_url=reverse_dns_map_url,
- offline=offline,
- nameservers=nameservers,
- dns_timeout=dns_timeout,
- dns_retries=dns_retries,
- strip_attachment_payloads=strip_attachment_payloads,batch_size=batch_size,since=since,create_folders=False,
- normalize_timespan_threshold_hours=normalize_timespan_threshold_hours,
+ n_procs=n_procs,
+ save_callback=callback,
+ max_unsaved_retries=max_unsaved_retries,
+ config=cfg,)
- callback(res)watch_kwargs:dict={"check_callback":check_callback,
diff --git a/_modules/parsedmarc/config.html b/_modules/parsedmarc/config.html
new file mode 100644
index 00000000..4e75979c
--- /dev/null
+++ b/_modules/parsedmarc/config.html
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
+ parsedmarc.config — parsedmarc 10.4.0 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# -*- coding: utf-8 -*-
+
+"""Centralized parser configuration (GitHub issue #503).
+
+This module defines :class:`ParserConfig`, a single frozen dataclass that
+carries every parsing/enrichment option (offline mode, IP database path,
+reverse DNS map location, PSL overrides location, DNS behavior, etc.) plus
+the three caches that the parser and enrichment code share across calls:
+the IP address info cache, the seen-aggregate-report-ID cache, and the
+reverse DNS map.
+
+``parsedmarc/__init__.py`` re-exports :data:`IP_ADDRESS_CACHE`,
+:data:`SEEN_AGGREGATE_REPORT_IDS`, and :data:`REVERSE_DNS_MAP` under the
+same names for backward compatibility. That compatibility is
+identity-based: ``parsedmarc.IP_ADDRESS_CACHE is parsedmarc.config.IP_ADDRESS_CACHE``
+must hold, not merely equal contents, since callers may mutate the caches
+in place.
+
+Only :mod:`parsedmarc.constants` and :mod:`parsedmarc.utils` are imported
+from the package here — importing :mod:`parsedmarc` itself would be
+circular, since ``parsedmarc/__init__.py`` imports from this module.
+"""
+
+from__future__importannotations
+
+fromdataclassesimportdataclass,field,fields
+
+fromexpiringdictimportExpiringDict
+
+fromparsedmarc.constantsimportDEFAULT_DNS_MAX_RETRIES,DEFAULT_DNS_TIMEOUT
+fromparsedmarc.utilsimportReverseDNSMap
+
+
+def_new_ip_address_cache()->ExpiringDict:
+"""Build a fresh IP-address-info cache (4 hour expiry)."""
+ returnExpiringDict(max_len=10000,max_age_seconds=14400)
+
+
+def_new_seen_aggregate_report_ids()->ExpiringDict:
+"""Build a fresh seen-aggregate-report-ID cache (1 hour expiry)."""
+ returnExpiringDict(max_len=100000000,max_age_seconds=3600)
+
+
+# Canonical default caches. parsedmarc/__init__.py re-exports these three
+# objects under the same names (IP_ADDRESS_CACHE, SEEN_AGGREGATE_REPORT_IDS,
+# REVERSE_DNS_MAP) for backward compatibility with code that imported them
+# directly from the top-level package. That compatibility promise is
+# identity-based — the re-exported names must point at these very same
+# objects, not merely equivalent ones — so that library callers who obtained
+# a reference before this refactor keep observing the same mutations as code
+# that goes through a ParserConfig built from these defaults (e.g. during
+# unpickling in a multiprocessing worker; see ParserConfig.__setstate__).
+IP_ADDRESS_CACHE=_new_ip_address_cache()
+SEEN_AGGREGATE_REPORT_IDS=_new_seen_aggregate_report_ids()
+REVERSE_DNS_MAP:ReverseDNSMap={}
+
+# The ParserConfig fields excluded from pickling; every other field must
+# survive the round-trip, so __getstate__ derives its contents from
+# dataclasses.fields() rather than enumerating option fields by hand.
+_CACHE_FIELD_NAMES=(
+ "ip_address_cache",
+ "seen_aggregate_report_ids",
+ "reverse_dns_map",
+)
+
+
+
+[docs]
+@dataclass(frozen=True)
+classParserConfig:
+"""Carries all parsing/enrichment options, plus the caches they share.
+
+ When passed as ``config=`` to parsedmarc's public parsing functions, the
+ individual option keyword arguments (``offline``, ``ip_db_path``,
+ ``nameservers``, etc.) are ignored in favor of the values carried on this
+ object.
+
+ Every explicitly constructed ``ParserConfig()`` owns fresh, isolated
+ cache objects (``ip_address_cache``, ``seen_aggregate_report_ids``,
+ ``reverse_dns_map``) via ``default_factory`` — two independently
+ constructed configs never share cache state. To derive a variant of an
+ existing config that *does* keep sharing its caches (e.g. to override
+ ``offline`` for one call while still benefiting from warm caches), use
+ ``dataclasses.replace(cfg, ...)``: since every field, including the three
+ cache fields, is an init field, ``replace()`` copies the source's cache
+ objects onto the new instance rather than constructing fresh ones.
+
+ Pickling (as happens when a config is captured in a
+ ``functools.partial`` payload submitted to a multiprocessing worker)
+ intentionally drops cache *contents*: ``__getstate__`` omits the three
+ cache fields, and ``__setstate__`` rebinds them to this module's
+ :data:`IP_ADDRESS_CACHE`, :data:`SEEN_AGGREGATE_REPORT_IDS`, and
+ :data:`REVERSE_DNS_MAP` — the unpickling process's module-default
+ caches — rather than either the sender's cache contents (which would be
+ expensive and stale to serialize per task) or brand new empty caches per
+ unpickle (which would silently defeat per-worker caching, since a
+ ``functools.partial`` payload is re-pickled for every task submitted to a
+ worker). Binding the module defaults means a freshly spawned worker
+ interpreter starts with empty caches and accumulates hits across the
+ tasks it handles, matching pre-refactor behavior.
+
+ Note that ``keep_alive`` and ``n_procs`` are deliberately **not** fields
+ on this class: those control process/worker orchestration, not parsing
+ or enrichment behavior.
+
+ Attributes:
+ offline: Do not make online requests (DNS, IP database download,
+ reverse DNS map download, PSL overrides download).
+ ip_db_path: Path to a local MMDB file from IPinfo, MaxMind, or DBIP.
+ always_use_local_files: Always use local/bundled files instead of
+ downloading the IP database, reverse DNS map, or PSL overrides.
+ reverse_dns_map_path: Path to a local reverse DNS map file.
+ reverse_dns_map_url: URL to a reverse DNS map file.
+ psl_overrides_path: Path to a local PSL overrides file.
+ psl_overrides_url: URL to a PSL overrides file.
+ nameservers: A list of one or more nameservers to use for DNS
+ queries (Cloudflare's public DNS resolvers by default).
+ dns_timeout: DNS query timeout, in seconds.
+ dns_retries: Number of times to retry a DNS query after a timeout or
+ other transient error.
+ strip_attachment_payloads: Remove attachment payloads from parsed
+ email results.
+ normalize_timespan_threshold_hours: Aggregate reports whose
+ timespan exceeds this many hours are normalized/split.
+ ip_address_cache: Cache of IP address enrichment results.
+ seen_aggregate_report_ids: Cache of already-seen aggregate report
+ IDs, used for de-duplication.
+ reverse_dns_map: The reverse DNS map used to classify base domains
+ found via reverse DNS.
+ """
+
+ offline:bool=False
+ ip_db_path:str|None=None
+ always_use_local_files:bool=False
+ reverse_dns_map_path:str|None=None
+ reverse_dns_map_url:str|None=None
+ psl_overrides_path:str|None=None
+ psl_overrides_url:str|None=None
+ nameservers:list[str]|None=None
+ dns_timeout:float=DEFAULT_DNS_TIMEOUT
+ dns_retries:int=DEFAULT_DNS_MAX_RETRIES
+ strip_attachment_payloads:bool=False
+ normalize_timespan_threshold_hours:float=24.0
+ ip_address_cache:ExpiringDict=field(
+ default_factory=_new_ip_address_cache,repr=False,compare=False
+ )
+ seen_aggregate_report_ids:ExpiringDict=field(
+ default_factory=_new_seen_aggregate_report_ids,repr=False,compare=False
+ )
+ reverse_dns_map:ReverseDNSMap=field(
+ default_factory=dict,repr=False,compare=False
+ )
+
+ def__getstate__(self)->dict[str,object]:
+"""Return picklable state, excluding the three cache fields.
+
+ Cache contents are process-local and potentially huge; they are
+ never serialized. See the class docstring for the full rationale.
+ """
+ return{
+ f.name:getattr(self,f.name)
+ forfinfields(self)
+ iff.namenotin_CACHE_FIELD_NAMES
+ }
+
+ def__setstate__(self,state:dict[str,object])->None:
+"""Restore option fields, then rebind caches to this module's
+ process-wide defaults (never fresh empty caches — see the class
+ docstring for why that would silently break per-worker caching).
+
+ Non-cache fields are first initialized to their class defaults, so
+ unpickling a ``ParserConfig`` serialized by an older parsedmarc
+ version (whose state predates fields added since) leaves the newer
+ fields at their defaults instead of unset entirely — ``__init__``
+ never runs during unpickling, so without this an absent field would
+ raise ``AttributeError`` on first access.
+ """
+ forfinfields(self):
+ iff.namenotin_CACHE_FIELD_NAMES:
+ object.__setattr__(self,f.name,f.default)
+ forkey,valueinstate.items():
+ object.__setattr__(self,key,value)
+ object.__setattr__(self,"ip_address_cache",IP_ADDRESS_CACHE)
+ object.__setattr__(self,"seen_aggregate_report_ids",SEEN_AGGREGATE_REPORT_IDS)
+ object.__setattr__(self,"reverse_dns_map",REVERSE_DNS_MAP)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/parsedmarc/elastic.html b/_modules/parsedmarc/elastic.html
index 64c75855..fe082209 100644
--- a/_modules/parsedmarc/elastic.html
+++ b/_modules/parsedmarc/elastic.html
@@ -5,14 +5,14 @@
- parsedmarc.elastic — parsedmarc 10.3.0 documentation
+ parsedmarc.elastic — parsedmarc 10.4.0 documentation
-
+
diff --git a/_modules/parsedmarc/opensearch.html b/_modules/parsedmarc/opensearch.html
index 64ab6760..bd653086 100644
--- a/_modules/parsedmarc/opensearch.html
+++ b/_modules/parsedmarc/opensearch.html
@@ -5,14 +5,14 @@
- parsedmarc.opensearch — parsedmarc 10.3.0 documentation
+ parsedmarc.opensearch — parsedmarc 10.4.0 documentation
-
+
diff --git a/_modules/parsedmarc/splunk.html b/_modules/parsedmarc/splunk.html
index 877f5cef..e2e53ae6 100644
--- a/_modules/parsedmarc/splunk.html
+++ b/_modules/parsedmarc/splunk.html
@@ -5,14 +5,14 @@
- parsedmarc.splunk — parsedmarc 10.3.0 documentation
+ parsedmarc.splunk — parsedmarc 10.4.0 documentation
-
+
diff --git a/_modules/parsedmarc/types.html b/_modules/parsedmarc/types.html
index 3279dc35..6529583e 100644
--- a/_modules/parsedmarc/types.html
+++ b/_modules/parsedmarc/types.html
@@ -5,14 +5,14 @@
- parsedmarc.types — parsedmarc 10.3.0 documentation
+ parsedmarc.types — parsedmarc 10.4.0 documentation
-
+
diff --git a/_modules/parsedmarc/utils.html b/_modules/parsedmarc/utils.html
index b215ff60..06759d90 100644
--- a/_modules/parsedmarc/utils.html
+++ b/_modules/parsedmarc/utils.html
@@ -5,14 +5,14 @@
- parsedmarc.utils — parsedmarc 10.3.0 documentation
+ parsedmarc.utils — parsedmarc 10.4.0 documentation
-
+
@@ -983,6 +983,8 @@
url:str|None=None,offline:bool=False,reverse_dns_map:ReverseDNSMap|None=None,
+ psl_overrides_path:str|None=None,
+ psl_overrides_url:str|None=None,)->ReverseDNSService:""" Returns the service name of a given base domain name from reverse DNS.
@@ -991,9 +993,11 @@
base_domain (str): The base domain of the reverse DNS lookup always_use_local_file (bool): Always use a local map file local_file_path (str): Path to a local map file
- url (str): URL ro a reverse DNS map
+ url (str): URL to a reverse DNS map offline (bool): Use the built-in copy of the reverse DNS map reverse_dns_map (dict): A reverse DNS map
+ psl_overrides_path (str): Path to a local PSL overrides file
+ psl_overrides_url (str): URL to a PSL overrides file Returns: dict: A dictionary containing name and type. If the service is unknown, the name will be
@@ -1014,6 +1018,8 @@
local_file_path=local_file_path,url=url,offline=offline,
+ psl_overrides_path=psl_overrides_path,
+ psl_overrides_url=psl_overrides_url,)service:ReverseDNSService
@@ -1041,6 +1047,8 @@
nameservers:list[str]|None=None,timeout:float=DEFAULT_DNS_TIMEOUT,retries:int=DEFAULT_DNS_MAX_RETRIES,
+ psl_overrides_path:str|None=None,
+ psl_overrides_url:str|None=None,)->IPAddressInfo:""" Returns reverse DNS and country information for the given IP address
@@ -1059,6 +1067,8 @@
timeout (float): Sets the DNS timeout in seconds retries (int): Number of times to retry on timeout or other transient errors
+ psl_overrides_path (str): Path to a local PSL overrides file
+ psl_overrides_url (str): URL to a PSL overrides file Returns: dict: ``ip_address``, ``reverse_dns``, ``country``
@@ -1111,6 +1121,8 @@
url=reverse_dns_map_url,always_use_local_file=always_use_local_files,reverse_dns_map=reverse_dns_map,
+ psl_overrides_path=psl_overrides_path,
+ psl_overrides_url=psl_overrides_url,)info["base_domain"]=base_domaininfo["type"]=service["type"]
@@ -1130,6 +1142,8 @@
local_file_path=reverse_dns_map_path,url=reverse_dns_map_url,offline=offline,
+ psl_overrides_path=psl_overrides_path,
+ psl_overrides_url=psl_overrides_url,)ifinfo["as_domain"]andinfo["as_domain"]inmap_value:service=map_value[info["as_domain"]]
diff --git a/_sources/api.md.txt b/_sources/api.md.txt
index 0db79f0a..b5909533 100644
--- a/_sources/api.md.txt
+++ b/_sources/api.md.txt
@@ -7,6 +7,13 @@
:members:
```
+## parsedmarc.config
+
+```{eval-rst}
+.. automodule:: parsedmarc.config
+ :members:
+```
+
## parsedmarc.elastic
```{eval-rst}
diff --git a/_sources/kibana.md.txt b/_sources/kibana.md.txt
index 8a2be4b7..d9f12b36 100644
--- a/_sources/kibana.md.txt
+++ b/_sources/kibana.md.txt
@@ -38,7 +38,7 @@ valid when a message is forwarded without changing the from address, which is
often caused by a mailbox forwarding rule. This is because DKIM signatures are
part of the message headers, whereas SPF relies on SMTP session headers.
-Underneath the pie charts. you can see graphs of DMARC passage and message
+Underneath the pie charts, you can see graphs of DMARC compliance and message
disposition over time.
Under the graphs you will find the most useful data tables on the dashboard. On
diff --git a/_sources/splunk.md.txt b/_sources/splunk.md.txt
index c884ef8f..80ed5805 100644
--- a/_sources/splunk.md.txt
+++ b/_sources/splunk.md.txt
@@ -18,5 +18,5 @@ The Splunk dashboards display the same content and layout as the
Kibana dashboards, although the Kibana dashboards have slightly
easier and more flexible filtering options.
-[xml files]: https://github.com/domainaware/parsedmarc/tree/master/splunk
+[xml files]: https://github.com/domainaware/parsedmarc/tree/master/dashboards/splunk
[http event collector (hec)]: http://docs.splunk.com/Documentation/Splunk/latest/Data/AboutHEC
diff --git a/_sources/usage.md.txt b/_sources/usage.md.txt
index c6a93ea2..e8374578 100644
--- a/_sources/usage.md.txt
+++ b/_sources/usage.md.txt
@@ -132,6 +132,24 @@ The full set of configuration options are:
payloads from results
- `silent` - bool: Set this to `False` to output results to STDOUT
- `output` - str: Directory to place JSON and CSV files in. This is required if you set either of the JSON output file options.
+ - `archive_directory` - str: Optional. When set, successfully
+ processed report files given as local file/directory path
+ arguments are moved into
+ `////`
+ (year and month come from the report's own begin/arrival date, with
+ the month zero-padded). A successfully parsed report whose archive
+ date can't be determined is left in place with a logged warning.
+ Files that fail to parse as a report are moved to
+ `/Invalid/`; files that fail for other reasons,
+ such as transient I/O errors, are left in place so a later run can
+ retry them. An existing destination file is never overwritten; a
+ numeric suffix is appended before the extension (e.g.
+ `report-1.xml`). This applies only to direct local file input —
+ reports fetched from mailboxes (IMAP, Microsoft Graph, Gmail API,
+ Maildir) use `[mailbox] archive_folder` instead, and mbox files are
+ never moved. Files already inside `archive_directory` are excluded
+ from processing, so the archive may safely live inside an input
+ directory. A failed move is logged and does not stop the run.
- `aggregate_json_filename` - str: filename for the aggregate
JSON output file
- `failure_json_filename` - str: filename for the failure
@@ -168,21 +186,32 @@ The full set of configuration options are:
- `fail_on_output_error` - bool: Exit with a non-zero status code if
any configured output destination fails while saving/publishing
reports (Default: `False`)
+
+ :::{note}
+ This option only controls the process exit code. Retaining mailbox
+ messages whose reports could not be saved is automatic and happens
+ either way — see
+ [Mailbox messages are only archived once the reports are saved](#mailbox-messages-are-only-archived-once-the-reports-are-saved).
+ :::
+
- `log_file` - str: Write log messages to a file at this path
- `n_procs` - int: Number of processes to run in parallel when
- parsing report files passed directly as CLI arguments
+ parsing report files passed directly as CLI arguments, messages
+ in mbox files, and messages from mailbox connections (IMAP,
+ Microsoft Graph, Gmail API, Maildir), including watch mode
(Default: `1`)
:::{note}
Setting this to a number larger than one can improve
- performance when processing thousands of files
+ performance when processing thousands of files or messages
:::
:::{note}
- `n_procs` only applies to report files passed directly on the
- command line. Messages from mbox files and from mailbox
- connections (IMAP, Microsoft Graph, Gmail API, Maildir) are
- always processed sequentially.
+ Only parsing is parallelized across worker processes. Fetching
+ messages, deduplicating reports, archiving/deleting mailbox
+ messages, and saving/publishing to outputs all stay sequential
+ in the main process. Each worker process keeps its own DNS/GeoIP
+ cache.
:::
- `mailbox`
@@ -195,12 +224,46 @@ The full set of configuration options are:
messages as they arrive or poll MS Graph for new messages
- `delete` - bool: Delete messages after processing them,
instead of archiving them
+ - `delete_aggregate` - bool: Delete aggregate report messages
+ after processing them, instead of archiving them
+ (Default: the value of `delete`)
+ - `delete_failure` - bool: Delete failure report messages
+ after processing them, instead of archiving them
+ (Default: the value of `delete`)
+ - `delete_smtp_tls` - bool: Delete SMTP TLS report messages
+ after processing them, instead of archiving them
+ (Default: the value of `delete`)
+ - `delete_invalid` - bool: Delete messages that could not be
+ parsed, instead of archiving them in the `Invalid`
+ subfolder, where they can be inspected for debugging
+ (Default: the value of `delete`)
+
+ :::{note}
+ Each of these four options overrides `delete` for one kind of
+ message only, and the other three keep inheriting `delete`. So
+ `delete = True` combined with `delete_failure = False` archives
+ failure report messages while deleting processed aggregate and
+ SMTP TLS report messages — and unparseable ones, unless
+ `delete_invalid = False` is set as well.
+ :::
+
- `test` - bool: Do not move or delete messages
- `batch_size` - int: Number of messages to read and process
before saving. Default `10`. Use `0` for no limit.
- - `check_timeout` - int: Number of seconds to wait for a IMAP
+ - `check_timeout` - int: Number of seconds to wait for an IMAP
IDLE response or the number of seconds until the next
mail check (Default: `30`)
+ - `max_unsaved_retries` - int: How many times a batch of messages
+ whose reports could not be saved is retried before its messages
+ are moved to the `Unsaved` archive subfolder instead of being
+ retried again (Default: `2`, i.e. the initial attempt plus two
+ retries). Use `0` to move messages on the first failed save;
+ negative values are rejected.
+ Failures are counted in memory, so the cap applies across watch-mode
+ checks within one long-running process, not across separate one-shot
+ runs. See
+ [Mailbox messages are only archived once the reports are saved](#mailbox-messages-are-only-archived-once-the-reports-are-saved)
+ below.
- `since` - str: Search for messages since certain time. (Examples: `5m|3h|2d|1w`)
Acceptable units - {"m":"minutes", "h":"hours", "d":"days", "w":"weeks"}.
Defaults to `1d` if incorrect value is provided.
@@ -806,6 +869,78 @@ PUT _cluster/settings
Increasing this value increases resource usage.
:::
+### Mailbox messages are only archived once the reports are saved
+
+parsedmarc processes a mailbox in batches of `batch_size` messages. Each
+batch is written to every configured output destination *before* any of
+that batch's messages are archived or deleted. If any destination reports a
+failure — an Elasticsearch outage, an expired Splunk HEC token, an
+unreachable Kafka broker, a full `--output` disk — the whole batch is left
+in the reports folder and retried on the next run or watch-mode check, so a
+report is never removed from the mailbox while it exists nowhere else
+([issue #242](https://github.com/domainaware/parsedmarc/issues/242)).
+
+This is all-or-nothing per batch: archiving a batch because most
+destinations accepted it would still permanently lose the data for the one
+that didn't. It also applies regardless of `fail_on_output_error`, which
+only controls the process exit code. Messages that could not be parsed at
+all carry no report data, so they are filed under `Invalid` (or deleted per
+`delete_invalid`) as usual.
+
+A destination that is broken rather than briefly unavailable would
+otherwise be retried forever, so retries are capped. Once a message's batch
+has failed `max_unsaved_retries + 1` times (three times by default), that
+message is moved to `/Unsaved` and stops being retried. **A
+message is never deleted on this path, whatever the `delete` options say.**
+To recover after fixing the output destination, either move the messages
+from `Archive/Unsaved` back into the reports folder, or run parsedmarc once
+with `reports_folder = Archive/Unsaved`.
+
+:::{note}
+The failure counts live in memory, so they are counted per parsedmarc
+process. In watch mode — a long-running process that checks the mailbox
+repeatedly — the cap works as described across checks. A one-shot run
+(`cron`, `systemd` timers) attempts each message exactly once and then
+exits, so its counts start over every time and the default cap is never
+reached: messages simply keep being retried on every run, which is the
+safe direction. Set `max_unsaved_retries = 0` if you want one-shot runs to
+move unsavable messages to `Unsaved` immediately instead.
+:::
+
+:::{warning}
+Retrying a batch means re-sending it. Output destinations that
+deduplicate — Elasticsearch, OpenSearch, and PostgreSQL, which recognize
+an already-saved report — are unaffected, and S3 is idempotent because
+each report is written to an object key built from its type, date, and
+report ID, so a retry overwrites the same object. Kafka,
+Splunk HEC, syslog, GELF, webhooks, Azure Log Analytics, and the
+`--output` JSON/CSV files all append unconditionally, so each retry adds
+another copy of every report in the batch. That is why the default retry
+cap is deliberately low: at most three deliveries per report before its
+message is set aside in `Unsaved`. The summary email covers everything
+parsed in a run, including reports whose batch failed to save, so a report
+retried across runs can also appear in more than one summary email.
+:::
+
+:::{note}
+Not every destination can report a failed delivery. The webhook output
+deliberately logs and swallows its own HTTP and network errors, and the
+syslog and GELF outputs send through Python logging handlers, which
+swallow delivery errors by design — so an unreachable webhook, syslog, or
+GELF endpoint is *not* treated as a failed save and does not hold a
+batch's messages back. Failures in Elasticsearch, OpenSearch, Splunk HEC,
+Kafka, S3, PostgreSQL, Azure Log Analytics, and the `--output` files are
+all detected and do.
+:::
+
+:::{note}
+`since` interacts with retries: a message that ages out of the configured
+`since` window stops being fetched, and therefore stops being retried
+automatically. It is never deleted or moved — it simply stays in the
+reports folder until it is processed by a run with a wider (or no)
+`since` window.
+:::
+
## Environment variable configuration
Any configuration option can be set via environment variables using the
@@ -941,6 +1076,50 @@ For sections with underscores in the name, the full section name is used:
| `gelf` | `PARSEDMARC_GELF_` |
| `webhook` | `PARSEDMARC_WEBHOOK_` |
+## Using parsedmarc as a library
+
+`parsedmarc` is also importable as a regular Python package, not just a CLI
+tool. The main entry points — `parse_report_file()`, `parse_aggregate_report_xml()`,
+`parse_aggregate_report_file()`, `parse_failure_report()`, `parse_report_email()`,
+`get_dmarc_reports_from_mbox()`, `get_dmarc_reports_from_mailbox()`, and
+`watch_inbox()` — are all importable
+directly from the `parsedmarc` package. See the [API reference](api.md) for
+the full set of modules and members.
+
+Each of these functions accepts either individual option keyword arguments
+(`offline`, `nameservers`, `dns_timeout`, etc.) or a single `config=` keyword
+argument carrying a `ParserConfig` instance:
+
+```python
+from parsedmarc import ParserConfig, parse_report_file, get_dmarc_reports_from_mailbox
+from parsedmarc.mail import IMAPConnection
+
+config = ParserConfig(
+ offline=False,
+ nameservers=["1.1.1.1", "1.0.0.1"],
+ dns_timeout=5.0,
+)
+
+report = parse_report_file("aggregate_report.xml.gz", config=config)
+
+connection = IMAPConnection(
+ host="imap.example.com", user="dmarc@example.com", password="..."
+)
+results = get_dmarc_reports_from_mailbox(connection, config=config)
+```
+
+A few things to keep in mind:
+
+- When `config=` is passed, the individual option keyword arguments are
+ ignored in favor of the values carried on the `ParserConfig` instance.
+- Each explicitly constructed `ParserConfig` owns its own isolated caches
+ (IP address info, seen aggregate report IDs, and the reverse DNS map).
+ Omitting `config=` falls back to the process-wide caches shared by every
+ call that doesn't pass one.
+- `keep_alive` and `n_procs` are not part of `ParserConfig` — they control
+ process/worker orchestration rather than parsing or enrichment behavior,
+ so they are always passed as separate keyword arguments.
+
## Performance tuning
For large mailbox imports or backfills, parsedmarc can consume a noticeable amount
@@ -951,8 +1130,12 @@ imports more predictable:
- Reduce `mailbox.batch_size` to smaller values such as `100-500` instead of
processing a very large message set at once. Smaller batches trade throughput
for lower peak memory use and less sink pressure.
-- Keep `n_procs` low for mailbox-heavy runs. In practice, `1-2` workers is often
- a safer starting point for large backfills than aggressive parallelism.
+- `n_procs` now parallelizes parsing for mailbox and mbox runs too, not just
+ report files passed directly as CLI arguments. It pays off most when a run
+ is bound by DNS/GeoIP enrichment rather than fetching or output. The
+ trade-off is memory and DNS load: at most roughly `2 * n_procs` messages
+ are held in flight at once, each worker process keeps its own DNS/GeoIP
+ cache, and DNS query volume can multiply by up to `n_procs`.
- Use `mailbox.since` to process reports in smaller time windows such as `1d`,
`7d`, or another interval that fits the backlog. This makes it easier to catch
up incrementally instead of loading an entire mailbox history in one run.
diff --git a/_static/documentation_options.js b/_static/documentation_options.js
index d5820723..0c200b2d 100644
--- a/_static/documentation_options.js
+++ b/_static/documentation_options.js
@@ -1,5 +1,5 @@
const DOCUMENTATION_OPTIONS = {
- VERSION: '10.3.0',
+ VERSION: '10.4.0',
LANGUAGE: 'en',
COLLAPSE_INDEX: false,
BUILDER: 'html',
diff --git a/api.html b/api.html
index f523849a..e5d1beae 100644
--- a/api.html
+++ b/api.html
@@ -6,14 +6,14 @@
- API reference — parsedmarc 10.3.0 documentation
+ API reference — parsedmarc 10.4.0 documentation
-
+
@@ -88,6 +88,27 @@
reports_folder (str) – The folder where reports can be found
archive_folder (str) – The folder to move processed mail to
-
delete (bool) – Delete messages after processing them
+
delete (bool) – Delete messages after processing them
+
delete_aggregate (bool | None) – Delete aggregate report messages
+after processing them, instead of moving them to the
+Aggregate archive subfolder; None (the default) inherits
+the value of delete
+
delete_failure (bool | None) – Delete failure report messages after
+processing them, instead of moving them to the Failure
+archive subfolder; None (the default) inherits the value of
+delete
+
delete_smtp_tls (bool | None) – Delete SMTP TLS report messages after
+processing them, instead of moving them to the SMTP-TLS
+archive subfolder; None (the default) inherits the value of
+delete
+
delete_invalid (bool | None) – Delete unparseable messages, instead
+of moving them to the Invalid archive subfolder where they
+can be inspected for debugging; None (the default) inherits
+the value of delete
test (bool) – Do not move or delete messages after processing them
ip_db_path (str) – Path to a MMDB file from IPinfo, MaxMind, or DBIP
always_use_local_files (bool) – Do not download files
@@ -376,6 +413,68 @@ failure report results
create_folders (bool) – Whether to create the destination folders
(not used in watch)
normalize_timespan_threshold_hours (float) – Normalize timespans beyond this
+
n_procs (int) – Number of processes to use for parsing messages in
+parallel. Fetching, archiving, and deduplication remain
+sequential in the calling process; only parsing is
+parallelized. With n_procs>1, invalid-message disposition
+happens after the parsing phase completes, rather than
+interleaved message-by-message as it is when n_procs is 1.
+Not part of config; always applies.
+
save_callback –
An optional callable invoked once per fetched batch
+with a ParsingResults dict holding only that batch’s newly
+parsed reports, after parsing but before any of the batch’s
+messages are deleted or moved out of reports_folder. It
+tells this function whether the batch was actually persisted:
+
+
Returning False means “not saved”: the batch’s messages
+are left in reports_folder for retry on the next run (or
+moved to {archive_folder}/Unsaved once they have failed
+max_unsaved_retries retries – see below), and the
+aggregate-report dedup cache is not updated for that batch, so
+a retry reparses the same reports instead of skipping them as
+duplicates.
+
Raising counts as “not saved” too: the same bookkeeping runs
+(the batch’s messages are held back or moved to Unsaved at
+the cap, and the failed attempt counts toward
+max_unsaved_retries), and the exception is then re-raised
+to the caller.
+
Any other return value, including None, commits the batch:
+the dedup cache is updated and the messages are deleted or
+archived exactly as they are with no callback.
+
+
None (the default) commits every batch, preserving the prior
+behavior. The callback is still invoked when test is
+True, so a test run exercises the full pipeline, but neither
+the mailbox nor the retry counters are touched regardless of
+what it returns.
+
+
max_unsaved_retries (int) – How many times a message may be retried
+after save_callback first reported its batch unsaved, before
+it is moved to the Unsaved archive subfolder instead of
+being retried again (default 2, i.e. the initial attempt plus
+two retries – at most three deliveries to any output
+destination that does not deduplicate). 0 moves a message on
+the first failed save; negative values raise ValueError.
+Messages moved to Unsaved are never deleted, whatever the
+delete options say; recover them by fixing the output
+destination and moving them back into reports_folder.
+Counts are kept in memory, per message and
+per process, are reset by a successful save, and are only kept
+when a save_callback is supplied – so the cap applies
+across repeated calls within one process (watch_inbox()’s
+checks), not across separate one-shot processes, each of which
+starts a message’s count over. Mailbox orchestration, so like
+batch_size it is not part of config.
+
config (ParserConfig) – a single object carrying all parsing and
+enrichment options plus the caches; when provided, it replaces
+the individual parsing and enrichment option keyword arguments
+listed above (DNS, GeoIP, offline mode, attachment payload
+stripping, timespan normalization), whose values are then
+ignored. The remaining keyword arguments control mailbox
+handling and orchestration rather than parsing (the folder
+names, the delete options, test, since,
+batch_size, save_callback, max_unsaved_retries);
+they are not part of config and always apply.
Parses a mailbox in mbox format containing e-mails with attached
DMARC reports
@@ -409,6 +508,14 @@ failure report results
ip_db_path (str) – Path to a MMDB file from IPinfo, MaxMind, or DBIP
offline (bool) – Do not make online queries for geolocation or DNS
normalize_timespan_threshold_hours (float) – Normalize timespans beyond this
+
n_procs (int) – Number of processes to use for parsing messages in
+parallel. Message reading, deduplication, and result assembly
+stay in the calling process; only parsing is parallelized. Not
+part of config; always applies.
+
config (ParserConfig) – a single object carrying all parsing and
+enrichment options plus the caches; when provided, the
+individual option keyword arguments listed above are ignored in
+favor of the config’s values.
Parses a file at the given path, a file-like object. or bytes as an
aggregate DMARC report
@@ -456,8 +563,13 @@ aggregate DMARC report
dns_timeout (float) – Sets the DNS timeout in seconds
dns_retries (int) – Number of times to retry DNS queries on timeout
or other transient errors
-
keep_alive (callable) – Keep alive function
+
keep_alive (callable) – Keep alive function. Not part of config;
+always applies.
normalize_timespan_threshold_hours (float) – Normalize timespans beyond this
+
config (ParserConfig) – a single object carrying all parsing and
+enrichment options plus the caches; when provided, the
+individual option keyword arguments listed above are ignored in
+favor of the config’s values.
Parses a DMARC XML report string and returns a consistent dict
Parameters:
@@ -488,8 +600,13 @@ with errors ignored)
timeout (float) – Sets the DNS timeout in seconds
retries (int) – Number of times to retry DNS queries on timeout or
other transient errors
-
keep_alive (callable) – Keep alive function
+
keep_alive (callable) – Keep alive function. Not part of config;
+always applies.
normalize_timespan_threshold_hours (float) – Normalize timespans beyond this
+
config (ParserConfig) – a single object carrying all parsing and
+enrichment options plus the caches; when provided, the
+individual option keyword arguments listed above are ignored in
+favor of the config’s values.
Converts a DMARC failure report and sample to a dict
Parameters:
@@ -523,6 +640,10 @@ other transient errors
or other transient errors
strip_attachment_payloads (bool) – Remove attachment payloads from
failure report results
+
config (ParserConfig) – a single object carrying all parsing and
+enrichment options plus the caches; when provided, the
+individual option keyword arguments listed above are ignored in
+favor of the config’s values.
Converts a DMARC failure report and sample to a dict
Parameters:
@@ -556,6 +677,10 @@ failure report results
or other transient errors
strip_attachment_payloads (bool) – Remove attachment payloads from
failure report results
+
config (ParserConfig) – a single object carrying all parsing and
+enrichment options plus the caches; when provided, the
+individual option keyword arguments listed above are ignored in
+favor of the config’s values.
@@ -586,8 +711,13 @@ failure report results
or other transient errors
strip_attachment_payloads (bool) – Remove attachment payloads from
failure report results
-
keep_alive (callable) – keep alive function
+
keep_alive (callable) – keep alive function. Not part of config;
+always applies.
normalize_timespan_threshold_hours (float) – Normalize timespans beyond this
+
config (ParserConfig) – a single object carrying all parsing and
+enrichment options plus the caches; when provided, the
+individual option keyword arguments listed above are ignored in
+favor of the config’s values.
Parses a DMARC aggregate or failure file at the given path, a
file-like object. or bytes
@@ -625,7 +755,13 @@ failure report results
reverse_dns_map_path (str) – Path to a reverse DNS map
reverse_dns_map_url (str) – URL to a reverse DNS map
offline (bool) – Do not make online queries for geolocation or DNS
-
keep_alive (callable) – Keep alive function
+
keep_alive (callable) – Keep alive function. Not part of config;
+always applies.
+
normalize_timespan_threshold_hours (float) – Normalize timespans beyond this
+
config (ParserConfig) – a single object carrying all parsing and
+enrichment options plus the caches; when provided, the
+individual option keyword arguments listed above are ignored in
+favor of the config’s values.
Returns:
@@ -799,7 +935,7 @@ layer dict objects suitable for use in a CSV
@@ -808,10 +944,40 @@ layer dict objects suitable for use in a CSV
Parameters:
mailbox_connection – The mailbox connection object
-
callback – The callback function to receive the parsing results
+
callback – The callback function to receive the parsing results.
+Passed straight through to get_dmarc_reports_from_mailbox()
+as its save_callback, so it now runs once per fetched batch,
+with only that batch’s reports, before those messages are
+deleted or moved out of reports_folder – rather than once
+afterward with the whole check’s accumulated results. Returning
+False reports the batch as unsaved, leaving its messages in
+place to be retried on the next check instead of archived or
+deleted (see save_callback and max_unsaved_retries on
+get_dmarc_reports_from_mailbox()). Raising counts as an
+unsaved batch too – same retention and retry cap – before the
+exception reaches the mailbox backend’s watch loop; what happens
+then is backend-specific: the Microsoft Graph and Gmail backends
+let it propagate and end the watch, while mailsuite’s IMAP and
+Maildir watch loops log it and keep checking.
reports_folder (str) – The IMAP folder where reports can be found
archive_folder (str) – The folder to move processed mail to
-
delete (bool) – Delete messages after processing them
+
delete (bool) – Delete messages after processing them
+
delete_aggregate (bool | None) – Delete aggregate report messages
+after processing them, instead of moving them to the
+Aggregate archive subfolder; None (the default) inherits
+the value of delete
+
delete_failure (bool | None) – Delete failure report messages after
+processing them, instead of moving them to the Failure
+archive subfolder; None (the default) inherits the value of
+delete
+
delete_smtp_tls (bool | None) – Delete SMTP TLS report messages after
+processing them, instead of moving them to the SMTP-TLS
+archive subfolder; None (the default) inherits the value of
+delete
+
delete_invalid (bool | None) – Delete unparseable messages, instead
+of moving them to the Invalid archive subfolder where they
+can be inspected for debugging; None (the default) inherits
+the value of delete
test (bool) – Do not move or delete messages after processing them
check_timeout (int) – Number of seconds to wait for a IMAP IDLE response
or the number of seconds until the next mail check
@@ -834,11 +1000,256 @@ failure report samples with None
reload (or shutdown) has been requested (e.g. via SIGHUP/SIGTERM).
Polled by the mailbox backend between checks, including the IMAP
IDLE loop, so the watcher exits cleanly at a safe boundary.
+
n_procs (int) – Number of processes to use for parsing messages in
+parallel. Passed through to get_dmarc_reports_from_mailbox
+on each check. Not part of config; always applies.
+
max_unsaved_retries (int) – How many times a message may be retried
+after callback first reported its batch unsaved, before it is
+moved to the Unsaved archive subfolder (default 2). Passed
+through to get_dmarc_reports_from_mailbox, where it is
+documented in full. Not part of config; always applies.
+
config (ParserConfig) – a single object carrying all parsing and
+enrichment options plus the caches; when provided, it replaces
+the individual parsing and enrichment option keyword arguments
+listed above (DNS, GeoIP, offline mode, attachment payload
+stripping, timespan normalization), whose values are then
+ignored. The remaining keyword arguments control mailbox
+handling and orchestration rather than parsing (the folder
+names, the delete options, test, since,
+batch_size, max_unsaved_retries); they are not part of
+config and always apply.
This module defines ParserConfig, a single frozen dataclass that
+carries every parsing/enrichment option (offline mode, IP database path,
+reverse DNS map location, PSL overrides location, DNS behavior, etc.) plus
+the three caches that the parser and enrichment code share across calls:
+the IP address info cache, the seen-aggregate-report-ID cache, and the
+reverse DNS map.
+
parsedmarc/__init__.py re-exports IP_ADDRESS_CACHE,
+SEEN_AGGREGATE_REPORT_IDS, and REVERSE_DNS_MAP under the
+same names for backward compatibility. That compatibility is
+identity-based: parsedmarc.IP_ADDRESS_CACHEisparsedmarc.config.IP_ADDRESS_CACHE
+must hold, not merely equal contents, since callers may mutate the caches
+in place.
+
Only parsedmarc.constants and parsedmarc.utils are imported
+from the package here — importing parsedmarc itself would be
+circular, since parsedmarc/__init__.py imports from this module.
Carries all parsing/enrichment options, plus the caches they share.
+
When passed as config= to parsedmarc’s public parsing functions, the
+individual option keyword arguments (offline, ip_db_path,
+nameservers, etc.) are ignored in favor of the values carried on this
+object.
+
Every explicitly constructed ParserConfig() owns fresh, isolated
+cache objects (ip_address_cache, seen_aggregate_report_ids,
+reverse_dns_map) via default_factory — two independently
+constructed configs never share cache state. To derive a variant of an
+existing config that does keep sharing its caches (e.g. to override
+offline for one call while still benefiting from warm caches), use
+dataclasses.replace(cfg,...): since every field, including the three
+cache fields, is an init field, replace() copies the source’s cache
+objects onto the new instance rather than constructing fresh ones.
+
Pickling (as happens when a config is captured in a
+functools.partial payload submitted to a multiprocessing worker)
+intentionally drops cache contents: __getstate__ omits the three
+cache fields, and __setstate__ rebinds them to this module’s
+IP_ADDRESS_CACHE, SEEN_AGGREGATE_REPORT_IDS, and
+REVERSE_DNS_MAP — the unpickling process’s module-default
+caches — rather than either the sender’s cache contents (which would be
+expensive and stale to serialize per task) or brand new empty caches per
+unpickle (which would silently defeat per-worker caching, since a
+functools.partial payload is re-pickled for every task submitted to a
+worker). Binding the module defaults means a freshly spawned worker
+interpreter starts with empty caches and accumulates hits across the
+tasks it handles, matching pre-refactor behavior.
+
Note that keep_alive and n_procs are deliberately not fields
+on this class: those control process/worker orchestration, not parsing
+or enrichment behavior.
diff --git a/installation.html b/installation.html
index d2b7cd84..a9ac91ea 100644
--- a/installation.html
+++ b/installation.html
@@ -6,14 +6,14 @@
- Installation — parsedmarc 10.3.0 documentation
+ Installation — parsedmarc 10.4.0 documentation
-
+
diff --git a/kibana.html b/kibana.html
index 12d762b2..1ee20c1d 100644
--- a/kibana.html
+++ b/kibana.html
@@ -6,14 +6,14 @@
- Using the Kibana dashboards — parsedmarc 10.3.0 documentation
+ Using the Kibana dashboards — parsedmarc 10.4.0 documentation
-
+
@@ -120,7 +120,7 @@ passes if a message passes SPF or DKIM alignment, only DKIM alignment remains
valid when a message is forwarded without changing the from address, which is
often caused by a mailbox forwarding rule. This is because DKIM signatures are
part of the message headers, whereas SPF relies on SMTP session headers.
-
Underneath the pie charts. you can see graphs of DMARC passage and message
+
Underneath the pie charts, you can see graphs of DMARC compliance and message
disposition over time.
Under the graphs you will find the most useful data tables on the dashboard. On
the left, there is a list of organizations that are sending you DMARC reports.
diff --git a/mailing-lists.html b/mailing-lists.html
index ff7e836d..6764a3bb 100644
--- a/mailing-lists.html
+++ b/mailing-lists.html
@@ -6,14 +6,14 @@
-
diff --git a/search.html b/search.html
index efd021f7..c2d4ed86 100644
--- a/search.html
+++ b/search.html
@@ -5,7 +5,7 @@
- Search — parsedmarc 10.3.0 documentation
+ Search — parsedmarc 10.4.0 documentation
@@ -13,7 +13,7 @@
-
+
diff --git a/searchindex.js b/searchindex.js
index cf45ff11..dd8e5d47 100644
--- a/searchindex.js
+++ b/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{"API reference":[[0,null]],"Accessing an inbox using OWA/EWS":[[2,null]],"Backfilling the combined DKIM/SPF result fields":[[4,"backfilling-the-combined-dkim-spf-result-fields"]],"Bug reports":[[1,"bug-reports"]],"CLI help":[[12,"cli-help"]],"CSV aggregate report":[[10,"csv-aggregate-report"]],"CSV failure report":[[10,"csv-failure-report"]],"Configuration file":[[12,"configuration-file"]],"Configuring parsedmarc for DavMail":[[2,"configuring-parsedmarc-for-davmail"]],"Contents":[[5,null]],"Contributing to parsedmarc":[[1,null]],"DMARC Alignment Guide":[[3,"dmarc-alignment-guide"]],"DMARC aggregate reports":[[7,"dmarc-aggregate-reports"]],"DMARC failure reports":[[7,"dmarc-failure-reports"]],"DMARC guides":[[3,"dmarc-guides"]],"Do":[[3,"do"],[8,"do"]],"Do not":[[3,"do-not"],[8,"do-not"]],"Docker Compose example":[[12,"docker-compose-example"]],"Docker secrets (_FILE suffix)":[[12,"docker-secrets-file-suffix"]],"Elasticsearch and Kibana":[[4,null]],"Environment variable configuration":[[12,"environment-variable-configuration"]],"Examples":[[12,"examples"]],"Features":[[5,"features"]],"IP-to-country database":[[6,"ip-to-country-database"]],"Indices and tables":[[0,"indices-and-tables"]],"Installation":[[4,"installation"],[6,null],[9,"installation"]],"Installing parsedmarc":[[6,"installing-parsedmarc"]],"JSON SMTP TLS report":[[10,"json-smtp-tls-report"]],"JSON aggregate report":[[10,"json-aggregate-report"]],"JSON failure report":[[10,"json-failure-report"]],"LISTSERV":[[3,"listserv"],[8,"listserv"]],"Lookalike domains":[[3,"lookalike-domains"]],"Mailing list best practices":[[3,"mailing-list-best-practices"],[8,"mailing-list-best-practices"]],"Mailman 2":[[3,"mailman-2"],[3,"id1"],[8,"mailman-2"],[8,"id1"]],"Mailman 3":[[3,"mailman-3"],[3,"id2"],[8,"mailman-3"],[8,"id2"]],"Multi-tenant support":[[12,"multi-tenant-support"]],"OpenSearch and Grafana":[[9,null]],"Optional dependencies":[[6,"optional-dependencies"]],"Performance tuning":[[12,"performance-tuning"]],"Prerequisites":[[6,"prerequisites"]],"Python Compatibility":[[5,"python-compatibility"]],"Records retention":[[4,"records-retention"],[9,"records-retention"]],"Reloading configuration without restarting":[[12,"reloading-configuration-without-restarting"]],"Resources":[[3,"resources"]],"Running DavMail as a systemd service":[[2,"running-davmail-as-a-systemd-service"]],"Running parsedmarc as a systemd service":[[12,"running-parsedmarc-as-a-systemd-service"]],"Running without a config file (env-only mode)":[[12,"running-without-a-config-file-env-only-mode"]],"SMTP TLS reporting":[[7,"smtp-tls-reporting"]],"SPF and DMARC record validation":[[3,"spf-and-dmarc-record-validation"]],"Sample aggregate report output":[[10,"sample-aggregate-report-output"]],"Sample failure report output":[[10,"sample-failure-report-output"]],"Sample outputs":[[10,null]],"Section name mapping":[[12,"section-name-mapping"]],"Specifying the config file via environment variable":[[12,"specifying-the-config-file-via-environment-variable"]],"Splunk":[[11,null]],"Testing multiple report analyzers":[[6,"testing-multiple-report-analyzers"]],"Understanding DMARC":[[3,null]],"Upgrading Kibana index patterns":[[4,"upgrading-kibana-index-patterns"]],"Using MaxMind GeoLite2 (optional)":[[6,"using-maxmind-geolite2-optional"]],"Using Microsoft Exchange":[[6,"using-microsoft-exchange"]],"Using a web proxy":[[6,"using-a-web-proxy"]],"Using parsedmarc":[[12,null]],"Using the Kibana dashboards":[[7,null]],"What about mailing lists?":[[3,"what-about-mailing-lists"],[8,null]],"What if a sender won\u2019t support DKIM/DMARC?":[[3,"what-if-a-sender-wont-support-dkim-dmarc"]],"Workarounds":[[3,"workarounds"],[8,"workarounds"]],"parsedmarc":[[0,"module-parsedmarc"]],"parsedmarc documentation - Open source DMARC report analyzer and visualizer":[[5,null]],"parsedmarc.elastic":[[0,"module-parsedmarc.elastic"]],"parsedmarc.opensearch":[[0,"module-parsedmarc.opensearch"]],"parsedmarc.splunk":[[0,"module-parsedmarc.splunk"]],"parsedmarc.types":[[0,"module-parsedmarc.types"]],"parsedmarc.utils":[[0,"module-parsedmarc.utils"]]},"docnames":["api","contributing","davmail","dmarc","elasticsearch","index","installation","kibana","mailing-lists","opensearch","output","splunk","usage"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.todo":2,"sphinx.ext.viewcode":1},"filenames":["api.md","contributing.md","davmail.md","dmarc.md","elasticsearch.md","index.md","installation.md","kibana.md","mailing-lists.md","opensearch.md","output.md","splunk.md","usage.md"],"indexentries":{"aggregatealignment (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateAlignment",false]],"aggregateauthresultdkim (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateAuthResultDKIM",false]],"aggregateauthresults (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateAuthResults",false]],"aggregateauthresultspf (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateAuthResultSPF",false]],"aggregateidentifiers (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateIdentifiers",false]],"aggregateparsedreport (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateParsedReport",false]],"aggregatepolicyevaluated (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregatePolicyEvaluated",false]],"aggregatepolicyoverridereason (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregatePolicyOverrideReason",false]],"aggregatepolicypublished (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregatePolicyPublished",false]],"aggregaterecord (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateRecord",false]],"aggregatereport (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateReport",false]],"aggregatereportmetadata (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateReportMetadata",false]],"alreadysaved":[[0,"parsedmarc.elastic.AlreadySaved",false],[0,"parsedmarc.opensearch.AlreadySaved",false]],"append_json() (in module parsedmarc)":[[0,"parsedmarc.append_json",false]],"close() (parsedmarc.splunk.hecclient method)":[[0,"parsedmarc.splunk.HECClient.close",false]],"configure_ipinfo_api() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.configure_ipinfo_api",false]],"convert_outlook_msg() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.convert_outlook_msg",false]],"create_indexes() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.create_indexes",false]],"create_indexes() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.create_indexes",false]],"decode_base64() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.decode_base64",false]],"downloaderror":[[0,"parsedmarc.utils.DownloadError",false]],"elasticsearcherror":[[0,"parsedmarc.elastic.ElasticsearchError",false]],"email_results() (in module parsedmarc)":[[0,"parsedmarc.email_results",false]],"email_results_via_msgraph() (in module parsedmarc)":[[0,"parsedmarc.email_results_via_msgraph",false]],"emailaddress (class in parsedmarc.types)":[[0,"parsedmarc.types.EmailAddress",false]],"emailattachment (class in parsedmarc.types)":[[0,"parsedmarc.types.EmailAttachment",false]],"emailparsererror":[[0,"parsedmarc.utils.EmailParserError",false]],"extract_report() (in module parsedmarc)":[[0,"parsedmarc.extract_report",false]],"extract_report_from_file_path() (in module parsedmarc)":[[0,"parsedmarc.extract_report_from_file_path",false]],"failureparsedreport (class in parsedmarc.types)":[[0,"parsedmarc.types.FailureParsedReport",false]],"failurereport (class in parsedmarc.types)":[[0,"parsedmarc.types.FailureReport",false]],"forensicparsedreport (in module parsedmarc.types)":[[0,"parsedmarc.types.ForensicParsedReport",false]],"forensicreport (in module parsedmarc.types)":[[0,"parsedmarc.types.ForensicReport",false]],"get_base_domain() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_base_domain",false]],"get_dmarc_reports_from_mailbox() (in module parsedmarc)":[[0,"parsedmarc.get_dmarc_reports_from_mailbox",false]],"get_dmarc_reports_from_mbox() (in module parsedmarc)":[[0,"parsedmarc.get_dmarc_reports_from_mbox",false]],"get_filename_safe_string() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_filename_safe_string",false]],"get_ip_address_country() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_ip_address_country",false]],"get_ip_address_db_record() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_ip_address_db_record",false]],"get_ip_address_info() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_ip_address_info",false]],"get_report_zip() (in module parsedmarc)":[[0,"parsedmarc.get_report_zip",false]],"get_reverse_dns() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_reverse_dns",false]],"get_service_from_reverse_dns_base_domain() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_service_from_reverse_dns_base_domain",false]],"hecclient (class in parsedmarc.splunk)":[[0,"parsedmarc.splunk.HECClient",false]],"human_timestamp_to_datetime() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.human_timestamp_to_datetime",false]],"human_timestamp_to_unix_timestamp() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.human_timestamp_to_unix_timestamp",false]],"invalidaggregatereport":[[0,"parsedmarc.InvalidAggregateReport",false]],"invaliddmarcreport":[[0,"parsedmarc.InvalidDMARCReport",false]],"invalidfailurereport":[[0,"parsedmarc.InvalidFailureReport",false]],"invalidforensicreport (in module parsedmarc)":[[0,"parsedmarc.InvalidForensicReport",false]],"invalidipinfoapikey":[[0,"parsedmarc.utils.InvalidIPinfoAPIKey",false]],"invalidsmtptlsreport":[[0,"parsedmarc.InvalidSMTPTLSReport",false]],"ipaddressinfo (class in parsedmarc.utils)":[[0,"parsedmarc.utils.IPAddressInfo",false]],"ipsourceinfo (class in parsedmarc.types)":[[0,"parsedmarc.types.IPSourceInfo",false]],"is_mbox() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.is_mbox",false]],"is_outlook_msg() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.is_outlook_msg",false]],"load_ip_db() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.load_ip_db",false]],"load_psl_overrides() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.load_psl_overrides",false]],"load_reverse_dns_map() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.load_reverse_dns_map",false]],"migrate_indexes() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.migrate_indexes",false]],"migrate_indexes() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.migrate_indexes",false]],"module":[[0,"module-parsedmarc",false],[0,"module-parsedmarc.elastic",false],[0,"module-parsedmarc.opensearch",false],[0,"module-parsedmarc.splunk",false],[0,"module-parsedmarc.types",false],[0,"module-parsedmarc.utils",false]],"opensearcherror":[[0,"parsedmarc.opensearch.OpenSearchError",false]],"parse_aggregate_report_file() (in module parsedmarc)":[[0,"parsedmarc.parse_aggregate_report_file",false]],"parse_aggregate_report_xml() (in module parsedmarc)":[[0,"parsedmarc.parse_aggregate_report_xml",false]],"parse_email() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.parse_email",false]],"parse_failure_report() (in module parsedmarc)":[[0,"parsedmarc.parse_failure_report",false]],"parse_forensic_report() (in module parsedmarc)":[[0,"parsedmarc.parse_forensic_report",false]],"parse_report_email() (in module parsedmarc)":[[0,"parsedmarc.parse_report_email",false]],"parse_report_file() (in module parsedmarc)":[[0,"parsedmarc.parse_report_file",false]],"parse_smtp_tls_report_json() (in module parsedmarc)":[[0,"parsedmarc.parse_smtp_tls_report_json",false]],"parsed_aggregate_reports_to_csv() (in module parsedmarc)":[[0,"parsedmarc.parsed_aggregate_reports_to_csv",false]],"parsed_aggregate_reports_to_csv_rows() (in module parsedmarc)":[[0,"parsedmarc.parsed_aggregate_reports_to_csv_rows",false]],"parsed_failure_reports_to_csv() (in module parsedmarc)":[[0,"parsedmarc.parsed_failure_reports_to_csv",false]],"parsed_failure_reports_to_csv_rows() (in module parsedmarc)":[[0,"parsedmarc.parsed_failure_reports_to_csv_rows",false]],"parsed_forensic_reports_to_csv() (in module parsedmarc)":[[0,"parsedmarc.parsed_forensic_reports_to_csv",false]],"parsed_forensic_reports_to_csv_rows() (in module parsedmarc)":[[0,"parsedmarc.parsed_forensic_reports_to_csv_rows",false]],"parsed_smtp_tls_reports_to_csv() (in module parsedmarc)":[[0,"parsedmarc.parsed_smtp_tls_reports_to_csv",false]],"parsed_smtp_tls_reports_to_csv_rows() (in module parsedmarc)":[[0,"parsedmarc.parsed_smtp_tls_reports_to_csv_rows",false]],"parsedemail (class in parsedmarc.types)":[[0,"parsedmarc.types.ParsedEmail",false]],"parsedmarc":[[0,"module-parsedmarc",false]],"parsedmarc.elastic":[[0,"module-parsedmarc.elastic",false]],"parsedmarc.opensearch":[[0,"module-parsedmarc.opensearch",false]],"parsedmarc.splunk":[[0,"module-parsedmarc.splunk",false]],"parsedmarc.types":[[0,"module-parsedmarc.types",false]],"parsedmarc.utils":[[0,"module-parsedmarc.utils",false]],"parsererror":[[0,"parsedmarc.ParserError",false]],"parsingresults (class in parsedmarc.types)":[[0,"parsedmarc.types.ParsingResults",false]],"query_dns() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.query_dns",false]],"reversednsservice (class in parsedmarc.utils)":[[0,"parsedmarc.utils.ReverseDNSService",false]],"save_aggregate_report_to_elasticsearch() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.save_aggregate_report_to_elasticsearch",false]],"save_aggregate_report_to_opensearch() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.save_aggregate_report_to_opensearch",false]],"save_aggregate_reports_to_splunk() (parsedmarc.splunk.hecclient method)":[[0,"parsedmarc.splunk.HECClient.save_aggregate_reports_to_splunk",false]],"save_failure_report_to_elasticsearch() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.save_failure_report_to_elasticsearch",false]],"save_failure_report_to_opensearch() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.save_failure_report_to_opensearch",false]],"save_failure_reports_to_splunk() (parsedmarc.splunk.hecclient method)":[[0,"parsedmarc.splunk.HECClient.save_failure_reports_to_splunk",false]],"save_forensic_report_to_elasticsearch() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.save_forensic_report_to_elasticsearch",false]],"save_forensic_report_to_opensearch() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.save_forensic_report_to_opensearch",false]],"save_forensic_reports_to_splunk() (parsedmarc.splunk.hecclient method)":[[0,"parsedmarc.splunk.HECClient.save_forensic_reports_to_splunk",false]],"save_output() (in module parsedmarc)":[[0,"parsedmarc.save_output",false]],"save_smtp_tls_report_to_elasticsearch() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.save_smtp_tls_report_to_elasticsearch",false]],"save_smtp_tls_report_to_opensearch() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.save_smtp_tls_report_to_opensearch",false]],"save_smtp_tls_reports_to_splunk() (parsedmarc.splunk.hecclient method)":[[0,"parsedmarc.splunk.HECClient.save_smtp_tls_reports_to_splunk",false]],"set_hosts() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.set_hosts",false]],"set_hosts() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.set_hosts",false]],"smtptlsfailuredetails (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSFailureDetails",false]],"smtptlsfailuredetailsoptional (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSFailureDetailsOptional",false]],"smtptlsparsedreport (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSParsedReport",false]],"smtptlspolicy (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSPolicy",false]],"smtptlspolicysummary (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSPolicySummary",false]],"smtptlsreport (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSReport",false]],"splunkerror":[[0,"parsedmarc.splunk.SplunkError",false]],"timestamp_to_datetime() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.timestamp_to_datetime",false]],"timestamp_to_human() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.timestamp_to_human",false]],"watch_inbox() (in module parsedmarc)":[[0,"parsedmarc.watch_inbox",false]]},"objects":{"":[[0,0,0,"-","parsedmarc"]],"parsedmarc":[[0,1,1,"","InvalidAggregateReport"],[0,1,1,"","InvalidDMARCReport"],[0,1,1,"","InvalidFailureReport"],[0,2,1,"","InvalidForensicReport"],[0,1,1,"","InvalidSMTPTLSReport"],[0,1,1,"","ParserError"],[0,3,1,"","append_json"],[0,0,0,"-","elastic"],[0,3,1,"","email_results"],[0,3,1,"","email_results_via_msgraph"],[0,3,1,"","extract_report"],[0,3,1,"","extract_report_from_file_path"],[0,3,1,"","get_dmarc_reports_from_mailbox"],[0,3,1,"","get_dmarc_reports_from_mbox"],[0,3,1,"","get_report_zip"],[0,0,0,"-","opensearch"],[0,3,1,"","parse_aggregate_report_file"],[0,3,1,"","parse_aggregate_report_xml"],[0,3,1,"","parse_failure_report"],[0,3,1,"","parse_forensic_report"],[0,3,1,"","parse_report_email"],[0,3,1,"","parse_report_file"],[0,3,1,"","parse_smtp_tls_report_json"],[0,3,1,"","parsed_aggregate_reports_to_csv"],[0,3,1,"","parsed_aggregate_reports_to_csv_rows"],[0,3,1,"","parsed_failure_reports_to_csv"],[0,3,1,"","parsed_failure_reports_to_csv_rows"],[0,3,1,"","parsed_forensic_reports_to_csv"],[0,3,1,"","parsed_forensic_reports_to_csv_rows"],[0,3,1,"","parsed_smtp_tls_reports_to_csv"],[0,3,1,"","parsed_smtp_tls_reports_to_csv_rows"],[0,3,1,"","save_output"],[0,0,0,"-","splunk"],[0,0,0,"-","types"],[0,0,0,"-","utils"],[0,3,1,"","watch_inbox"]],"parsedmarc.elastic":[[0,1,1,"","AlreadySaved"],[0,1,1,"","ElasticsearchError"],[0,3,1,"","create_indexes"],[0,3,1,"","migrate_indexes"],[0,3,1,"","save_aggregate_report_to_elasticsearch"],[0,3,1,"","save_failure_report_to_elasticsearch"],[0,3,1,"","save_forensic_report_to_elasticsearch"],[0,3,1,"","save_smtp_tls_report_to_elasticsearch"],[0,3,1,"","set_hosts"]],"parsedmarc.opensearch":[[0,1,1,"","AlreadySaved"],[0,1,1,"","OpenSearchError"],[0,3,1,"","create_indexes"],[0,3,1,"","migrate_indexes"],[0,3,1,"","save_aggregate_report_to_opensearch"],[0,3,1,"","save_failure_report_to_opensearch"],[0,3,1,"","save_forensic_report_to_opensearch"],[0,3,1,"","save_smtp_tls_report_to_opensearch"],[0,3,1,"","set_hosts"]],"parsedmarc.splunk":[[0,4,1,"","HECClient"],[0,1,1,"","SplunkError"]],"parsedmarc.splunk.HECClient":[[0,5,1,"","close"],[0,5,1,"","save_aggregate_reports_to_splunk"],[0,5,1,"","save_failure_reports_to_splunk"],[0,5,1,"","save_forensic_reports_to_splunk"],[0,5,1,"","save_smtp_tls_reports_to_splunk"]],"parsedmarc.types":[[0,4,1,"","AggregateAlignment"],[0,4,1,"","AggregateAuthResultDKIM"],[0,4,1,"","AggregateAuthResultSPF"],[0,4,1,"","AggregateAuthResults"],[0,4,1,"","AggregateIdentifiers"],[0,4,1,"","AggregateParsedReport"],[0,4,1,"","AggregatePolicyEvaluated"],[0,4,1,"","AggregatePolicyOverrideReason"],[0,4,1,"","AggregatePolicyPublished"],[0,4,1,"","AggregateRecord"],[0,4,1,"","AggregateReport"],[0,4,1,"","AggregateReportMetadata"],[0,4,1,"","EmailAddress"],[0,4,1,"","EmailAttachment"],[0,4,1,"","FailureParsedReport"],[0,4,1,"","FailureReport"],[0,2,1,"","ForensicParsedReport"],[0,2,1,"","ForensicReport"],[0,4,1,"","IPSourceInfo"],[0,4,1,"","ParsedEmail"],[0,4,1,"","ParsingResults"],[0,4,1,"","SMTPTLSFailureDetails"],[0,4,1,"","SMTPTLSFailureDetailsOptional"],[0,4,1,"","SMTPTLSParsedReport"],[0,4,1,"","SMTPTLSPolicy"],[0,4,1,"","SMTPTLSPolicySummary"],[0,4,1,"","SMTPTLSReport"]],"parsedmarc.utils":[[0,1,1,"","DownloadError"],[0,1,1,"","EmailParserError"],[0,4,1,"","IPAddressInfo"],[0,1,1,"","InvalidIPinfoAPIKey"],[0,4,1,"","ReverseDNSService"],[0,3,1,"","configure_ipinfo_api"],[0,3,1,"","convert_outlook_msg"],[0,3,1,"","decode_base64"],[0,3,1,"","get_base_domain"],[0,3,1,"","get_filename_safe_string"],[0,3,1,"","get_ip_address_country"],[0,3,1,"","get_ip_address_db_record"],[0,3,1,"","get_ip_address_info"],[0,3,1,"","get_reverse_dns"],[0,3,1,"","get_service_from_reverse_dns_base_domain"],[0,3,1,"","human_timestamp_to_datetime"],[0,3,1,"","human_timestamp_to_unix_timestamp"],[0,3,1,"","is_mbox"],[0,3,1,"","is_outlook_msg"],[0,3,1,"","load_ip_db"],[0,3,1,"","load_psl_overrides"],[0,3,1,"","load_reverse_dns_map"],[0,3,1,"","parse_email"],[0,3,1,"","query_dns"],[0,3,1,"","timestamp_to_datetime"],[0,3,1,"","timestamp_to_human"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","attribute","Python attribute"],"3":["py","function","Python function"],"4":["py","class","Python class"],"5":["py","method","Python method"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:attribute","3":"py:function","4":"py:class","5":"py:method"},"terms":{"00z":10,"00z_exampl":10,"09t00":10,"09t23":10,"1d":12,"1g":4,"1w":12,"2017a":[3,8],"21vianet":12,"2d":12,"2k":12,"30s":12,"3d":10,"3h":12,"59z":10,"5m":[2,12],"7d":12,"A":[0,3,7,12],"AT":10,"After":[2,4,12],"All":[3,8,12],"An":[0,12],"And":0,"As":[4,7],"By":[7,12],"Do":[0,12],"Each":[4,7,12],"For":[0,4,12],"From":[3,8,12],"Further":7,"Have":12,"Here":10,"IF":12,"If":[0,3,4,6,7,8,12],"In":[0,2,3,7,8,12],"It":[2,4,7,10,12],"More":6,"Most":[7,12],"NOT":12,"No":[3,6,8],"Not":12,"On":[3,4,6,7,8,12],"Or":[4,6,12],"Some":[2,3,7,8],"That":7,"The":[0,3,4,6,7,11,12],"Then":[2,3,4,6,8,12],"There":7,"These":[7,12],"This":[5,6,7,10,12],"To":[2,4,6,7,9,10,12],"Under":[4,7],"What":5,"When":[0,3,5,8,12],"Where":[3,8],"While":[7,12],"With":[7,12],"YOUR":4,"You":[2,7,12],"_":12,"_attempt":0,"_cluster":12,"_input":0,"_ipdatabaserecord":0,"_serverless_rejected_set":0,"_sourc":4,"_task":4,"_update_by_queri":[0,4],"aadst":12,"abl":6,"abort":[4,12],"abov":[2,4,6,7,12],"absent":4,"accept":[0,3,4,7,8,12],"access":[0,4,5,12],"access_key_id":12,"access_token":0,"accessright":12,"accident":[3,8],"account":[6,7,12],"acm":10,"acquir":12,"acquisit":12,"across":7,"action":[3,8,12],"activ":[4,5,12],"active_primary_shard":12,"active_shard":12,"actual":[3,10],"ad":12,"add":[0,2,3,4,6,7,8,12],"addit":[3,8,12],"address":[0,2,3,4,6,7,8,10,12],"addresse":7,"adjust":4,"adkim":10,"admin":[3,8,12],"administr":[3,8,12],"advanc":7,"agari":5,"agent":4,"agg":7,"aggreg":[0,4,5,11,12],"aggregate_csv_filenam":[0,12],"aggregate_index":0,"aggregate_json_filenam":[0,12],"aggregate_report":0,"aggregate_top":12,"aggregate_url":12,"aggregatealign":0,"aggregateauthresult":0,"aggregateauthresultdkim":0,"aggregateauthresultspf":0,"aggregateidentifi":0,"aggregateparsedreport":0,"aggregatepolicyevalu":0,"aggregatepolicyoverridereason":0,"aggregatepolicypublish":0,"aggregaterecord":0,"aggregatereport":0,"aggregatereportmetadata":0,"aggress":12,"alia":[0,12],"alias":12,"align":[4,5,7,10],"aliv":0,"allow":[2,3,8,12],"allow_unencrypted_storag":12,"allowremot":2,"allowstringindic":7,"alon":12,"alreadi":[0,4,12],"alreadysav":0,"also":[0,2,3,4,6,7,8,12],"alter":[3,8],"altern":[5,12],"although":11,"alway":[0,2,4,12],"always_use_local_fil":[0,12],"amazon":5,"amd64":12,"amount":12,"analyt":[5,12],"analyz":12,"ani":[0,3,4,6,7,8,12],"anonym":10,"anoth":[6,12],"answer":[0,12],"apach":5,"api":[2,4,5,12],"api_key":[0,12],"app":12,"appear":[4,7,12],"append":[0,12],"append_json":0,"appendix":10,"appid":12,"appli":12,"applic":[4,12],"applicationaccesspolici":12,"approach":12,"approxim":2,"apt":[2,4,6],"archiv":[0,12],"archive_fold":[0,12],"argument":12,"arm64":12,"around":12,"array":[0,4],"arraylist":4,"arriv":12,"arrival_d":10,"arrival_date_utc":[0,10],"artifact":4,"as_domain":[0,10],"as_nam":[0,10],"ask":3,"asmx":2,"asn":[0,6,10,12],"aspf":10,"assert":12,"assign":4,"associ":0,"assum":12,"assume_utc":0,"astimezon":0,"att":10,"attach":[0,3,8,10,12],"attachment_filenam":0,"attempt":[0,12],"attribut":[6,7],"audit":4,"auth":[0,2,4,7,10,12],"auth_failur":10,"auth_method":12,"auth_mod":12,"auth_result":10,"auth_typ":[0,12],"authent":[0,2,3,4,7,12],"authentication_mechan":10,"authentication_result":10,"authentication_typ":12,"auto":2,"automat":[4,6,12],"avail":[0,6,12],"avoid":[7,12],"aw":[0,12],"aws_region":[0,12],"aws_servic":[0,12],"awssigv4":[0,12],"az":12,"azur":[5,12],"b":10,"b2c":7,"back":[0,12],"backend":[0,12],"backfil":[0,5,12],"background":[0,4],"backlog":12,"backward":12,"bare":12,"base":[0,2,3,4,6,7,8,10,12],"base64":0,"base_domain":[0,10],"basic":[0,2,12],"batch":12,"batch_siz":[0,12],"bcc":[0,10],"bd6e1bb5":10,"becaus":[2,3,4,7,8,12],"becom":12,"befor":[0,12],"begin_d":10,"behavior":0,"behind":6,"benefit":5,"best":[7,12],"beyond":0,"bin":[2,4,6,12],"binari":[0,12],"binaryio":0,"bind":2,"bindaddress":2,"blank":[3,8],"block":[0,2,4,12],"bodi":[0,3,8,10,12],"bool":[0,4,12],"boundari":[0,12],"box":12,"brand":[5,7],"break":[3,4,8],"broken":0,"browser":4,"bucket":12,"budget":0,"bug":[5,12],"build":6,"built":0,"bundl":[0,6,7,12],"busi":7,"button":[3,8],"byte":0,"c":[10,12],"ca":[4,12],"cach":[0,12],"cafile_path":12,"call":[0,5,12],"callabl":0,"callback":0,"came":[3,8],"can":[0,2,3,4,5,6,7,8,12],"cap":[0,12],"carri":[0,4,6,7,12],"case":[2,3,8,12],"catch":[0,12],"caught":0,"caus":[3,4,7,8,12],"cc":[0,10],"center":7,"cento":[4,6],"cert":[4,12],"cert_path":12,"certain":[0,12],"certfile_path":12,"certif":[0,4,7,12],"certificate_password":12,"certificate_path":12,"cest":10,"chain":0,"chang":[4,7,11,12],"charact":[2,12],"charset":10,"chart":7,"cheap":[0,4],"check":[0,2,3,4,7,12],"check_timeout":[0,12],"checkbox":4,"checkdmarc":3,"china":12,"chinacloudapi":12,"chines":7,"chmod":[2,4,12],"choos":[3,8],"chown":[2,12],"ci":7,"cisco":12,"class":[0,4],"clean":[0,12],"clear":0,"cli":[0,5],"click":[4,7],"client":[2,3,4,8,12],"client_assert":12,"client_id":12,"client_secret":12,"clientassert":12,"clientsecret":12,"clientsotimeout":2,"clock":0,"close":[0,12],"cloud":[0,12],"cloudflar":[0,12],"cluster":[0,4,12],"cn":12,"co":4,"code":[0,4,12],"collect":[7,12],"collector":[11,12],"column":7,"com":[1,2,3,8,9,10,12],"combin":[0,5,7,12],"come":[0,7],"comfort":12,"comma":[6,12],"command":[0,2,3,4,6,8,12],"comment":12,"commerci":[4,5],"common":[3,4,6,8],"communiti":[3,8],"compat":[0,7,12],"complet":[3,4,12],"compli":[3,4,6,8,9],"complianc":7,"compliant":[3,8],"compon":[6,7],"compress":5,"comput":7,"condit":7,"conf":6,"config":[0,2,6],"config_fil":12,"config_reload":0,"configur":[0,3,4,5,6,7,8,9],"configure_ipinfo_api":0,"confirm":12,"conflict":4,"conform":4,"connect":[0,2,4,12],"connection_str":12,"consent":12,"consid":[5,7],"consist":[0,5,10],"consol":[4,12],"constant":0,"consult":6,"consum":[7,12],"contact":[7,12],"contain":[0,7,11,12],"content":[0,3,4,8,10,11,12],"continu":4,"contrib":6,"contribut":5,"control":[0,4,12],"convent":12,"convert":[0,3,8],"convert_outlook_msg":0,"copi":[0,6,11,12],"core":[3,8],"correct":[0,6,7,12],"correspond":[4,12],"corrupt":0,"count":[0,2,4,7,10],"countri":[0,7,10,12],"country_cod":0,"cpan":6,"cr":12,"crash":[2,4,12],"creat":[0,2,3,4,6,8,12],"create_fold":0,"create_index":0,"creation":12,"creativ":6,"credenti":[4,6,12],"credentials_fil":12,"cron":6,"cross":0,"crt":4,"csr":4,"csv":[0,5,12],"csvs":12,"ctrl":12,"ctx":4,"cumul":6,"curl":4,"current":[0,2,4,12],"custom":[4,7,12],"d":[0,4,12],"daemon":[2,4,12],"daili":[0,12],"dashboard":[4,5,9,11],"dat":0,"data":[0,4,5,6,7,9,11,12],"databas":[0,12],"date":[0,3,8,10],"date_utc":10,"datetim":0,"davmail":5,"day":[0,4,9,12],"db_path":0,"dbip":[0,6,12],"dbname":12,"dce":12,"dcr":12,"dcr_aggregate_stream":12,"dcr_failure_stream":12,"dcr_immutable_id":12,"dcr_smtp_tls_stream":12,"dd":[0,12],"de":10,"dearmor":4,"deb":4,"debian":[4,5,6],"debug":12,"decemb":6,"decod":0,"decode_base64":0,"dedic":6,"deeper":4,"def":4,"default":[0,2,4,5,6,7,12],"defect":4,"defens":[4,5],"delay":[0,2,10,12],"deleg":12,"delegated_us":12,"delet":[0,2,4,12],"deliber":12,"delivery_result":10,"demystifi":3,"deni":12,"depend":[0,4,5,12],"deploy":[3,8,12],"deprec":[7,12],"depth":4,"describ":12,"descript":[2,6,12],"design":12,"destin":[0,12],"det":4,"detail":[0,4,6,7,12],"dev":[6,12],"devel":6,"develop":5,"devicecod":12,"dict":0,"dictionari":0,"differ":[7,12],"difficult":12,"dig":0,"digest":[3,8],"dir":6,"direct":[7,12],"directori":[0,6,12],"dis":10,"disabl":[0,2,6,12],"disclaim":[3,8],"disk":[0,12],"display":[3,7,11],"display_nam":10,"disposit":[7,10],"distinguish":12,"distribut":6,"distro":6,"dk":4,"dkim":[0,5,7,8,10],"dkim_align":10,"dkim_domain":10,"dkim_result":[4,10],"dkim_results_combin":[0,4],"dkim_selector":10,"dkm":3,"dmarc":[0,4,6,8,9,10,11,12],"dmarc_aggreg":[4,7],"dmarc_align":10,"dmarc_failur":4,"dmarc_moderation_act":[3,8],"dmarc_none_moderation_act":[3,8],"dmarc_quarantine_moderation_act":[3,8],"dmarcian":5,"dmarcresport":12,"dnf":6,"dns":[0,3,6,7,12],"dns_retri":[0,12],"dns_test_address":12,"dns_timeout":[0,12],"dnspython":0,"doc":[0,9,12],"doctyp":10,"document":[0,2,4,7,12],"dod":12,"doe":[3,8,12],"doesn":0,"dom":4,"domain":[0,4,7,8,10,12],"domainawar":[1,3,12],"don":3,"doubl":12,"download":[0,2,4,6,12],"downloaderror":0,"dr":4,"draft":[5,10,12],"dropdown":7,"dsn":12,"dtd":10,"dummi":12,"dure":[2,12],"e":[0,2,3,4,6,8,12],"e7":10,"earlier":[0,7],"easi":[4,9],"easier":[11,12],"echo":4,"edit":[2,6,7,12],"editor":11,"effect":12,"effici":4,"either":[4,5,12],"elast":[4,5,7,12],"elasticsearch":[0,5,12],"elasticsearcherror":0,"elig":12,"elk":12,"els":4,"email":[0,3,5,6,7,8,10,11,12],"email_result":0,"email_results_via_msgraph":0,"emailaddress":0,"emailattach":0,"emailparsererror":0,"empti":[0,3,4,8,12],"en":[3,4,8,10],"enabl":[2,4,7,12],"enableew":2,"enablekeepal":2,"enableproxi":2,"encod":[0,10,12],"encount":0,"encrypt":[4,12],"encryptedsavedobject":4,"encryptionkey":4,"end":[0,3,4,5,12],"end_dat":10,"endpoint":[5,12],"endpoint_url":12,"enforc":[3,8],"enough":12,"enrich":6,"enrol":4,"ensur":[3,4,6,8],"enterpris":12,"entir":[0,3,7,8,12],"entra":12,"entri":0,"envelop":3,"envelope_from":10,"envelope_to":10,"environ":[5,6],"eof":0,"eol":5,"epel":6,"equival":4,"era":0,"error":[0,4,7,10,12],"erroritemnotfound":12,"es":[0,12],"escap":12,"especi":[7,12],"etc":[2,3,4,6,8,12],"even":[2,3,8,12],"event":[2,11,12],"everi":[0,2,4,6,7,12],"ew":5,"ex":12,"exact":[3,8],"exampl":[3,4,6,8,10],"exceed":7,"except":[0,12],"exchang":[2,10,12],"exclud":2,"execreload":12,"execstart":[2,12],"exhaust":12,"exist":[0,3,4,8,12],"exit":[0,12],"expect":12,"expiri":7,"expiringdict":0,"explain":[3,8],"explicit":[0,3,6,8,12],"export":[4,7,12],"extra":12,"extract":[0,2],"extract_report":0,"extract_report_from_file_path":0,"eye":[2,12],"f":[0,4],"factor":2,"fail":[0,3,7,8,10,12],"fail_on_output_error":12,"failed_sess":7,"failed_session_count":10,"failov":0,"failur":[0,5,11,12],"failure_csv_filenam":[0,12],"failure_detail":[4,10],"failure_details_combin":[0,4],"failure_index":0,"failure_json_filenam":[0,12],"failure_report":0,"failure_top":12,"failure_url":12,"failureparsedreport":0,"failurereport":0,"fall":[0,12],"fallback":[0,12],"fals":[0,2,4,10,12],"fantast":[3,8],"fast":0,"faster":12,"fatal":[0,12],"fds":4,"featur":[4,12],"feedback":0,"feedback_report":0,"feedback_typ":10,"fetch":[0,7,12],"field":[0,5,7],"file":[0,2,5,6,7,11],"file_path":[0,12],"filenam":[0,12],"filename_safe_subject":10,"filepath":12,"fill":[4,6],"filter":[0,3,7,8,11,12],"final":5,"financ":12,"find":[3,7,8,12],"fine":[3,8],"finish":12,"first":[0,3,6,7,8,12],"first_strip_reply_to":[3,8],"fit":[3,8,12],"fix":[4,12],"flag":[0,2,6,12],"flat":0,"flexibl":11,"flight":12,"float":[0,12],"flush":12,"fo":[0,10],"fold":0,"folder":[0,2,12],"foldersizelimit":2,"follow":[2,4,5,12],"footer":[3,8],"forc":12,"foreground":12,"forens":[5,7,12],"forensicparsedreport":0,"forensicreport":0,"form":12,"format":[0,7,12],"former":[5,7,12],"forward":[3,7,8],"found":[0,4,6,12],"foundat":10,"fqdn":4,"fraud":5,"free":[6,12],"fresh":[0,4,12],"freshest":12,"friend":7,"from_is_list":[3,8],"ftp_proxi":6,"full":12,"fulli":[0,3,4,7,8,12],"function":0,"g":[0,2,3,4,6,8,12],"gateway":2,"gb":4,"gcc":[6,12],"gdpr":[4,9],"gelf":[5,12],"general":[3,6,8,12],"generat":[3,4,8,10],"geoip":[6,12],"geoipupd":6,"geolite2":5,"geoloc":[0,12],"get":[0,2,4,6,12],"get_base_domain":0,"get_dmarc_reports_from_mailbox":0,"get_dmarc_reports_from_mbox":0,"get_filename_safe_str":0,"get_ip_address_countri":0,"get_ip_address_db_record":0,"get_ip_address_info":0,"get_report_zip":0,"get_reverse_dn":0,"get_service_from_reverse_dns_base_domain":0,"ghcr":12,"github":[1,6,10,12],"give":[0,4],"given":[0,12],"glass":7,"glob":12,"global":12,"gmail":[5,7,12],"gmail_api":12,"go":[0,3,8],"goe":[3,8],"googl":[7,12],"googleapi":12,"got":12,"gov":12,"govern":12,"gpg":4,"grafana":5,"grant":12,"graph":[0,2,5,7,12],"graph_url":12,"graylog":5,"group":[2,7,12],"guard":0,"guid":[4,5],"guidanc":12,"gzip":[0,5],"h":[0,4,12],"hamburg":4,"hand":[3,8],"handl":[5,12],"handler":7,"happen":[0,4],"hard":12,"has_defect":10,"hasn":12,"head":10,"header":[0,3,7,8,10,12],"header_from":10,"headless":2,"health":12,"healthcar":12,"heap":4,"heavi":[4,12],"hec":[0,11,12],"hecclient":0,"hectokengoesher":12,"help":5,"hh":0,"hierarchi":12,"high":[7,12],"higher":[3,8],"histor":[4,12],"histori":12,"hit":[0,12],"home":6,"hop":10,"host":[0,2,3,4,5,8,12],"hostnam":[0,12],"hour":[0,12],"hover":7,"href":10,"html":[3,4,8,10],"http":[0,2,4,5,6,10,11,12],"http_proxi":6,"https":[0,1,2,3,4,6,8,9,12],"https_proxi":6,"httpx":12,"human":[0,7],"human_timestamp":0,"human_timestamp_to_datetim":0,"human_timestamp_to_unix_timestamp":0,"hup":12,"icon":7,"id":[3,4,8,10,12],"ideal":[3,8],"idempot":4,"ident":[3,4,8,12],"identifi":10,"idl":[0,2,12],"ignor":[0,12],"imag":12,"imap":[0,2,5,12],"imap_password":12,"imapalwaysapproxmsgs":2,"imapautoexpung":2,"imapcli":5,"imapidledelay":2,"imapport":2,"immedi":[2,12],"immut":12,"impli":12,"import":[4,7,12],"improv":12,"inbox":[0,3,5,8,12],"inc":10,"includ":[0,3,4,6,7,8,12],"include_list_post_head":[3,8],"include_rfc2369_head":[3,8],"include_sender_head":[3,8],"include_spam_trash":12,"incom":[7,12],"incorrect":12,"increas":[4,12],"increment":12,"indefinit":12,"indent":12,"index":[0,5,7,9,11,12],"index_prefix":[0,4,12],"index_prefix_domain_map":12,"index_suffix":[0,4,12],"indic":[3,5],"individu":[7,12],"industri":12,"inform":[0,4,7,12],"infrequ":12,"ingest":12,"ini":[2,12],"initi":0,"inner":12,"input":0,"input_":0,"insid":4,"inspect":12,"instal":[0,2,5,12],"installed_app":12,"instanc":12,"instanceof":4,"instead":[0,3,4,6,8,12],"instruct":4,"int":[0,12],"intend":[3,8],"interact":[2,4,12],"interakt":10,"interfer":[3,8],"interpret":[0,6],"interrupt":12,"interval":12,"interval_begin":10,"interval_end":10,"invalid":[0,12],"invalidaggregatereport":0,"invaliddmarcreport":0,"invalidfailurereport":0,"invalidforensicreport":0,"invalidipinfoapikey":0,"invalidsmtptlsreport":0,"invis":4,"involv":7,"io":[0,12],"ip":[0,3,4,7,12],"ip_address":[0,10],"ip_db_path":[0,6,12],"ip_db_url":12,"ipaddressinfo":0,"ipinfo":[0,6,12],"ipinfo_api_token":12,"ipinfo_url":12,"ipsourceinfo":0,"ipv4":0,"ipv6":0,"is_mbox":0,"is_outlook_msg":0,"iso":[0,12],"issu":[0,1],"item":[0,12],"java":2,"job":[3,6,8],"joe":[3,8],"journalctl":[2,12],"jre":2,"json":[0,4,5,12],"june":5,"junk":12,"just":[4,7],"jvm":4,"jwt":12,"kafka":[5,12],"kb4099855":6,"kb4134118":6,"kb4295699":6,"keep":[0,4,7,12],"keep_al":0,"keepal":2,"key":[0,3,4,6,12],"keyfile_path":12,"keyout":4,"keyr":4,"keystor":4,"keyword":0,"kibana":[5,11],"kill":12,"killsign":12,"kind":12,"know":3,"known":[0,3,7,8,12],"kubernet":12,"kwarg":0,"l4":12,"l5":12,"label":12,"lack":4,"lang":4,"languag":[3,8],"larg":[2,12],"larger":12,"last":6,"later":[0,4,6,12],"latest":[2,4,9,12],"layer":0,"layout":11,"leak":7,"least":[4,6,12],"leav":3,"left":[0,7],"legaci":[0,5],"legal":[3,8],"legitim":[7,12],"less":12,"level":[0,3,4,12],"lf":12,"libemail":6,"libpq":12,"librari":12,"libxml2":6,"libxslt":6,"licens":6,"life":5,"lifetim":0,"lifetimetimeout":0,"like":[0,3,6,7,8,12],"limit":[0,2,12],"line":[3,8,12],"link":[3,4,7,8],"linux":[3,6,8],"list":[0,2,4,5,7,12],"listen":[2,12],"lite":[0,6,12],"live":[7,12],"ll":[3,8],"load":[0,4,12],"load_ip_db":0,"load_psl_overrid":0,"load_reverse_dns_map":0,"local":[0,2,4,6,10,12],"local_file_path":0,"local_psl_overrides_path":12,"local_reverse_dns_map_path":12,"localhost":[4,12],"locat":[7,12],"log":[0,2,4,5,12],"log_analyt":12,"log_fil":12,"logger":12,"login":[4,12],"logstash":4,"long":[0,3,12],"longer":[3,6,8],"look":[0,3,7],"lookup":[0,12],"loop":[0,12],"loopback":2,"loss":0,"lot":7,"low":12,"lower":12,"lua":10,"m":[0,6,12],"m365":12,"maco":6,"magnifi":7,"mail":[0,5,6,10,12],"mail_bcc":0,"mail_cc":0,"mail_from":0,"mail_to":0,"mailbox":[0,7,12],"mailbox_check_timeout":12,"mailbox_connect":0,"mailboxconnect":0,"maildir":12,"maildir_cr":12,"maildir_path":12,"mailer":10,"mailrelay":10,"mailsuit":12,"mailto":6,"main":4,"mainpid":12,"maintain":5,"make":[0,3,4,6,8,9,12],"malici":[7,12],"manag":[4,7,12],"mandatori":12,"manual":[0,4,12],"map":0,"mariadb":12,"market":7,"massiv":12,"match":[0,4,7,11,12],"max_ag":10,"max_shards_per_nod":12,"maximum":4,"maxmind":[0,5,12],"may":[5,7,12],"mbox":[0,12],"md":0,"mean":12,"meantim":0,"mechan":3,"member":[3,8],"memori":12,"mention":7,"menu":[4,7],"merg":0,"messag":[0,2,3,4,6,7,8,10,12],"message_id":10,"meta":10,"method":12,"metric":7,"mfrom":[4,10],"microsoft":[0,2,5,10,12],"microsoftgraph":12,"microsoftonlin":12,"mid":12,"might":[0,3,7,8],"migrat":[0,7,12],"migrate_index":0,"mime":10,"min":0,"minim":12,"minimum":[4,12],"minimum_should_match":4,"minut":[0,2,12],"mirror":0,"miss":[4,6,12],"mitig":[3,8],"mix":0,"mm":[0,12],"mmdb":[0,6,12],"mobil":[3,8],"mode":[0,2,4,6,10],"modern":[2,3,8],"modifi":[0,3,8,12],"modul":[0,5,6,12],"mon":10,"monitor":[3,12],"month":[0,12],"monthly_index":[0,12],"mous":7,"move":[0,4,12],"ms":[0,10,12],"msal":12,"msg":[0,6],"msg_byte":0,"msg_date":0,"msg_footer":[3,8],"msg_header":[3,8],"msgconvert":[0,6],"msgraph":12,"msgraphconnect":0,"mta":7,"much":12,"multi":[2,5],"multipl":[0,7,12],"mung":[3,8],"must":[2,3,4,8,12],"must_not":4,"mutual":[4,12],"mv":4,"mx":[7,10],"n":[0,10,12],"n_proc":12,"naiv":0,"name":[0,3,4,7,10,11],"nameserv":[0,12],"nano":[2,12],"nation":12,"navig":[3,8],"ncontent":10,"ndate":10,"ndjson":[4,7],"necessarili":7,"need":[0,2,3,4,6,7,8,12],"neither":12,"nelson":[3,8],"net":[2,12],"network":[0,2,4,12],"never":[0,4,12],"new":[0,2,4,5,6,7,12],"newer":6,"newest":[2,12],"newkey":4,"news":3,"next":[0,4,12],"nfrom":10,"nmessag":10,"nmime":10,"node":4,"nologin":6,"non":[0,3,4,8,12],"nonameserv":0,"none":[0,3,4,10,12],"noproxyfor":2,"norepli":[3,10],"normal":[0,10,12],"normalize_timespan_threshold_hour":0,"normalized_timespan":10,"nosecureimap":2,"notabl":7,"note":12,"noth":[4,12],"notic":12,"now":[4,6,7],"nsubject":10,"nto":10,"null":[4,6,10],"number":[0,7,12],"number_of_replica":[0,12],"number_of_shard":[0,12],"nwettbewerb":10,"nx":10,"o":[2,4,12],"oR":6,"oauth2":12,"oauth2_port":12,"object":[0,4,7],"observ":[7,12],"occur":[0,7],"occurr":11,"oct":10,"offic":2,"office365":2,"offici":12,"offlin":[0,6,12],"offset":[0,12],"often":[7,12],"old":[0,7],"older":[4,6,10,12],"oldest":[2,12],"ole":[0,6],"omit":12,"onc":[0,4,7,12],"ondmarc":5,"one":[0,3,4,5,6,7,8,12],"onli":[0,2,3,4,6,7,8],"onlin":[0,2,12],"onto":0,"oor":0,"op":0,"open":[0,3],"opendn":12,"opensearch":[4,5,7,12],"opensearch_dashboard":7,"opensearcherror":0,"openssl":4,"oper":12,"opt":[2,6,12],"option":[0,2,3,4,5,8,11,12],"order":12,"org":[0,6,9,10,12],"org_email":10,"org_extra_contact_info":10,"org_nam":10,"organiz":[2,5,7,12],"organization_nam":10,"origin":[3,8,12],"original_envelope_id":10,"original_mail_from":10,"original_rcpt_to":10,"original_timespan_second":10,"os":0,"oserror":0,"otherwis":[0,12],"outdat":7,"outgo":[3,8,12],"outlook":[0,2,6,12],"output":[0,5,12],"output_directori":0,"outsid":12,"overal":0,"overrid":[0,6,12],"overwrit":[0,4],"owa":[5,12],"owned":6,"ownership":6,"p":[3,4,10],"p12":4,"pack":4,"packag":[0,4,6],"packet":0,"pad":0,"page":[3,4,6,7,8],"paginate_messag":12,"painless":4,"pair":[4,7],"pan":10,"panel":7,"parallel":12,"paramet":[0,12],"parent":7,"pars":[0,3,5,6,10,12],"parse_aggregate_report_fil":0,"parse_aggregate_report_xml":0,"parse_email":0,"parse_failure_report":0,"parse_forensic_report":0,"parse_report_email":0,"parse_report_fil":0,"parse_smtp_tls_report_json":0,"parsed_aggregate_reports_to_csv":0,"parsed_aggregate_reports_to_csv_row":0,"parsed_failure_reports_to_csv":0,"parsed_failure_reports_to_csv_row":0,"parsed_forensic_reports_to_csv":0,"parsed_forensic_reports_to_csv_row":0,"parsed_sampl":10,"parsed_smtp_tls_reports_to_csv":0,"parsed_smtp_tls_reports_to_csv_row":0,"parsedemail":0,"parsedmarc":[4,9,10,11],"parsedmarc_":12,"parsedmarc_config_fil":12,"parsedmarc_debug":12,"parsedmarc_elasticsearch_":12,"parsedmarc_elasticsearch_host":12,"parsedmarc_elasticsearch_ssl":12,"parsedmarc_gelf_":12,"parsedmarc_general_":12,"parsedmarc_general_debug":12,"parsedmarc_general_ipinfo_api_token":12,"parsedmarc_general_ipinfo_url":12,"parsedmarc_general_offlin":12,"parsedmarc_general_save_aggreg":12,"parsedmarc_general_save_failur":12,"parsedmarc_gmail_api_":12,"parsedmarc_gmail_api_credentials_file_fil":12,"parsedmarc_imap_":12,"parsedmarc_imap_host":12,"parsedmarc_imap_password":12,"parsedmarc_imap_password_fil":12,"parsedmarc_imap_us":12,"parsedmarc_kafka_":12,"parsedmarc_log_analytics_":12,"parsedmarc_mailbox_":12,"parsedmarc_mailbox_watch":12,"parsedmarc_maildir_":12,"parsedmarc_msgraph_":12,"parsedmarc_opensearch_":12,"parsedmarc_s3_":12,"parsedmarc_smtp_":12,"parsedmarc_splunk_hec_":12,"parsedmarc_splunk_hec_index":12,"parsedmarc_splunk_hec_token":12,"parsedmarc_splunk_hec_url":12,"parsedmarc_syslog_":12,"parsedmarc_webhook_":12,"parser":0,"parsererror":0,"parsingresult":0,"part":[3,4,7,8,12],"particular":[7,12],"pass":[0,3,7,10,12],"passag":7,"passsword":12,"password":[0,4,6,12],"paste":[4,11],"patch":6,"path":[0,4,6,12],"pathlik":0,"pattern":[0,5,7,12],"payload":[0,12],"pct":10,"peak":12,"pem":12,"per":[0,4,7,12],"percentag":7,"perform":[2,5],"period":12,"perl":[0,6],"permiss":[4,12],"persist":12,"peter":10,"pick":[6,12],"pickup":6,"pid":12,"pie":7,"pin":12,"pip":[6,12],"pkcs12":12,"place":[0,4,7,12],"plain":[0,12],"plaintext":[3,8],"platform":[3,6,8,12],"pleas":[1,5,12],"plug":12,"plus":[7,12],"point":[4,6,12],"pol":4,"polici":[0,3,4,7,8,10,12],"policies_combin":[0,4],"policy_domain":[4,10],"policy_evalu":10,"policy_override_com":10,"policy_override_reason":10,"policy_publish":10,"policy_str":10,"policy_typ":[4,10],"policyscopegroupid":12,"poll":[0,2,12],"popul":0,"port":[0,2,12],"portal":12,"posit":[0,12],"posix":0,"possibl":12,"post":[3,4,8,12],"poster":[3,8],"postgr":12,"postgresql":[5,12],"postorius":[3,8],"powershel":12,"ppa":6,"practic":12,"pre":[6,12],"prebuilt":12,"predict":12,"prefer":[2,6,12],"prefix":[0,3,8,12],"premad":[5,11],"prepend":0,"prerequisit":5,"present":12,"pressur":12,"pretti":12,"prettifi":12,"previous":[0,2,4,6,12],"pri":[2,12],"primari":0,"print":12,"printabl":10,"prioriti":12,"privaci":[3,6,7,8,12],"privat":12,"probe":0,"problem":12,"proc":12,"proceed":4,"process":[0,2,5,6,12],"produc":[0,10],"program":12,"programdata":6,"progress":[4,12],"project":[0,2,3,5,11,12],"prompt":4,"proofpoint":5,"properti":2,"protect":[2,3,5,8,12],"protocol":12,"provid":[0,4,7,12],"provis":12,"prox":6,"proxi":2,"proxyhost":2,"proxypassword":2,"proxyport":2,"proxyus":2,"ps":4,"psl":[0,12],"psl_overrid":0,"psl_overrides_path":0,"psl_overrides_url":[0,12],"psycopg":12,"public":[0,3,10,12],"public_suffix_list":0,"publicbaseurl":4,"publicsuffix":0,"publish":[3,12],"published_polici":0,"pull":12,"purpos":4,"put":[4,12],"python":[0,4,6],"python3":6,"qo":4,"quarantin":[3,8],"queri":[0,4,12],"query_dn":0,"quick":0,"quickstart":12,"quit":12,"quot":[10,12],"quota":[0,12],"r":[2,10,12],"rais":[0,12],"ram":[4,12],"rate":[0,12],"rather":[0,3,4,7,8,12],"ratio":7,"raw":12,"re":[4,6,12],"reach":12,"reachabl":12,"read":[0,12],"readabl":[0,12],"readwrit":12,"real":[0,7],"realli":3,"reason":[0,2,4,5,12],"rebuilt":0,"receiv":[0,7,10,12],"receiveddatetim":12,"receiving_ip":[4,10],"receiving_mx_hostnam":[4,10],"recent":0,"recipi":7,"recogn":7,"recommend":12,"recommended_dns_nameserv":0,"record":[0,5,6,10,12],"record_typ":0,"recurs":12,"redact":12,"redi":12,"reduc":[6,12],"refer":[4,5,7],"referenc":12,"refresh":[6,12],"refresh_interv":12,"refus":4,"regard":12,"regardless":[0,10,12],"region":[0,12],"region_nam":12,"regist":[6,12],"registr":12,"regul":[4,6,9,12],"regular":[3,8],"reject":[0,3,8,12],"relat":[3,12],"relay":[3,8],"releas":[0,4,6],"reli":[6,7],"reliabl":12,"reload":[0,2,4],"remain":[0,7,12],"remot":2,"remov":[0,3,4,8,12],"render":7,"repeat":[0,3,8],"replac":[0,3,4,8,12],"repli":[2,3,8],"replic":12,"replica":[0,12],"reply_goes_to_list":[3,8],"reply_to":10,"replyto":[3,8],"repopul":0,"report":[0,4,11,12],"report_id":10,"report_metadata":10,"report_typ":0,"reported_domain":10,"reports_fold":[0,12],"repositori":[6,11],"req":4,"request":[0,2,4,12],"requir":[0,2,3,4,5,6,7,8,12],"require_encrypt":0,"res":4,"reserv":12,"resid":12,"resolv":[0,12],"resort":6,"resourc":[0,4,5,12],"respect":7,"respons":[0,12],"rest":[0,12],"restart":[2,3,4,6,8],"restartsec":[2,12],"restor":4,"restrict":12,"restrictaccess":12,"restructur":7,"result":[0,5,7,10,12],"result_typ":[4,10],"resum":12,"retain":[3,8,12],"retent":5,"retri":[0,4,12],"retriev":2,"retry_attempt":12,"retry_delay":12,"return":[0,4],"revers":[0,6,7,12],"reverse_dn":[0,10],"reverse_dns_base_domain":0,"reverse_dns_map":0,"reverse_dns_map_path":0,"reverse_dns_map_url":[0,12],"reversednsservic":0,"review":7,"rewrit":[0,3,8],"rfc":[0,3,5,8,10],"rfc2369":[3,8],"rfc822":2,"rhel":[4,5,6],"ri":4,"right":[4,7],"rm":4,"rmh":4,"ro":0,"rocki":6,"rollup":6,"root":[2,12],"rough":12,"row":7,"rpm":4,"rpt":[5,7],"rsa":4,"rt":4,"rua":[5,6],"ruf":[5,6,7,12],"rule":[7,12],"run":[0,4,5,6],"runtimeerror":12,"rw":[2,12],"s":[0,2,3,4,6,7,8,10,12],"s3":[5,12],"safe":[0,4],"safer":12,"sampl":[0,5,7,12],"sample_headers_on":10,"satisfi":7,"save":[0,4,6,7,12],"save_aggreg":12,"save_aggregate_report_to_elasticsearch":0,"save_aggregate_report_to_opensearch":0,"save_aggregate_reports_to_splunk":0,"save_failur":12,"save_failure_report_to_elasticsearch":0,"save_failure_report_to_opensearch":0,"save_failure_reports_to_splunk":0,"save_forens":12,"save_forensic_report_to_elasticsearch":0,"save_forensic_report_to_opensearch":0,"save_forensic_reports_to_splunk":0,"save_output":0,"save_smtp_tl":12,"save_smtp_tls_report_to_elasticsearch":0,"save_smtp_tls_report_to_opensearch":0,"save_smtp_tls_reports_to_splunk":0,"sbin":6,"sc":4,"scalar":4,"schedul":[6,12],"schema":[5,10,12],"scheme":0,"scope":[4,7,10,12],"script":[4,6],"scrub_nondigest":[3,8],"sdk":12,"search":[0,3,4,8,12],"second":[0,2,12],"secret_access_key":12,"section":4,"secur":[0,4,12],"see":[2,3,4,6,7,12],"seek":0,"segment":7,"sel":4,"select":0,"selector":[4,7,10],"self":[4,5],"send":[0,2,3,4,5,7,8,11,12],"sender":[5,7,8],"sending_mta_ip":[4,10],"sendmail":[0,12],"sensit":12,"sent":[0,3,8,12],"sentinel":5,"separ":[0,3,4,6,7,9,11,12],"sequenc":0,"sequenti":12,"serial":12,"server":[0,2,3,4,5,6,7,10,12],"server_ip":4,"serverless":[0,12],"servernameon":10,"servic":[0,3,4,5,6,7,8,10],"service_account":12,"service_account_us":12,"session":[0,7],"set":[0,2,3,4,6,7,8,9,12],"set_host":0,"setup":[4,6,9,12],"shape":[0,4],"shard":[0,12],"share":[4,6,7,12],"sharealik":6,"sharepoint":10,"shell":6,"ship":[6,12],"short":12,"shot":12,"shouldn":[3,8],"show":[2,7,12],"shown":[6,7,12],"shutdown":[0,12],"sibl":7,"side":[7,12],"sighup":[0,6,12],"sigkil":12,"sign":[0,3,4,6,12],"signal":12,"signatur":[3,7,8],"sigterm":[0,12],"sigv4":[0,12],"silent":[6,12],"similar":7,"simpl":5,"simpli":0,"simplifi":0,"sinc":[0,6,7,12],"singl":[0,7,12],"sink":12,"sister":3,"six":12,"size":[2,4],"skel":6,"skip":[0,4,12],"skip_certificate_verif":[0,12],"slight":11,"slow":0,"small":[4,12],"smaller":12,"smi":4,"smtp":[0,3,4,5,12],"smtp_tls":[0,4,12],"smtp_tls_csv_filenam":[0,12],"smtp_tls_index":0,"smtp_tls_json_filenam":[0,12],"smtp_tls_report":0,"smtp_tls_url":12,"smtptlsfailuredetail":0,"smtptlsfailuredetailsopt":0,"smtptlsparsedreport":0,"smtptlspolici":0,"smtptlspolicysummari":0,"smtptlsreport":0,"socket":2,"solut":6,"somehow":12,"someon":4,"sometim":12,"sort":12,"sourc":[0,3,4,6,7,10],"source_as_domain":10,"source_as_nam":10,"source_asn":10,"source_base_domain":10,"source_countri":10,"source_ip_address":10,"source_nam":10,"source_reverse_dn":10,"source_typ":10,"sourceforg":2,"sovereign":12,"sp":[3,4,10],"spam":12,"special":12,"specif":[3,6,7,12],"specifi":[2,3],"spf":[0,5,7,10],"spf_align":10,"spf_domain":10,"spf_result":[4,10],"spf_results_combin":[0,4],"spf_scope":10,"splunk":[5,12],"splunk_hec":12,"splunkerror":0,"splunkhec":12,"sponsor":5,"spoof":[3,8],"spurious":12,"sr":4,"ss":0,"ssl":[0,2,4,12],"ssl_cert_path":0,"stabl":4,"stack":[4,7,12],"standard":[0,5,6,10],"start":[0,2,4,7,9,11,12],"starttl":[7,12],"startup":[0,4,6],"static":12,"status":[2,12],"stay":7,"stdout":12,"step":[3,4,6,8,12],"still":[0,3,4,8,10,12],"stop":12,"storag":[0,4,12],"store":[2,4,7,9,12],"str":[0,12],"straight":12,"stream":12,"string":[0,4,7,12],"strip":[0,3,8,12],"strip_attachment_payload":[0,12],"strong":12,"structur":5,"sts":[7,10,12],"stsv1":10,"style":0,"subdomain":[0,3,12],"subfield":4,"subject":[0,3,8,10,12],"subject_prefix":[3,8],"submiss":0,"submit":[0,4],"subsidiari":7,"substitut":6,"success":12,"successful_sess":7,"successful_session_count":10,"sudo":[2,4,6,12],"suffici":12,"suffix":0,"suggest":7,"suit":12,"suitabl":0,"sum":7,"summari":[3,8,12],"supervis":12,"suppli":[0,7,12],"support":[2,4,5,7,10,11],"sure":4,"surfac":[7,12],"sw50zxjha3rpdmugv2v0dgjld2vyymvylcocymvyc2ljahq":10,"switch":7,"syslog":[2,5,12],"system":[2,3,4,6,8,12],"systemctl":[2,4,12],"systemd":5,"systemdr":6,"t":[0,4,5,7,8,10,12],"tab":[3,4,8],"tabl":[4,5,7,12],"tag":6,"take":[0,12],"talk":[0,4],"target":[0,2,12],"task":[0,4,6],"tbi":10,"tcp":12,"tee":4,"tell":[3,7,8],"templat":[3,8],"temporari":7,"tenant":5,"tenant_id":12,"term":6,"test":[0,10,12],"text":[0,4,10],"thank":10,"therebi":[3,8],"third":0,"though":7,"thousand":12,"three":7,"throughput":12,"tier":12,"time":[0,2,4,6,7,12],"timeout":[0,2,12],"timeoutstopsec":12,"timespan":0,"timespan_requires_norm":10,"timestamp":[0,12],"timestamp_to_datetim":0,"timestamp_to_human":0,"timezon":10,"tld":3,"tls":[0,4,5,12],"to_domain":10,"to_utc":0,"togeth":[7,12],"token":[0,4,12],"token_fil":12,"tool":12,"top":[3,7,12],"topic":12,"total":7,"touch":[3,8],"tracker":1,"trade":12,"tradit":[3,8],"traffic":7,"trail":12,"transfer":10,"transient":[0,12],"transpar":5,"transport":[4,12],"trash":12,"treat":0,"treatment":0,"tri":[0,12],"troubleshoot":12,"true":[0,2,4,10,12],"trust":12,"truststor":4,"truth":7,"tsvb":7,"tuesday":6,"tune":5,"two":[6,7],"txt":[0,12],"typ":4,"type":[4,5,7,10,12],"typic":12,"typo":12,"u":[2,6,12],"ubuntu":[4,6],"udp":[0,12],"ui":[3,8],"unchang":[0,12],"uncondit":[3,8],"underlying":[0,12],"underneath":7,"underscor":12,"understand":[5,7],"unencrypt":12,"unexpir":12,"unfortun":[3,8],"unit":[0,2,12],"unix":0,"unknown":0,"unless":[6,12],"unreach":12,"unread":12,"unrel":6,"unsubscrib":[3,8],"unsuit":12,"unus":0,"unzip":2,"updat":[4,6,12],"update_by_queri":0,"upersecur":12,"upgrad":[2,5,6,12],"upload":12,"upper":7,"uppercas":12,"uri":[6,12],"url":[0,2,4,12],"us":[10,12],"usabl":12,"usag":12,"use":[0,3,4,5,8,10],"use_ssl":0,"user":[0,2,3,4,6,7,8,10,12],"user_ag":10,"useradd":[2,6],"usernam":[0,12],"usernamepassword":12,"usesystemproxi":2,"usr":[4,6],"utc":[0,12],"utf":10,"util":5,"v":12,"v2":0,"valid":[0,7,10,12],"valimail":5,"valu":[0,3,4,7,8,12],"var":[3,8,12],"variabl":5,"variant":12,"various":6,"vendor":3,"venv":[6,12],"verbatim":12,"verbos":12,"veri":[0,4,7,12],"verif":[0,4,12],"verifi":0,"verification_mod":4,"version":[0,2,4,5,9,10,11,12],"vew":2,"via":[0,2],"view":[7,12],"vim":4,"virtualenv":6,"visual":[4,7,9],"volum":[7,12],"vulner":3,"w":[0,12],"w3c":10,"wait":[0,12],"wait_for_complet":[0,4],"wall":0,"want":[2,12],"wantedbi":[2,12],"warn":[0,4,12],"watch":[0,2,4,6,12],"watch_inbox":0,"watcher":[0,12],"way":[0,4,7],"web":[2,4],"webdav":2,"webhook":[5,12],"webmail":[3,7,8],"week":[0,6,12],"well":[2,7,12],"wettbewerb":10,"wget":4,"whalensolut":12,"wheel":12,"whenev":[0,2,12],"wherea":7,"wherev":12,"whether":[0,12],"whi":[3,7,12],"whole":[0,7],"whose":12,"wide":[6,10,12],"wiki":10,"will":[0,2,3,4,6,7,8,12],"win":12,"window":[6,12],"within":0,"without":[3,4,6,7,8],"won":5,"work":[2,3,4,5,6,7,8,12],"worker":12,"workstat":2,"worst":[3,12],"worth":12,"wrap":[3,8],"wrapper":12,"write":[0,4,12],"written":12,"www":[4,6,12],"x":[4,7,10],"x509":4,"xennn":10,"xml":[0,11],"xml_schema":10,"xms4g":4,"xmx4g":4,"xpack":4,"xxxx":4,"y":[4,6],"yahoo":7,"yaml":12,"year":12,"yes":[3,8],"yet":[0,3,4,12],"yml":4,"yyyi":[0,12],"z":12,"zero":12,"zip":[0,2,5,12],"\u00fcbersicht":10},"titles":["API reference","Contributing to parsedmarc","Accessing an inbox using OWA/EWS","Understanding DMARC","Elasticsearch and Kibana","parsedmarc documentation - Open source DMARC report analyzer and visualizer","Installation","Using the Kibana dashboards","What about mailing lists?","OpenSearch and Grafana","Sample outputs","Splunk","Using parsedmarc"],"titleterms":{"Do":[3,8],"What":[3,8],"_file":12,"access":2,"aggreg":[7,10],"align":3,"analyz":[5,6],"api":0,"backfil":4,"best":[3,8],"bug":1,"cli":12,"combin":4,"compat":5,"compos":12,"config":12,"configur":[2,12],"content":5,"contribut":1,"countri":6,"csv":10,"dashboard":7,"databas":6,"davmail":2,"depend":6,"dkim":[3,4],"dmarc":[3,5,7],"docker":12,"document":5,"domain":3,"elast":0,"elasticsearch":4,"env":12,"environ":12,"ew":2,"exampl":12,"exchang":6,"failur":[7,10],"featur":5,"field":4,"file":12,"geolite2":6,"grafana":9,"guid":3,"help":12,"inbox":2,"index":4,"indic":0,"instal":[4,6,9],"ip":6,"json":10,"kibana":[4,7],"list":[3,8],"listserv":[3,8],"lookalik":3,"mail":[3,8],"mailman":[3,8],"map":12,"maxmind":6,"microsoft":6,"mode":12,"multi":12,"multipl":6,"name":12,"onli":12,"open":5,"opensearch":[0,9],"option":6,"output":10,"owa":2,"parsedmarc":[0,1,2,5,6,12],"pattern":4,"perform":12,"practic":[3,8],"prerequisit":6,"proxi":6,"python":5,"record":[3,4,9],"refer":0,"reload":12,"report":[1,5,6,7,10],"resourc":3,"restart":12,"result":4,"retent":[4,9],"run":[2,12],"sampl":10,"secret":12,"section":12,"sender":3,"servic":[2,12],"smtp":[7,10],"sourc":5,"specifi":12,"spf":[3,4],"splunk":[0,11],"suffix":12,"support":[3,12],"systemd":[2,12],"t":3,"tabl":0,"tenant":12,"test":6,"tls":[7,10],"tune":12,"type":0,"understand":3,"upgrad":4,"use":[2,6,7,12],"util":0,"valid":3,"variabl":12,"via":12,"visual":5,"web":6,"without":12,"won":3,"workaround":[3,8]}})
\ No newline at end of file
+Search.setIndex({"alltitles":{"API reference":[[0,null]],"Accessing an inbox using OWA/EWS":[[2,null]],"Backfilling the combined DKIM/SPF result fields":[[4,"backfilling-the-combined-dkim-spf-result-fields"]],"Bug reports":[[1,"bug-reports"]],"CLI help":[[12,"cli-help"]],"CSV aggregate report":[[10,"csv-aggregate-report"]],"CSV failure report":[[10,"csv-failure-report"]],"Configuration file":[[12,"configuration-file"]],"Configuring parsedmarc for DavMail":[[2,"configuring-parsedmarc-for-davmail"]],"Contents":[[5,null]],"Contributing to parsedmarc":[[1,null]],"DMARC Alignment Guide":[[3,"dmarc-alignment-guide"]],"DMARC aggregate reports":[[7,"dmarc-aggregate-reports"]],"DMARC failure reports":[[7,"dmarc-failure-reports"]],"DMARC guides":[[3,"dmarc-guides"]],"Do":[[3,"do"],[8,"do"]],"Do not":[[3,"do-not"],[8,"do-not"]],"Docker Compose example":[[12,"docker-compose-example"]],"Docker secrets (_FILE suffix)":[[12,"docker-secrets-file-suffix"]],"Elasticsearch and Kibana":[[4,null]],"Environment variable configuration":[[12,"environment-variable-configuration"]],"Examples":[[12,"examples"]],"Features":[[5,"features"]],"IP-to-country database":[[6,"ip-to-country-database"]],"Indices and tables":[[0,"indices-and-tables"]],"Installation":[[4,"installation"],[6,null],[9,"installation"]],"Installing parsedmarc":[[6,"installing-parsedmarc"]],"JSON SMTP TLS report":[[10,"json-smtp-tls-report"]],"JSON aggregate report":[[10,"json-aggregate-report"]],"JSON failure report":[[10,"json-failure-report"]],"LISTSERV":[[3,"listserv"],[8,"listserv"]],"Lookalike domains":[[3,"lookalike-domains"]],"Mailbox messages are only archived once the reports are saved":[[12,"mailbox-messages-are-only-archived-once-the-reports-are-saved"]],"Mailing list best practices":[[3,"mailing-list-best-practices"],[8,"mailing-list-best-practices"]],"Mailman 2":[[3,"mailman-2"],[3,"id1"],[8,"mailman-2"],[8,"id1"]],"Mailman 3":[[3,"mailman-3"],[3,"id2"],[8,"mailman-3"],[8,"id2"]],"Multi-tenant support":[[12,"multi-tenant-support"]],"OpenSearch and Grafana":[[9,null]],"Optional dependencies":[[6,"optional-dependencies"]],"Performance tuning":[[12,"performance-tuning"]],"Prerequisites":[[6,"prerequisites"]],"Python Compatibility":[[5,"python-compatibility"]],"Records retention":[[4,"records-retention"],[9,"records-retention"]],"Reloading configuration without restarting":[[12,"reloading-configuration-without-restarting"]],"Resources":[[3,"resources"]],"Running DavMail as a systemd service":[[2,"running-davmail-as-a-systemd-service"]],"Running parsedmarc as a systemd service":[[12,"running-parsedmarc-as-a-systemd-service"]],"Running without a config file (env-only mode)":[[12,"running-without-a-config-file-env-only-mode"]],"SMTP TLS reporting":[[7,"smtp-tls-reporting"]],"SPF and DMARC record validation":[[3,"spf-and-dmarc-record-validation"]],"Sample aggregate report output":[[10,"sample-aggregate-report-output"]],"Sample failure report output":[[10,"sample-failure-report-output"]],"Sample outputs":[[10,null]],"Section name mapping":[[12,"section-name-mapping"]],"Specifying the config file via environment variable":[[12,"specifying-the-config-file-via-environment-variable"]],"Splunk":[[11,null]],"Testing multiple report analyzers":[[6,"testing-multiple-report-analyzers"]],"Understanding DMARC":[[3,null]],"Upgrading Kibana index patterns":[[4,"upgrading-kibana-index-patterns"]],"Using MaxMind GeoLite2 (optional)":[[6,"using-maxmind-geolite2-optional"]],"Using Microsoft Exchange":[[6,"using-microsoft-exchange"]],"Using a web proxy":[[6,"using-a-web-proxy"]],"Using parsedmarc":[[12,null]],"Using parsedmarc as a library":[[12,"using-parsedmarc-as-a-library"]],"Using the Kibana dashboards":[[7,null]],"What about mailing lists?":[[3,"what-about-mailing-lists"],[8,null]],"What if a sender won\u2019t support DKIM/DMARC?":[[3,"what-if-a-sender-wont-support-dkim-dmarc"]],"Workarounds":[[3,"workarounds"],[8,"workarounds"]],"parsedmarc":[[0,"module-parsedmarc"]],"parsedmarc documentation - Open source DMARC report analyzer and visualizer":[[5,null]],"parsedmarc.config":[[0,"module-parsedmarc.config"]],"parsedmarc.elastic":[[0,"module-parsedmarc.elastic"]],"parsedmarc.opensearch":[[0,"module-parsedmarc.opensearch"]],"parsedmarc.splunk":[[0,"module-parsedmarc.splunk"]],"parsedmarc.types":[[0,"module-parsedmarc.types"]],"parsedmarc.utils":[[0,"module-parsedmarc.utils"]]},"docnames":["api","contributing","davmail","dmarc","elasticsearch","index","installation","kibana","mailing-lists","opensearch","output","splunk","usage"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.todo":2,"sphinx.ext.viewcode":1},"filenames":["api.md","contributing.md","davmail.md","dmarc.md","elasticsearch.md","index.md","installation.md","kibana.md","mailing-lists.md","opensearch.md","output.md","splunk.md","usage.md"],"indexentries":{"aggregatealignment (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateAlignment",false]],"aggregateauthresultdkim (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateAuthResultDKIM",false]],"aggregateauthresults (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateAuthResults",false]],"aggregateauthresultspf (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateAuthResultSPF",false]],"aggregateidentifiers (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateIdentifiers",false]],"aggregateparsedreport (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateParsedReport",false]],"aggregatepolicyevaluated (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregatePolicyEvaluated",false]],"aggregatepolicyoverridereason (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregatePolicyOverrideReason",false]],"aggregatepolicypublished (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregatePolicyPublished",false]],"aggregaterecord (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateRecord",false]],"aggregatereport (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateReport",false]],"aggregatereportmetadata (class in parsedmarc.types)":[[0,"parsedmarc.types.AggregateReportMetadata",false]],"alreadysaved":[[0,"parsedmarc.elastic.AlreadySaved",false],[0,"parsedmarc.opensearch.AlreadySaved",false]],"always_use_local_files (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.always_use_local_files",false]],"append_json() (in module parsedmarc)":[[0,"parsedmarc.append_json",false]],"close() (parsedmarc.splunk.hecclient method)":[[0,"parsedmarc.splunk.HECClient.close",false]],"configure_ipinfo_api() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.configure_ipinfo_api",false]],"convert_outlook_msg() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.convert_outlook_msg",false]],"create_indexes() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.create_indexes",false]],"create_indexes() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.create_indexes",false]],"decode_base64() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.decode_base64",false]],"dns_retries (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.dns_retries",false]],"dns_timeout (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.dns_timeout",false]],"downloaderror":[[0,"parsedmarc.utils.DownloadError",false]],"elasticsearcherror":[[0,"parsedmarc.elastic.ElasticsearchError",false]],"email_results() (in module parsedmarc)":[[0,"parsedmarc.email_results",false]],"email_results_via_msgraph() (in module parsedmarc)":[[0,"parsedmarc.email_results_via_msgraph",false]],"emailaddress (class in parsedmarc.types)":[[0,"parsedmarc.types.EmailAddress",false]],"emailattachment (class in parsedmarc.types)":[[0,"parsedmarc.types.EmailAttachment",false]],"emailparsererror":[[0,"parsedmarc.utils.EmailParserError",false]],"extract_report() (in module parsedmarc)":[[0,"parsedmarc.extract_report",false]],"extract_report_from_file_path() (in module parsedmarc)":[[0,"parsedmarc.extract_report_from_file_path",false]],"failureparsedreport (class in parsedmarc.types)":[[0,"parsedmarc.types.FailureParsedReport",false]],"failurereport (class in parsedmarc.types)":[[0,"parsedmarc.types.FailureReport",false]],"forensicparsedreport (in module parsedmarc.types)":[[0,"parsedmarc.types.ForensicParsedReport",false]],"forensicreport (in module parsedmarc.types)":[[0,"parsedmarc.types.ForensicReport",false]],"get_base_domain() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_base_domain",false]],"get_dmarc_reports_from_mailbox() (in module parsedmarc)":[[0,"parsedmarc.get_dmarc_reports_from_mailbox",false]],"get_dmarc_reports_from_mbox() (in module parsedmarc)":[[0,"parsedmarc.get_dmarc_reports_from_mbox",false]],"get_filename_safe_string() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_filename_safe_string",false]],"get_ip_address_country() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_ip_address_country",false]],"get_ip_address_db_record() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_ip_address_db_record",false]],"get_ip_address_info() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_ip_address_info",false]],"get_report_zip() (in module parsedmarc)":[[0,"parsedmarc.get_report_zip",false]],"get_reverse_dns() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_reverse_dns",false]],"get_service_from_reverse_dns_base_domain() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.get_service_from_reverse_dns_base_domain",false]],"hecclient (class in parsedmarc.splunk)":[[0,"parsedmarc.splunk.HECClient",false]],"human_timestamp_to_datetime() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.human_timestamp_to_datetime",false]],"human_timestamp_to_unix_timestamp() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.human_timestamp_to_unix_timestamp",false]],"invalidaggregatereport":[[0,"parsedmarc.InvalidAggregateReport",false]],"invaliddmarcreport":[[0,"parsedmarc.InvalidDMARCReport",false]],"invalidfailurereport":[[0,"parsedmarc.InvalidFailureReport",false]],"invalidforensicreport (in module parsedmarc)":[[0,"parsedmarc.InvalidForensicReport",false]],"invalidipinfoapikey":[[0,"parsedmarc.utils.InvalidIPinfoAPIKey",false]],"invalidsmtptlsreport":[[0,"parsedmarc.InvalidSMTPTLSReport",false]],"ip_address_cache (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.ip_address_cache",false]],"ip_db_path (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.ip_db_path",false]],"ipaddressinfo (class in parsedmarc.utils)":[[0,"parsedmarc.utils.IPAddressInfo",false]],"ipsourceinfo (class in parsedmarc.types)":[[0,"parsedmarc.types.IPSourceInfo",false]],"is_mbox() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.is_mbox",false]],"is_outlook_msg() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.is_outlook_msg",false]],"load_ip_db() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.load_ip_db",false]],"load_psl_overrides() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.load_psl_overrides",false]],"load_reverse_dns_map() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.load_reverse_dns_map",false]],"migrate_indexes() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.migrate_indexes",false]],"migrate_indexes() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.migrate_indexes",false]],"module":[[0,"module-parsedmarc",false],[0,"module-parsedmarc.config",false],[0,"module-parsedmarc.elastic",false],[0,"module-parsedmarc.opensearch",false],[0,"module-parsedmarc.splunk",false],[0,"module-parsedmarc.types",false],[0,"module-parsedmarc.utils",false]],"nameservers (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.nameservers",false]],"normalize_timespan_threshold_hours (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.normalize_timespan_threshold_hours",false]],"offline (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.offline",false]],"opensearcherror":[[0,"parsedmarc.opensearch.OpenSearchError",false]],"parse_aggregate_report_file() (in module parsedmarc)":[[0,"parsedmarc.parse_aggregate_report_file",false]],"parse_aggregate_report_xml() (in module parsedmarc)":[[0,"parsedmarc.parse_aggregate_report_xml",false]],"parse_email() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.parse_email",false]],"parse_failure_report() (in module parsedmarc)":[[0,"parsedmarc.parse_failure_report",false]],"parse_forensic_report() (in module parsedmarc)":[[0,"parsedmarc.parse_forensic_report",false]],"parse_report_email() (in module parsedmarc)":[[0,"parsedmarc.parse_report_email",false]],"parse_report_file() (in module parsedmarc)":[[0,"parsedmarc.parse_report_file",false]],"parse_smtp_tls_report_json() (in module parsedmarc)":[[0,"parsedmarc.parse_smtp_tls_report_json",false]],"parsed_aggregate_reports_to_csv() (in module parsedmarc)":[[0,"parsedmarc.parsed_aggregate_reports_to_csv",false]],"parsed_aggregate_reports_to_csv_rows() (in module parsedmarc)":[[0,"parsedmarc.parsed_aggregate_reports_to_csv_rows",false]],"parsed_failure_reports_to_csv() (in module parsedmarc)":[[0,"parsedmarc.parsed_failure_reports_to_csv",false]],"parsed_failure_reports_to_csv_rows() (in module parsedmarc)":[[0,"parsedmarc.parsed_failure_reports_to_csv_rows",false]],"parsed_forensic_reports_to_csv() (in module parsedmarc)":[[0,"parsedmarc.parsed_forensic_reports_to_csv",false]],"parsed_forensic_reports_to_csv_rows() (in module parsedmarc)":[[0,"parsedmarc.parsed_forensic_reports_to_csv_rows",false]],"parsed_smtp_tls_reports_to_csv() (in module parsedmarc)":[[0,"parsedmarc.parsed_smtp_tls_reports_to_csv",false]],"parsed_smtp_tls_reports_to_csv_rows() (in module parsedmarc)":[[0,"parsedmarc.parsed_smtp_tls_reports_to_csv_rows",false]],"parsedemail (class in parsedmarc.types)":[[0,"parsedmarc.types.ParsedEmail",false]],"parsedmarc":[[0,"module-parsedmarc",false]],"parsedmarc.config":[[0,"module-parsedmarc.config",false]],"parsedmarc.elastic":[[0,"module-parsedmarc.elastic",false]],"parsedmarc.opensearch":[[0,"module-parsedmarc.opensearch",false]],"parsedmarc.splunk":[[0,"module-parsedmarc.splunk",false]],"parsedmarc.types":[[0,"module-parsedmarc.types",false]],"parsedmarc.utils":[[0,"module-parsedmarc.utils",false]],"parserconfig (class in parsedmarc.config)":[[0,"parsedmarc.config.ParserConfig",false]],"parsererror":[[0,"parsedmarc.ParserError",false]],"parsingresults (class in parsedmarc.types)":[[0,"parsedmarc.types.ParsingResults",false]],"psl_overrides_path (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.psl_overrides_path",false]],"psl_overrides_url (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.psl_overrides_url",false]],"query_dns() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.query_dns",false]],"reverse_dns_map (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.reverse_dns_map",false]],"reverse_dns_map_path (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.reverse_dns_map_path",false]],"reverse_dns_map_url (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.reverse_dns_map_url",false]],"reversednsservice (class in parsedmarc.utils)":[[0,"parsedmarc.utils.ReverseDNSService",false]],"save_aggregate_report_to_elasticsearch() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.save_aggregate_report_to_elasticsearch",false]],"save_aggregate_report_to_opensearch() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.save_aggregate_report_to_opensearch",false]],"save_aggregate_reports_to_splunk() (parsedmarc.splunk.hecclient method)":[[0,"parsedmarc.splunk.HECClient.save_aggregate_reports_to_splunk",false]],"save_failure_report_to_elasticsearch() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.save_failure_report_to_elasticsearch",false]],"save_failure_report_to_opensearch() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.save_failure_report_to_opensearch",false]],"save_failure_reports_to_splunk() (parsedmarc.splunk.hecclient method)":[[0,"parsedmarc.splunk.HECClient.save_failure_reports_to_splunk",false]],"save_forensic_report_to_elasticsearch() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.save_forensic_report_to_elasticsearch",false]],"save_forensic_report_to_opensearch() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.save_forensic_report_to_opensearch",false]],"save_forensic_reports_to_splunk() (parsedmarc.splunk.hecclient method)":[[0,"parsedmarc.splunk.HECClient.save_forensic_reports_to_splunk",false]],"save_output() (in module parsedmarc)":[[0,"parsedmarc.save_output",false]],"save_smtp_tls_report_to_elasticsearch() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.save_smtp_tls_report_to_elasticsearch",false]],"save_smtp_tls_report_to_opensearch() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.save_smtp_tls_report_to_opensearch",false]],"save_smtp_tls_reports_to_splunk() (parsedmarc.splunk.hecclient method)":[[0,"parsedmarc.splunk.HECClient.save_smtp_tls_reports_to_splunk",false]],"seen_aggregate_report_ids (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.seen_aggregate_report_ids",false]],"set_hosts() (in module parsedmarc.elastic)":[[0,"parsedmarc.elastic.set_hosts",false]],"set_hosts() (in module parsedmarc.opensearch)":[[0,"parsedmarc.opensearch.set_hosts",false]],"smtptlsfailuredetails (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSFailureDetails",false]],"smtptlsfailuredetailsoptional (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSFailureDetailsOptional",false]],"smtptlsparsedreport (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSParsedReport",false]],"smtptlspolicy (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSPolicy",false]],"smtptlspolicysummary (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSPolicySummary",false]],"smtptlsreport (class in parsedmarc.types)":[[0,"parsedmarc.types.SMTPTLSReport",false]],"splunkerror":[[0,"parsedmarc.splunk.SplunkError",false]],"strip_attachment_payloads (parsedmarc.config.parserconfig attribute)":[[0,"parsedmarc.config.ParserConfig.strip_attachment_payloads",false]],"timestamp_to_datetime() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.timestamp_to_datetime",false]],"timestamp_to_human() (in module parsedmarc.utils)":[[0,"parsedmarc.utils.timestamp_to_human",false]],"watch_inbox() (in module parsedmarc)":[[0,"parsedmarc.watch_inbox",false]]},"objects":{"":[[0,0,0,"-","parsedmarc"]],"parsedmarc":[[0,1,1,"","InvalidAggregateReport"],[0,1,1,"","InvalidDMARCReport"],[0,1,1,"","InvalidFailureReport"],[0,2,1,"","InvalidForensicReport"],[0,1,1,"","InvalidSMTPTLSReport"],[0,1,1,"","ParserError"],[0,3,1,"","append_json"],[0,0,0,"-","config"],[0,0,0,"-","elastic"],[0,3,1,"","email_results"],[0,3,1,"","email_results_via_msgraph"],[0,3,1,"","extract_report"],[0,3,1,"","extract_report_from_file_path"],[0,3,1,"","get_dmarc_reports_from_mailbox"],[0,3,1,"","get_dmarc_reports_from_mbox"],[0,3,1,"","get_report_zip"],[0,0,0,"-","opensearch"],[0,3,1,"","parse_aggregate_report_file"],[0,3,1,"","parse_aggregate_report_xml"],[0,3,1,"","parse_failure_report"],[0,3,1,"","parse_forensic_report"],[0,3,1,"","parse_report_email"],[0,3,1,"","parse_report_file"],[0,3,1,"","parse_smtp_tls_report_json"],[0,3,1,"","parsed_aggregate_reports_to_csv"],[0,3,1,"","parsed_aggregate_reports_to_csv_rows"],[0,3,1,"","parsed_failure_reports_to_csv"],[0,3,1,"","parsed_failure_reports_to_csv_rows"],[0,3,1,"","parsed_forensic_reports_to_csv"],[0,3,1,"","parsed_forensic_reports_to_csv_rows"],[0,3,1,"","parsed_smtp_tls_reports_to_csv"],[0,3,1,"","parsed_smtp_tls_reports_to_csv_rows"],[0,3,1,"","save_output"],[0,0,0,"-","splunk"],[0,0,0,"-","types"],[0,0,0,"-","utils"],[0,3,1,"","watch_inbox"]],"parsedmarc.config":[[0,4,1,"","ParserConfig"]],"parsedmarc.config.ParserConfig":[[0,2,1,"","always_use_local_files"],[0,2,1,"","dns_retries"],[0,2,1,"","dns_timeout"],[0,2,1,"","ip_address_cache"],[0,2,1,"","ip_db_path"],[0,2,1,"","nameservers"],[0,2,1,"","normalize_timespan_threshold_hours"],[0,2,1,"","offline"],[0,2,1,"","psl_overrides_path"],[0,2,1,"","psl_overrides_url"],[0,2,1,"","reverse_dns_map"],[0,2,1,"","reverse_dns_map_path"],[0,2,1,"","reverse_dns_map_url"],[0,2,1,"","seen_aggregate_report_ids"],[0,2,1,"","strip_attachment_payloads"]],"parsedmarc.elastic":[[0,1,1,"","AlreadySaved"],[0,1,1,"","ElasticsearchError"],[0,3,1,"","create_indexes"],[0,3,1,"","migrate_indexes"],[0,3,1,"","save_aggregate_report_to_elasticsearch"],[0,3,1,"","save_failure_report_to_elasticsearch"],[0,3,1,"","save_forensic_report_to_elasticsearch"],[0,3,1,"","save_smtp_tls_report_to_elasticsearch"],[0,3,1,"","set_hosts"]],"parsedmarc.opensearch":[[0,1,1,"","AlreadySaved"],[0,1,1,"","OpenSearchError"],[0,3,1,"","create_indexes"],[0,3,1,"","migrate_indexes"],[0,3,1,"","save_aggregate_report_to_opensearch"],[0,3,1,"","save_failure_report_to_opensearch"],[0,3,1,"","save_forensic_report_to_opensearch"],[0,3,1,"","save_smtp_tls_report_to_opensearch"],[0,3,1,"","set_hosts"]],"parsedmarc.splunk":[[0,4,1,"","HECClient"],[0,1,1,"","SplunkError"]],"parsedmarc.splunk.HECClient":[[0,5,1,"","close"],[0,5,1,"","save_aggregate_reports_to_splunk"],[0,5,1,"","save_failure_reports_to_splunk"],[0,5,1,"","save_forensic_reports_to_splunk"],[0,5,1,"","save_smtp_tls_reports_to_splunk"]],"parsedmarc.types":[[0,4,1,"","AggregateAlignment"],[0,4,1,"","AggregateAuthResultDKIM"],[0,4,1,"","AggregateAuthResultSPF"],[0,4,1,"","AggregateAuthResults"],[0,4,1,"","AggregateIdentifiers"],[0,4,1,"","AggregateParsedReport"],[0,4,1,"","AggregatePolicyEvaluated"],[0,4,1,"","AggregatePolicyOverrideReason"],[0,4,1,"","AggregatePolicyPublished"],[0,4,1,"","AggregateRecord"],[0,4,1,"","AggregateReport"],[0,4,1,"","AggregateReportMetadata"],[0,4,1,"","EmailAddress"],[0,4,1,"","EmailAttachment"],[0,4,1,"","FailureParsedReport"],[0,4,1,"","FailureReport"],[0,2,1,"","ForensicParsedReport"],[0,2,1,"","ForensicReport"],[0,4,1,"","IPSourceInfo"],[0,4,1,"","ParsedEmail"],[0,4,1,"","ParsingResults"],[0,4,1,"","SMTPTLSFailureDetails"],[0,4,1,"","SMTPTLSFailureDetailsOptional"],[0,4,1,"","SMTPTLSParsedReport"],[0,4,1,"","SMTPTLSPolicy"],[0,4,1,"","SMTPTLSPolicySummary"],[0,4,1,"","SMTPTLSReport"]],"parsedmarc.utils":[[0,1,1,"","DownloadError"],[0,1,1,"","EmailParserError"],[0,4,1,"","IPAddressInfo"],[0,1,1,"","InvalidIPinfoAPIKey"],[0,4,1,"","ReverseDNSService"],[0,3,1,"","configure_ipinfo_api"],[0,3,1,"","convert_outlook_msg"],[0,3,1,"","decode_base64"],[0,3,1,"","get_base_domain"],[0,3,1,"","get_filename_safe_string"],[0,3,1,"","get_ip_address_country"],[0,3,1,"","get_ip_address_db_record"],[0,3,1,"","get_ip_address_info"],[0,3,1,"","get_reverse_dns"],[0,3,1,"","get_service_from_reverse_dns_base_domain"],[0,3,1,"","human_timestamp_to_datetime"],[0,3,1,"","human_timestamp_to_unix_timestamp"],[0,3,1,"","is_mbox"],[0,3,1,"","is_outlook_msg"],[0,3,1,"","load_ip_db"],[0,3,1,"","load_psl_overrides"],[0,3,1,"","load_reverse_dns_map"],[0,3,1,"","parse_email"],[0,3,1,"","query_dns"],[0,3,1,"","timestamp_to_datetime"],[0,3,1,"","timestamp_to_human"]]},"objnames":{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","attribute","Python attribute"],"3":["py","function","Python function"],"4":["py","class","Python class"],"5":["py","method","Python method"]},"objtypes":{"0":"py:module","1":"py:exception","2":"py:attribute","3":"py:function","4":"py:class","5":"py:method"},"terms":{"00z":10,"00z_exampl":10,"09t00":10,"09t23":10,"1d":12,"1g":4,"1w":12,"2017a":[3,8],"21vianet":12,"2d":12,"2k":12,"30s":12,"3d":10,"3h":12,"59z":10,"5m":[2,12],"7d":12,"A":[0,3,7,12],"AT":10,"After":[2,4,12],"All":[3,8,12],"An":[0,12],"And":0,"As":[4,7],"By":[7,12],"Do":[0,12],"Each":[4,7,12],"For":[0,4,12],"From":[3,8,12],"Further":7,"Have":12,"Here":10,"How":[0,12],"I":12,"IF":12,"If":[0,3,4,6,7,8,12],"In":[0,2,3,7,8,12],"It":[0,2,4,7,10,12],"More":6,"Most":[7,12],"NOT":12,"No":[3,6,8],"Not":[0,12],"On":[3,4,6,7,8,12],"Or":[4,6,12],"So":12,"Some":[2,3,7,8],"That":[0,7,12],"The":[0,3,4,6,7,11,12],"Then":[2,3,4,6,8,12],"There":7,"These":[7,12],"This":[0,5,6,7,10,12],"To":[0,2,4,6,7,9,10,12],"Under":[4,7],"What":5,"When":[0,3,5,8,12],"Where":[3,8],"While":[7,12],"With":[0,7,12],"YOUR":4,"You":[2,7,12],"_":12,"__getstate__":0,"__init__":0,"__setstate__":0,"_attempt":0,"_cluster":12,"_input":0,"_ipdatabaserecord":0,"_serverless_rejected_set":0,"_sourc":4,"_task":4,"_update_by_queri":[0,4],"aadst":12,"abl":6,"abort":[4,12],"abov":[0,2,4,6,7,12],"absent":4,"accept":[0,3,4,7,8,12],"access":[0,4,5,12],"access_key_id":12,"access_token":0,"accessright":12,"accident":[3,8],"account":[6,7,12],"accumul":0,"acm":10,"acquir":12,"acquisit":12,"across":[0,7,12],"action":[3,8,12],"activ":[4,5,12],"active_primary_shard":12,"active_shard":12,"actual":[0,3,10],"ad":12,"add":[0,2,3,4,6,7,8,12],"addit":[3,8,12],"address":[0,2,3,4,6,7,8,10,12],"addresse":7,"adjust":4,"adkim":10,"admin":[3,8,12],"administr":[3,8,12],"advanc":7,"afterward":0,"agari":5,"age":12,"agent":4,"agg":7,"aggreg":[0,4,5,11,12],"aggregate_csv_filenam":[0,12],"aggregate_index":0,"aggregate_json_filenam":[0,12],"aggregate_report":[0,12],"aggregate_top":12,"aggregate_url":12,"aggregatealign":0,"aggregateauthresult":0,"aggregateauthresultdkim":0,"aggregateauthresultspf":0,"aggregateidentifi":0,"aggregateparsedreport":0,"aggregatepolicyevalu":0,"aggregatepolicyoverridereason":0,"aggregatepolicypublish":0,"aggregaterecord":0,"aggregatereport":0,"aggregatereportmetadata":0,"alia":[0,12],"alias":12,"align":[4,5,7,10],"aliv":0,"allow":[2,3,8,12],"allow_unencrypted_storag":12,"allowremot":2,"allowstringindic":7,"alon":12,"alreadi":[0,4,12],"alreadysav":0,"also":[0,2,3,4,6,7,8,12],"alter":[3,8],"altern":[5,12],"although":11,"alway":[0,2,4,12],"always_use_local_fil":[0,12],"amazon":5,"amd64":12,"amount":12,"analyt":[5,12],"analyz":12,"ani":[0,3,4,6,7,8,12],"anonym":10,"anoth":[6,12],"answer":[0,12],"apach":5,"api":[2,4,5,12],"api_key":[0,12],"app":12,"appear":[4,7,12],"append":[0,12],"append_json":0,"appendix":10,"appid":12,"appli":[0,12],"applic":[4,12],"applicationaccesspolici":12,"approach":12,"approxim":2,"apt":[2,4,6],"archiv":0,"archive_directori":12,"archive_fold":[0,12],"argument":[0,12],"arm64":12,"around":12,"array":[0,4],"arraylist":4,"arriv":12,"arrival_d":10,"arrival_date_utc":[0,10],"artifact":4,"as_domain":[0,10],"as_nam":[0,10],"asid":12,"ask":3,"asmx":2,"asn":[0,6,10,12],"aspf":10,"assembl":0,"assert":12,"assign":4,"associ":0,"assum":12,"assume_utc":0,"astimezon":0,"att":10,"attach":[0,3,8,10,12],"attachment_filenam":0,"attempt":[0,12],"attribut":[6,7],"audit":4,"auth":[0,2,4,7,10,12],"auth_failur":10,"auth_method":12,"auth_mod":12,"auth_result":10,"auth_typ":[0,12],"authent":[0,2,3,4,7,12],"authentication_mechan":10,"authentication_result":10,"authentication_typ":12,"auto":2,"automat":[4,6,12],"avail":[0,6,12],"avoid":[7,12],"aw":[0,12],"aws_region":[0,12],"aws_servic":[0,12],"awssigv4":[0,12],"az":12,"azur":[5,12],"b":10,"b2c":7,"back":[0,12],"backend":[0,12],"backfil":[0,5,12],"background":[0,4],"backlog":12,"backward":[0,12],"bare":12,"base":[0,2,3,4,6,7,8,10,12],"base64":0,"base_domain":[0,10],"basic":[0,2,12],"batch":[0,12],"batch_siz":[0,12],"bcc":[0,10],"bd6e1bb5":10,"becaus":[2,3,4,7,8,12],"becom":12,"befor":[0,12],"begin":12,"begin_d":10,"behavior":[0,12],"behind":6,"benefit":[0,5],"best":[7,12],"beyond":0,"bin":[2,4,6,12],"binari":[0,12],"binaryio":0,"bind":[0,2],"bindaddress":2,"blank":[3,8],"block":[0,2,4,12],"bodi":[0,3,8,10,12],"bookkeep":0,"bool":[0,4,12],"bound":12,"boundari":[0,12],"box":12,"brand":[0,5,7],"break":[3,4,8],"briefli":12,"broken":[0,12],"broker":12,"browser":4,"bucket":12,"budget":0,"bug":[5,12],"build":6,"built":[0,12],"bundl":[0,6,7,12],"busi":7,"button":[3,8],"byte":0,"c":[10,12],"ca":[4,12],"cach":[0,12],"cafile_path":12,"call":[0,5,12],"callabl":0,"callback":0,"caller":0,"came":[3,8],"can":[0,2,3,4,5,6,7,8,12],"cap":[0,12],"captur":0,"carri":[0,4,6,7,12],"case":[2,3,8,12],"catch":[0,12],"caught":0,"caus":[3,4,7,8,12],"cc":[0,10],"center":7,"cento":[4,6],"central":0,"cert":[4,12],"cert_path":12,"certain":[0,12],"certfile_path":12,"certif":[0,4,7,12],"certificate_password":12,"certificate_path":12,"cest":10,"cfg":0,"chain":0,"chang":[4,7,11,12],"charact":[2,12],"charset":10,"chart":7,"cheap":[0,4],"check":[0,2,3,4,7,12],"check_timeout":[0,12],"checkbox":4,"checkdmarc":3,"china":12,"chinacloudapi":12,"chines":7,"chmod":[2,4,12],"choos":[3,8],"chown":[2,12],"ci":7,"circular":0,"cisco":12,"class":[0,4],"classifi":0,"clean":[0,12],"clear":0,"cli":[0,5],"click":[4,7],"client":[2,3,4,8,12],"client_assert":12,"client_id":12,"client_secret":12,"clientassert":12,"clientsecret":12,"clientsotimeout":2,"clock":0,"close":[0,12],"cloud":[0,12],"cloudflar":[0,12],"cluster":[0,4,12],"cn":12,"co":4,"code":[0,4,12],"collect":[7,12],"collector":[11,12],"column":7,"com":[1,2,3,8,9,10,12],"combin":[0,5,7,12],"come":[0,7,12],"comfort":12,"comma":[6,12],"command":[0,2,3,4,6,8,12],"comment":12,"commerci":[4,5],"commit":0,"common":[3,4,6,8],"communiti":[3,8],"compat":[0,7,12],"complet":[0,3,4,12],"compli":[3,4,6,8,9],"complianc":7,"compliant":[3,8],"compon":[6,7],"compress":5,"comput":7,"condit":7,"conf":6,"config":[2,5,6],"config_fil":12,"config_reload":0,"configur":[0,3,4,5,6,7,8,9],"configure_ipinfo_api":0,"confirm":12,"conflict":4,"conform":4,"connect":[0,2,4,12],"connection_str":12,"consent":12,"consid":[5,7],"consist":[0,5,10],"consol":[4,12],"constant":0,"construct":[0,12],"consult":6,"consum":[7,12],"contact":[7,12],"contain":[0,7,11,12],"content":[0,3,4,8,10,11,12],"continu":4,"contrib":6,"contribut":5,"control":[0,4,12],"convent":12,"convert":[0,3,8],"convert_outlook_msg":0,"copi":[0,6,11,12],"core":[3,8],"correct":[0,6,7,12],"correspond":[4,12],"corrupt":0,"count":[0,2,4,7,10,12],"counter":0,"countri":[0,7,10,12],"country_cod":0,"cover":12,"cpan":6,"cr":12,"crash":[2,4,12],"creat":[0,2,3,4,6,8,12],"create_fold":0,"create_index":0,"creation":12,"creativ":6,"credenti":[4,6,12],"credentials_fil":12,"cron":[6,12],"cross":0,"crt":4,"csr":4,"csv":[0,5,12],"csvs":12,"ctrl":12,"ctx":4,"cumul":6,"curl":4,"current":[0,2,4,12],"custom":[4,7,12],"d":[0,4,12],"daemon":[2,4,12],"daili":[0,12],"dashboard":[4,5,9,11],"dat":0,"data":[0,4,5,6,7,9,11,12],"databas":[0,12],"dataclass":0,"date":[0,3,8,10,12],"date_utc":10,"datetim":0,"davmail":5,"day":[0,4,9,12],"db_path":0,"dbip":[0,6,12],"dbname":12,"dce":12,"dcr":12,"dcr_aggregate_stream":12,"dcr_failure_stream":12,"dcr_immutable_id":12,"dcr_smtp_tls_stream":12,"dd":[0,12],"de":[0,10],"dearmor":4,"deb":4,"debian":[4,5,6],"debug":[0,12],"decemb":6,"decod":0,"decode_base64":0,"dedic":6,"dedup":0,"dedupl":[0,12],"deeper":4,"def":4,"default":[0,2,4,5,6,7,12],"default_factori":0,"defeat":0,"defect":4,"defens":[4,5],"defin":0,"delay":[0,2,10,12],"deleg":12,"delegated_us":12,"delet":[0,2,4,12],"delete_aggreg":[0,12],"delete_failur":[0,12],"delete_invalid":[0,12],"delete_smtp_tl":[0,12],"deliber":[0,12],"deliveri":[0,12],"delivery_result":10,"demystifi":3,"deni":12,"depend":[0,4,5,12],"deploy":[3,8,12],"deprec":[7,12],"depth":4,"deriv":0,"describ":12,"descript":[2,6,12],"design":12,"destin":[0,12],"det":4,"detail":[0,4,6,7,12],"detect":12,"determin":12,"dev":[6,12],"devel":6,"develop":5,"devicecod":12,"dict":0,"dictionari":0,"didn":12,"differ":[7,12],"difficult":12,"dig":0,"digest":[3,8],"dir":6,"direct":[7,12],"directori":[0,6,12],"dis":10,"disabl":[0,2,6,12],"disclaim":[3,8],"disk":[0,12],"display":[3,7,11],"display_nam":10,"disposit":[0,7,10],"distinguish":12,"distribut":6,"distro":6,"dk":4,"dkim":[0,5,7,8,10],"dkim_align":10,"dkim_domain":10,"dkim_result":[4,10],"dkim_results_combin":[0,4],"dkim_selector":10,"dkm":3,"dmarc":[0,4,6,8,9,10,11,12],"dmarc_aggreg":[4,7],"dmarc_align":10,"dmarc_failur":4,"dmarc_moderation_act":[3,8],"dmarc_none_moderation_act":[3,8],"dmarc_quarantine_moderation_act":[3,8],"dmarcian":5,"dmarcresport":12,"dnf":6,"dns":[0,3,6,7,12],"dns_retri":[0,12],"dns_test_address":12,"dns_timeout":[0,12],"dnspython":0,"doc":[0,9,12],"doctyp":10,"document":[0,2,4,7,12],"dod":12,"doe":[0,3,8,12],"doesn":[0,12],"dom":4,"domain":[0,4,7,8,10,12],"domainawar":[1,3,12],"don":3,"doubl":12,"download":[0,2,4,6,12],"downloaderror":0,"dr":4,"draft":[5,10,12],"drop":0,"dropdown":7,"dsn":12,"dtd":10,"dummi":12,"duplic":0,"dure":[2,12],"e":[0,2,3,4,6,8,12],"e7":10,"earlier":[0,7],"easi":[4,9],"easier":[11,12],"echo":4,"edit":[2,6,7,12],"editor":11,"effect":12,"effici":4,"either":[0,4,5,12],"elast":[4,5,7,12],"elasticsearch":[0,5,12],"elasticsearcherror":0,"elig":12,"elk":12,"els":[4,12],"email":[0,3,5,6,7,8,10,11,12],"email_result":0,"email_results_via_msgraph":0,"emailaddress":0,"emailattach":0,"emailparsererror":0,"empti":[0,3,4,8,12],"en":[3,4,8,10],"enabl":[2,4,7,12],"enableew":2,"enablekeepal":2,"enableproxi":2,"encod":[0,10,12],"encount":0,"encrypt":[4,12],"encryptedsavedobject":4,"encryptionkey":4,"end":[0,3,4,5,12],"end_dat":10,"endpoint":[5,12],"endpoint_url":12,"enforc":[3,8],"enough":12,"enrich":[0,6,12],"enrol":4,"ensur":[3,4,6,8],"enterpris":12,"entir":[0,3,7,8,12],"entra":12,"entri":[0,12],"envelop":3,"envelope_from":10,"envelope_to":10,"environ":[5,6],"eof":0,"eol":5,"epel":6,"equal":0,"equival":4,"era":0,"error":[0,4,7,10,12],"erroritemnotfound":12,"es":[0,12],"escap":12,"especi":[7,12],"etc":[0,2,3,4,6,8,12],"even":[2,3,8,12],"event":[2,11,12],"everi":[0,2,4,6,7,12],"everyth":12,"ew":5,"ex":12,"exact":[0,3,8,12],"exampl":[3,4,6,8,10],"exceed":[0,7],"except":[0,12],"exchang":[2,10,12],"exclud":[2,12],"execreload":12,"execstart":[2,12],"exercis":0,"exhaust":12,"exist":[0,3,4,8,12],"exit":[0,12],"expect":12,"expens":0,"expir":12,"expiri":7,"expiringdict":0,"explain":[3,8],"explicit":[0,3,6,8,12],"export":[0,4,7,12],"extens":12,"extra":12,"extract":[0,2],"extract_report":0,"extract_report_from_file_path":0,"eye":[2,12],"f":[0,4],"factor":2,"factori":0,"fail":[0,3,7,8,10,12],"fail_on_output_error":12,"failed_sess":7,"failed_session_count":10,"failov":0,"failur":[0,5,11,12],"failure_csv_filenam":[0,12],"failure_detail":[4,10],"failure_details_combin":[0,4],"failure_index":0,"failure_json_filenam":[0,12],"failure_report":0,"failure_top":12,"failure_url":12,"failureparsedreport":0,"failurereport":0,"fall":[0,12],"fallback":[0,12],"fals":[0,2,4,10,12],"fantast":[3,8],"fast":0,"faster":12,"fatal":[0,12],"favor":[0,12],"fds":4,"featur":[4,12],"feedback":0,"feedback_report":0,"feedback_typ":10,"fetch":[0,7,12],"field":[0,5,7],"file":[0,2,5,6,7,11],"file_path":[0,12],"filenam":[0,12],"filename_safe_subject":10,"filepath":12,"fill":[4,6],"filter":[0,3,7,8,11,12],"final":5,"financ":12,"find":[3,7,8,12],"fine":[3,8],"finish":12,"first":[0,3,6,7,8,12],"first_strip_reply_to":[3,8],"fit":[3,8,12],"fix":[0,4,12],"flag":[0,2,6,12],"flat":0,"flexibl":11,"flight":12,"float":[0,12],"flush":12,"fo":[0,10],"fold":0,"folder":[0,2,12],"foldersizelimit":2,"follow":[2,4,5,12],"footer":[3,8],"forc":12,"foreground":12,"forens":[5,7,12],"forensicparsedreport":0,"forensicreport":0,"forev":12,"form":12,"format":[0,7,12],"former":[5,7,12],"forward":[3,7,8],"found":[0,4,6,12],"foundat":10,"four":12,"fqdn":4,"fraud":5,"free":[6,12],"fresh":[0,4,12],"freshest":12,"friend":7,"from_is_list":[3,8],"frozen":0,"ftp_proxi":6,"full":[0,12],"fulli":[0,3,4,7,8,12],"function":[0,12],"functool":0,"g":[0,2,3,4,6,8,12],"gateway":2,"gb":4,"gcc":[6,12],"gdpr":[4,9],"gelf":[5,12],"general":[3,6,8,12],"generat":[3,4,8,10],"geoip":[0,6,12],"geoipupd":6,"geolite2":5,"geoloc":[0,12],"get":[0,2,4,6,12],"get_base_domain":0,"get_dmarc_reports_from_mailbox":[0,12],"get_dmarc_reports_from_mbox":[0,12],"get_filename_safe_str":0,"get_ip_address_countri":0,"get_ip_address_db_record":0,"get_ip_address_info":0,"get_report_zip":0,"get_reverse_dn":0,"get_service_from_reverse_dns_base_domain":0,"ghcr":12,"github":[0,1,6,10,12],"give":[0,4],"given":[0,12],"glass":7,"glob":12,"global":12,"gmail":[0,5,7,12],"gmail_api":12,"go":[0,3,8],"goe":[3,8],"googl":[7,12],"googleapi":12,"got":12,"gov":12,"govern":12,"gpg":4,"grafana":5,"grant":12,"graph":[0,2,5,7,12],"graph_url":12,"graylog":5,"group":[2,7,12],"guard":0,"guid":[4,5],"guidanc":12,"gz":12,"gzip":[0,5],"h":[0,4,12],"hamburg":4,"hand":[3,8],"handl":[0,5,12],"handler":[7,12],"happen":[0,4,12],"hard":12,"has_defect":10,"hasn":12,"head":10,"header":[0,3,7,8,10,12],"header_from":10,"headless":2,"health":12,"healthcar":12,"heap":4,"heavi":4,"hec":[0,11,12],"hecclient":0,"hectokengoesher":12,"held":[0,12],"help":5,"hh":0,"hierarchi":12,"high":[7,12],"higher":[3,8],"histor":[4,12],"histori":12,"hit":[0,12],"hold":[0,12],"home":6,"hop":10,"host":[0,2,3,4,5,8,12],"hostnam":[0,12],"hour":[0,12],"hover":7,"href":10,"html":[3,4,8,10],"http":[0,2,4,5,6,10,11,12],"http_proxi":6,"https":[0,1,2,3,4,6,8,9,12],"https_proxi":6,"httpx":12,"human":[0,7],"human_timestamp":0,"human_timestamp_to_datetim":0,"human_timestamp_to_unix_timestamp":0,"hup":12,"icon":7,"id":[0,3,4,8,10,12],"ideal":[3,8],"idempot":[4,12],"ident":[0,3,4,8,12],"identifi":10,"idl":[0,2,12],"ignor":[0,12],"imag":12,"imap":[0,2,5,12],"imap_password":12,"imapalwaysapproxmsgs":2,"imapautoexpung":2,"imapcli":5,"imapconnect":12,"imapidledelay":2,"imapport":2,"immedi":[2,12],"immut":12,"impli":12,"import":[0,4,7,12],"improv":12,"inbox":[0,3,5,8,12],"inc":10,"includ":[0,3,4,6,7,8,12],"include_list_post_head":[3,8],"include_rfc2369_head":[3,8],"include_sender_head":[3,8],"include_spam_trash":12,"incom":[7,12],"incorrect":12,"increas":[4,12],"increment":12,"indefinit":12,"indent":12,"independ":0,"index":[0,5,7,9,11,12],"index_prefix":[0,4,12],"index_prefix_domain_map":12,"index_suffix":[0,4,12],"indic":[3,5],"individu":[0,7,12],"industri":12,"info":[0,12],"inform":[0,4,7,12],"infrequ":12,"ingest":12,"inherit":[0,12],"ini":[2,12],"init":0,"initi":[0,12],"inner":12,"input":[0,12],"input_":0,"insid":[4,12],"inspect":[0,12],"instal":[0,2,5,12],"installed_app":12,"instanc":[0,12],"instanceof":4,"instead":[0,3,4,6,8,12],"instruct":4,"int":[0,12],"intend":[3,8],"intent":0,"interact":[2,4,12],"interakt":10,"interfer":[3,8],"interleav":0,"interpret":[0,6],"interrupt":12,"interval":12,"interval_begin":10,"interval_end":10,"invalid":[0,12],"invalidaggregatereport":0,"invaliddmarcreport":0,"invalidfailurereport":0,"invalidforensicreport":0,"invalidipinfoapikey":0,"invalidsmtptlsreport":0,"invis":4,"invok":0,"involv":7,"io":[0,12],"ip":[0,3,4,7,12],"ip_address":[0,10],"ip_address_cach":0,"ip_db_path":[0,6,12],"ip_db_url":12,"ipaddressinfo":0,"ipinfo":[0,6,12],"ipinfo_api_token":12,"ipinfo_url":12,"ipsourceinfo":0,"ipv4":0,"ipv6":0,"is_mbox":0,"is_outlook_msg":0,"iso":[0,12],"isol":[0,12],"issu":[0,1,12],"item":[0,12],"java":2,"job":[3,6,8],"joe":[3,8],"journalctl":[2,12],"jre":2,"json":[0,4,5,12],"june":5,"junk":12,"just":[4,7,12],"jvm":4,"jwt":12,"kafka":[5,12],"kb4099855":6,"kb4134118":6,"kb4295699":6,"keep":[0,4,7,12],"keep_al":[0,12],"keepal":2,"kept":0,"key":[0,3,4,6,12],"keyfile_path":12,"keyout":4,"keyr":4,"keystor":4,"keyword":[0,12],"kibana":[5,11],"kill":12,"killsign":12,"kind":12,"know":3,"known":[0,3,7,8,12],"kubernet":12,"kwarg":0,"l4":12,"l5":12,"label":12,"lack":4,"lang":4,"languag":[3,8],"larg":[2,12],"larger":12,"last":6,"later":[0,4,6,12],"latest":[2,4,9,12],"layer":0,"layout":11,"leak":7,"least":[4,6,12],"leav":[0,3],"left":[0,7,12],"legaci":[0,5],"legal":[3,8],"legitim":[7,12],"less":12,"let":0,"level":[0,3,4,12],"lf":12,"libemail":6,"libpq":12,"librari":5,"libxml2":6,"libxslt":6,"licens":6,"life":5,"lifetim":0,"lifetimetimeout":0,"like":[0,3,6,7,8,12],"limit":[0,2,12],"line":[3,8,12],"link":[3,4,7,8],"linux":[3,6,8],"list":[0,2,4,5,7,12],"listen":[2,12],"lite":[0,6,12],"live":[7,12],"ll":[3,8],"load":[0,4,12],"load_ip_db":0,"load_psl_overrid":0,"load_reverse_dns_map":0,"local":[0,2,4,6,10,12],"local_file_path":0,"local_psl_overrides_path":12,"local_reverse_dns_map_path":12,"localhost":[4,12],"locat":[0,7,12],"log":[0,2,4,5,12],"log_analyt":12,"log_fil":12,"logger":12,"login":[4,12],"logstash":4,"long":[0,3,12],"longer":[3,6,8],"look":[0,3,7],"lookup":[0,12],"loop":[0,12],"loopback":2,"lose":12,"loss":0,"lot":7,"low":12,"lower":12,"lua":10,"m":[0,6,12],"m365":12,"maco":6,"magnifi":7,"mail":[0,5,6,10,12],"mail_bcc":0,"mail_cc":0,"mail_from":0,"mail_to":0,"mailbox":[0,7],"mailbox_check_timeout":12,"mailbox_connect":0,"mailboxconnect":0,"maildir":[0,12],"maildir_cr":12,"maildir_path":12,"mailer":10,"mailrelay":10,"mailsuit":[0,12],"mailto":6,"main":[4,12],"mainpid":12,"maintain":5,"make":[0,3,4,6,8,9,12],"malici":[7,12],"manag":[4,7,12],"mandatori":12,"mani":[0,12],"manual":[0,4,12],"map":0,"mariadb":12,"market":7,"massiv":12,"match":[0,4,7,11,12],"max_ag":10,"max_shards_per_nod":12,"max_unsaved_retri":[0,12],"maximum":4,"maxmind":[0,5,12],"may":[0,5,7,12],"mbox":[0,12],"md":0,"mean":[0,12],"meantim":0,"mechan":3,"member":[3,8,12],"memori":[0,12],"mention":7,"menu":[4,7],"mere":0,"merg":0,"messag":[0,2,3,4,6,7,8,10],"message_id":10,"meta":10,"method":12,"metric":7,"mfrom":[4,10],"microsoft":[0,2,5,10,12],"microsoftgraph":12,"microsoftonlin":12,"mid":12,"might":[0,3,7,8],"migrat":[0,7,12],"migrate_index":0,"mime":10,"min":0,"mind":12,"minim":12,"minimum":[4,12],"minimum_should_match":4,"minut":[0,2,12],"mirror":0,"miss":[4,6,12],"mitig":[3,8],"mix":0,"mm":[0,12],"mmdb":[0,6,12],"mobil":[3,8],"mode":[0,2,4,6,10],"modern":[2,3,8],"modifi":[0,3,8,12],"modul":[0,5,6,12],"mon":10,"monitor":[3,12],"month":[0,12],"monthly_index":[0,12],"mous":7,"move":[0,4,12],"ms":[0,10,12],"msal":12,"msg":[0,6],"msg_byte":0,"msg_date":0,"msg_footer":[3,8],"msg_header":[3,8],"msgconvert":[0,6],"msgraph":12,"msgraphconnect":0,"mta":7,"much":12,"multi":[2,5],"multipl":[0,7,12],"multipli":12,"multiprocess":0,"mung":[3,8],"must":[0,2,3,4,8,12],"must_not":4,"mutat":0,"mutual":[4,12],"mv":4,"mx":[7,10],"n":[0,10,12],"n_proc":[0,12],"naiv":0,"name":[0,3,4,7,10,11],"nameserv":[0,12],"nano":[2,12],"nation":12,"navig":[3,8],"ncontent":10,"ndate":10,"ndjson":[4,7],"necessarili":7,"need":[0,2,3,4,6,7,8,12],"negat":[0,12],"neither":[0,12],"nelson":[3,8],"net":[2,12],"network":[0,2,4,12],"never":[0,4,12],"new":[0,2,4,5,6,7,12],"newer":6,"newest":[2,12],"newkey":4,"newli":0,"news":3,"next":[0,4,12],"nfrom":10,"nmessag":10,"nmime":10,"node":4,"nologin":6,"non":[0,3,4,8,12],"nonameserv":0,"none":[0,3,4,10,12],"noproxyfor":2,"norepli":[3,10],"normal":[0,10,12],"normalize_timespan_threshold_hour":0,"normalized_timespan":10,"nosecureimap":2,"notabl":7,"note":[0,12],"noth":[4,12],"notic":12,"now":[0,4,6,7,12],"nowher":12,"nsubject":10,"nto":10,"null":[4,6,10],"number":[0,7,12],"number_of_replica":[0,12],"number_of_shard":[0,12],"numer":12,"nwettbewerb":10,"nx":10,"o":[2,4,12],"oR":6,"oauth2":12,"oauth2_port":12,"object":[0,4,7,12],"observ":[7,12],"occur":[0,7],"occurr":11,"oct":10,"offic":2,"office365":2,"offici":12,"offlin":[0,6,12],"offset":[0,12],"often":[7,12],"old":[0,7],"older":[4,6,10,12],"oldest":[2,12],"ole":[0,6],"omit":[0,12],"onc":[0,4,7],"ondmarc":5,"one":[0,3,4,5,6,7,8,12],"onli":[0,2,3,4,6,7,8],"onlin":[0,2,12],"onto":0,"oor":0,"op":0,"open":[0,3],"opendn":12,"opensearch":[4,5,7,12],"opensearch_dashboard":7,"opensearcherror":0,"openssl":4,"oper":12,"opt":[2,6,12],"option":[0,2,3,4,5,8,11,12],"orchestr":[0,12],"order":12,"org":[0,6,9,10,12],"org_email":10,"org_extra_contact_info":10,"org_nam":10,"organiz":[2,5,7,12],"organization_nam":10,"origin":[3,8,12],"original_envelope_id":10,"original_mail_from":10,"original_rcpt_to":10,"original_timespan_second":10,"os":0,"oserror":0,"otherwis":[0,12],"outag":12,"outdat":7,"outgo":[3,8,12],"outlook":[0,2,6,12],"output":[0,5,12],"output_directori":0,"outsid":12,"overal":0,"overrid":[0,6,12],"overwrit":[0,4,12],"overwritten":12,"owa":[5,12],"owned":6,"ownership":6,"owns":[0,12],"p":[3,4,10],"p12":4,"pack":4,"packag":[0,4,6,12],"packet":0,"pad":[0,12],"page":[3,4,6,7,8],"paginate_messag":12,"painless":4,"pair":[4,7],"pan":10,"panel":7,"parallel":[0,12],"paramet":[0,12],"parent":7,"pars":[0,3,5,6,10,12],"parse_aggregate_report_fil":[0,12],"parse_aggregate_report_xml":[0,12],"parse_email":0,"parse_failure_report":[0,12],"parse_forensic_report":0,"parse_report_email":[0,12],"parse_report_fil":[0,12],"parse_smtp_tls_report_json":0,"parsed_aggregate_reports_to_csv":0,"parsed_aggregate_reports_to_csv_row":0,"parsed_failure_reports_to_csv":0,"parsed_failure_reports_to_csv_row":0,"parsed_forensic_reports_to_csv":0,"parsed_forensic_reports_to_csv_row":0,"parsed_sampl":10,"parsed_smtp_tls_reports_to_csv":0,"parsed_smtp_tls_reports_to_csv_row":0,"parsedemail":0,"parsedmarc":[4,9,10,11],"parsedmarc_":12,"parsedmarc_config_fil":12,"parsedmarc_debug":12,"parsedmarc_elasticsearch_":12,"parsedmarc_elasticsearch_host":12,"parsedmarc_elasticsearch_ssl":12,"parsedmarc_gelf_":12,"parsedmarc_general_":12,"parsedmarc_general_debug":12,"parsedmarc_general_ipinfo_api_token":12,"parsedmarc_general_ipinfo_url":12,"parsedmarc_general_offlin":12,"parsedmarc_general_save_aggreg":12,"parsedmarc_general_save_failur":12,"parsedmarc_gmail_api_":12,"parsedmarc_gmail_api_credentials_file_fil":12,"parsedmarc_imap_":12,"parsedmarc_imap_host":12,"parsedmarc_imap_password":12,"parsedmarc_imap_password_fil":12,"parsedmarc_imap_us":12,"parsedmarc_kafka_":12,"parsedmarc_log_analytics_":12,"parsedmarc_mailbox_":12,"parsedmarc_mailbox_watch":12,"parsedmarc_maildir_":12,"parsedmarc_msgraph_":12,"parsedmarc_opensearch_":12,"parsedmarc_s3_":12,"parsedmarc_smtp_":12,"parsedmarc_splunk_hec_":12,"parsedmarc_splunk_hec_index":12,"parsedmarc_splunk_hec_token":12,"parsedmarc_splunk_hec_url":12,"parsedmarc_syslog_":12,"parsedmarc_webhook_":12,"parser":0,"parserconfig":[0,12],"parsererror":0,"parsingresult":0,"part":[0,3,4,7,8,12],"partial":0,"particular":[7,12],"pass":[0,3,7,10,12],"passsword":12,"password":[0,4,6,12],"paste":[4,11],"patch":6,"path":[0,4,6,12],"pathlik":0,"pattern":[0,5,7,12],"pay":12,"payload":[0,12],"pct":10,"peak":12,"pem":12,"per":[0,4,7,12],"percentag":7,"perform":[2,5],"period":12,"perl":[0,6],"perman":12,"permiss":[4,12],"persist":[0,12],"peter":10,"phase":0,"pick":[6,12],"pickl":0,"pickup":6,"pid":12,"pie":7,"pin":12,"pip":[6,12],"pipelin":0,"pkcs12":12,"place":[0,4,7,12],"plain":[0,12],"plaintext":[3,8],"platform":[3,6,8,12],"pleas":[1,5,12],"plug":12,"plus":[0,7,12],"point":[4,6,12],"pol":4,"polici":[0,3,4,7,8,10,12],"policies_combin":[0,4],"policy_domain":[4,10],"policy_evalu":10,"policy_override_com":10,"policy_override_reason":10,"policy_publish":10,"policy_str":10,"policy_typ":[4,10],"policyscopegroupid":12,"poll":[0,2,12],"popul":0,"port":[0,2,12],"portal":12,"posit":[0,12],"posix":0,"possibl":12,"post":[3,4,8,12],"poster":[3,8],"postgr":12,"postgresql":[5,12],"postorius":[3,8],"powershel":12,"ppa":6,"pre":[0,6,12],"prebuilt":12,"predict":12,"prefer":[2,6,12],"prefix":[0,3,8,12],"premad":[5,11],"prepend":0,"prerequisit":5,"present":12,"preserv":0,"pressur":12,"pretti":12,"prettifi":12,"previous":[0,2,4,6,12],"pri":[2,12],"primari":0,"print":12,"printabl":10,"prior":0,"prioriti":12,"privaci":[3,6,7,8,12],"privat":12,"probe":0,"problem":12,"proc":12,"proceed":4,"process":[0,2,5,6,12],"produc":[0,10],"program":12,"programdata":6,"progress":[4,12],"project":[0,2,3,5,11,12],"prompt":4,"proofpoint":5,"propag":0,"properti":2,"protect":[2,3,5,8,12],"protocol":12,"provid":[0,4,7,12],"provis":12,"prox":6,"proxi":2,"proxyhost":2,"proxypassword":2,"proxyport":2,"proxyus":2,"ps":4,"psl":[0,12],"psl_overrid":0,"psl_overrides_path":0,"psl_overrides_url":[0,12],"psycopg":12,"public":[0,3,10,12],"public_suffix_list":0,"publicbaseurl":4,"publicsuffix":0,"publish":[3,12],"published_polici":0,"pull":12,"purpos":4,"put":[4,12],"py":0,"python":[0,4,6,12],"python3":6,"qo":4,"quarantin":[3,8],"queri":[0,4,12],"query_dn":0,"quick":0,"quickstart":12,"quit":12,"quot":[10,12],"quota":[0,12],"r":[2,10,12],"rais":[0,12],"ram":[4,12],"rate":[0,12],"rather":[0,3,4,7,8,12],"ratio":7,"raw":12,"re":[0,4,6,12],"reach":[0,12],"reachabl":12,"read":[0,12],"readabl":[0,12],"readwrit":12,"real":[0,7],"realli":3,"reason":[0,2,4,5,12],"rebind":0,"rebuilt":0,"receiv":[0,7,10,12],"receiveddatetim":12,"receiving_ip":[4,10],"receiving_mx_hostnam":[4,10],"recent":0,"recipi":7,"recogn":[7,12],"recommend":12,"recommended_dns_nameserv":0,"record":[0,5,6,10,12],"record_typ":0,"recov":[0,12],"recurs":12,"redact":12,"redi":12,"reduc":[6,12],"refactor":0,"refer":[4,5,7,12],"referenc":12,"refresh":[6,12],"refresh_interv":12,"refus":4,"regard":12,"regardless":[0,10,12],"region":[0,12],"region_nam":12,"regist":[6,12],"registr":12,"regul":[4,6,9,12],"regular":[3,8,12],"reject":[0,3,8,12],"relat":[3,12],"relay":[3,8],"releas":[0,4,6],"reli":[6,7],"reliabl":12,"reload":[0,2,4],"remain":[0,7,12],"remot":2,"remov":[0,3,4,8,12],"render":7,"repars":0,"repeat":[0,3,8,12],"replac":[0,3,4,8,12],"repli":[2,3,8],"replic":12,"replica":[0,12],"reply_goes_to_list":[3,8],"reply_to":10,"replyto":[3,8],"repopul":0,"report":[0,4,11],"report_id":10,"report_metadata":10,"report_typ":0,"reported_domain":10,"reports_fold":[0,12],"repositori":[6,11],"req":4,"request":[0,2,4,12],"requir":[0,2,3,4,5,6,7,8,12],"require_encrypt":0,"res":4,"reserv":12,"reset":0,"resid":12,"resolv":[0,12],"resort":6,"resourc":[0,4,5,12],"respect":7,"respons":[0,12],"rest":[0,12],"restart":[2,3,4,6,8],"restartsec":[2,12],"restor":4,"restrict":12,"restrictaccess":12,"restructur":7,"result":[0,5,7,10,12],"result_typ":[4,10],"resum":12,"retain":[3,8,12],"retent":[0,5],"retri":[0,4,12],"retriev":2,"retry_attempt":12,"retry_delay":12,"return":[0,4],"revers":[0,6,7,12],"reverse_dn":[0,10],"reverse_dns_base_domain":0,"reverse_dns_map":0,"reverse_dns_map_path":0,"reverse_dns_map_url":[0,12],"reversednsservic":0,"review":7,"rewrit":[0,3,8],"rfc":[0,3,5,8,10],"rfc2369":[3,8],"rfc822":2,"rhel":[4,5,6],"ri":4,"right":[4,7],"rm":4,"rmh":4,"rocki":6,"rollup":6,"root":[2,12],"rough":12,"row":7,"rpm":4,"rpt":[5,7],"rsa":4,"rt":4,"rua":[5,6],"ruf":[5,6,7,12],"rule":[7,12],"run":[0,4,5,6],"runtimeerror":12,"rw":[2,12],"s":[0,2,3,4,6,7,8,10,12],"s3":[5,12],"safe":[0,4,12],"sampl":[0,5,7,12],"sample_headers_on":10,"satisfi":7,"save":[0,4,6,7],"save_aggreg":12,"save_aggregate_report_to_elasticsearch":0,"save_aggregate_report_to_opensearch":0,"save_aggregate_reports_to_splunk":0,"save_callback":0,"save_failur":12,"save_failure_report_to_elasticsearch":0,"save_failure_report_to_opensearch":0,"save_failure_reports_to_splunk":0,"save_forens":12,"save_forensic_report_to_elasticsearch":0,"save_forensic_report_to_opensearch":0,"save_forensic_reports_to_splunk":0,"save_output":0,"save_smtp_tl":12,"save_smtp_tls_report_to_elasticsearch":0,"save_smtp_tls_report_to_opensearch":0,"save_smtp_tls_reports_to_splunk":0,"say":[0,12],"sbin":6,"sc":4,"scalar":4,"schedul":[6,12],"schema":[5,10,12],"scheme":0,"scope":[4,7,10,12],"script":[4,6],"scrub_nondigest":[3,8],"sdk":12,"search":[0,3,4,8,12],"second":[0,2,12],"secret_access_key":12,"section":4,"secur":[0,4,12],"see":[0,2,3,4,6,7,12],"seek":0,"seen":[0,12],"seen_aggregate_report_id":0,"segment":7,"sel":4,"select":0,"selector":[4,7,10],"self":[4,5],"send":[0,2,3,4,5,7,8,11,12],"sender":[0,5,7,8],"sending_mta_ip":[4,10],"sendmail":[0,12],"sensit":12,"sent":[0,3,8,12],"sentinel":5,"separ":[0,3,4,6,7,9,11,12],"sequenc":0,"sequenti":[0,12],"serial":[0,12],"server":[0,2,3,4,5,6,7,10,12],"server_ip":4,"serverless":[0,12],"servernameon":10,"servic":[0,3,4,5,6,7,8,10],"service_account":12,"service_account_us":12,"session":[0,7],"set":[0,2,3,4,6,7,8,9,12],"set_host":0,"setup":[4,6,9,12],"shape":[0,4],"shard":[0,12],"share":[0,4,6,7,12],"sharealik":6,"sharepoint":10,"shell":6,"ship":[6,12],"short":12,"shot":[0,12],"shouldn":[3,8],"show":[2,7,12],"shown":[6,7,12],"shutdown":[0,12],"sibl":7,"side":[7,12],"sighup":[0,6,12],"sigkil":12,"sign":[0,3,4,6,12],"signal":12,"signatur":[3,7,8],"sigterm":[0,12],"sigv4":[0,12],"silent":[0,6,12],"similar":7,"simpl":5,"simpli":[0,12],"simplifi":0,"sinc":[0,6,7,12],"singl":[0,7,12],"sink":12,"sister":3,"six":12,"size":[2,4],"skel":6,"skip":[0,4,12],"skip_certificate_verif":[0,12],"slight":11,"slow":0,"small":[4,12],"smaller":12,"smi":4,"smtp":[0,3,4,5,12],"smtp_tls":[0,4,12],"smtp_tls_csv_filenam":[0,12],"smtp_tls_index":0,"smtp_tls_json_filenam":[0,12],"smtp_tls_report":0,"smtp_tls_url":12,"smtptlsfailuredetail":0,"smtptlsfailuredetailsopt":0,"smtptlsparsedreport":0,"smtptlspolici":0,"smtptlspolicysummari":0,"smtptlsreport":0,"socket":2,"solut":6,"somehow":12,"someon":4,"sometim":12,"sort":12,"sourc":[0,3,4,6,7,10],"source_as_domain":10,"source_as_nam":10,"source_asn":10,"source_base_domain":10,"source_countri":10,"source_ip_address":10,"source_nam":10,"source_reverse_dn":10,"source_typ":10,"sourceforg":2,"sovereign":12,"sp":[3,4,10],"spam":12,"spawn":0,"special":12,"specif":[0,3,6,7,12],"specifi":[2,3],"spf":[0,5,7,10],"spf_align":10,"spf_domain":10,"spf_result":[4,10],"spf_results_combin":[0,4],"spf_scope":10,"split":0,"splunk":[5,12],"splunk_hec":12,"splunkerror":0,"splunkhec":12,"sponsor":5,"spoof":[3,8],"spurious":12,"sr":4,"ss":0,"ssl":[0,2,4,12],"ssl_cert_path":0,"stabl":4,"stack":[4,7,12],"stale":0,"standard":[0,5,6,10],"start":[0,2,4,7,9,11,12],"starttl":[7,12],"startup":[0,4,6],"state":0,"static":12,"status":[2,12],"stay":[0,7,12],"stdout":12,"step":[3,4,6,8,12],"still":[0,3,4,8,10,12],"stop":12,"storag":[0,4,12],"store":[2,4,7,9,12],"str":[0,12],"straight":[0,12],"stream":12,"string":[0,4,7,12],"strip":[0,3,8,12],"strip_attachment_payload":[0,12],"strong":12,"structur":5,"sts":[7,10,12],"stsv1":10,"style":0,"subdomain":[0,3,12],"subfield":4,"subfold":[0,12],"subject":[0,3,8,10,12],"subject_prefix":[3,8],"submiss":0,"submit":[0,4],"subsidiari":7,"substitut":6,"success":[0,12],"successful_sess":7,"successful_session_count":10,"sudo":[2,4,6,12],"suffici":12,"suffix":0,"suggest":7,"suit":12,"suitabl":0,"sum":7,"summari":[3,8,12],"supervis":12,"suppli":[0,7,12],"support":[2,4,5,7,10,11],"sure":4,"surfac":[7,12],"sw50zxjha3rpdmugv2v0dgjld2vyymvylcocymvyc2ljahq":10,"swallow":12,"switch":7,"syslog":[2,5,12],"system":[2,3,4,6,8,12],"systemctl":[2,4,12],"systemd":5,"systemdr":6,"t":[0,4,5,7,8,10,12],"tab":[3,4,8],"tabl":[4,5,7,12],"tag":6,"take":[0,12],"talk":[0,4],"target":[0,2,12],"task":[0,4,6],"tbi":10,"tcp":12,"tee":4,"tell":[0,3,7,8],"templat":[3,8],"temporari":7,"tenant":5,"tenant_id":12,"term":6,"test":[0,10,12],"text":[0,4,10],"thank":10,"therebi":[3,8],"therefor":12,"thing":12,"third":0,"though":7,"thousand":12,"three":[0,7,12],"throughput":12,"tier":12,"time":[0,2,4,6,7,12],"timeout":[0,2,12],"timeoutstopsec":12,"timer":12,"timespan":0,"timespan_requires_norm":10,"timestamp":[0,12],"timestamp_to_datetim":0,"timestamp_to_human":0,"timezon":10,"tld":3,"tls":[0,4,5,12],"to_domain":10,"to_utc":0,"togeth":[7,12],"token":[0,4,12],"token_fil":12,"tool":12,"top":[3,7,12],"topic":12,"total":7,"touch":[0,3,8],"toward":0,"tracker":1,"trade":12,"tradit":[3,8],"traffic":7,"trail":12,"transfer":10,"transient":[0,12],"transpar":5,"transport":[4,12],"trash":12,"treat":[0,12],"treatment":0,"tri":[0,12],"troubleshoot":12,"true":[0,2,4,10,12],"trust":12,"truststor":4,"truth":7,"tsvb":7,"tuesday":6,"tune":5,"two":[0,6,7,12],"txt":[0,12],"typ":4,"type":[4,5,7,10,12],"typic":12,"typo":12,"u":[2,6,12],"ubuntu":[4,6],"udp":[0,12],"ui":[3,8],"unaffect":12,"unavail":12,"unchang":[0,12],"uncondit":[3,8,12],"underlying":[0,12],"underneath":7,"underscor":12,"understand":[5,7],"unencrypt":12,"unexpir":12,"unfortun":[3,8],"unit":[0,2,12],"unix":0,"unknown":0,"unless":[6,12],"unpars":[0,12],"unpickl":0,"unreach":12,"unread":12,"unrel":6,"unsav":[0,12],"unsubscrib":[3,8],"unsuit":12,"unus":0,"unzip":2,"updat":[0,4,6,12],"update_by_queri":0,"upersecur":12,"upgrad":[2,5,6,12],"upload":12,"upper":7,"uppercas":12,"uri":[6,12],"url":[0,2,4,12],"us":[10,12],"usabl":12,"usag":12,"use":[0,3,4,5,8,10],"use_ssl":0,"user":[0,2,3,4,6,7,8,10,12],"user_ag":10,"useradd":[2,6],"usernam":[0,12],"usernamepassword":12,"usesystemproxi":2,"usr":[4,6],"usual":12,"utc":[0,12],"utf":10,"util":5,"v":12,"v2":0,"valid":[0,7,10,12],"valimail":5,"valu":[0,3,4,7,8,12],"valueerror":0,"var":[3,8,12],"variabl":5,"variant":[0,12],"various":6,"vendor":3,"venv":[6,12],"verbatim":12,"verbos":12,"veri":[0,4,7,12],"verif":[0,4,12],"verifi":0,"verification_mod":4,"version":[0,2,4,5,9,10,11,12],"vew":2,"via":[0,2],"view":[7,12],"vim":4,"virtualenv":6,"visual":[4,7,9],"volum":[7,12],"vulner":3,"w":[0,12],"w3c":10,"wait":[0,12],"wait_for_complet":[0,4],"wall":0,"want":[2,12],"wantedbi":[2,12],"warm":0,"warn":[0,4,12],"watch":[0,2,4,6,12],"watch_inbox":[0,12],"watcher":[0,12],"way":[0,4,7,12],"web":[2,4],"webdav":2,"webhook":[5,12],"webmail":[3,7,8],"week":[0,6,12],"well":[2,7,12],"wettbewerb":10,"wget":4,"whalensolut":12,"whatev":[0,12],"wheel":12,"whenev":[0,2,12],"wherea":7,"wherev":12,"whether":[0,12],"whi":[3,7,12],"whole":[0,7,12],"whose":[0,12],"wide":[6,10,12],"wider":12,"wiki":10,"will":[0,2,3,4,6,7,8,12],"win":12,"window":[6,12],"within":[0,12],"without":[3,4,6,7,8],"won":5,"work":[2,3,4,5,6,7,8,12],"worker":[0,12],"workstat":2,"worst":[3,12],"worth":12,"wrap":[3,8],"wrapper":12,"write":[0,4,12],"written":12,"www":[4,6,12],"x":[4,7,10],"x509":4,"xennn":10,"xml":[0,11,12],"xml_schema":10,"xms4g":4,"xmx4g":4,"xpack":4,"xxxx":4,"y":[4,6],"yahoo":7,"yaml":12,"year":12,"yes":[3,8],"yet":[0,3,4,12],"yml":4,"yyyi":[0,12],"z":12,"zero":12,"zip":[0,2,5,12],"\u00fcbersicht":10},"titles":["API reference","Contributing to parsedmarc","Accessing an inbox using OWA/EWS","Understanding DMARC","Elasticsearch and Kibana","parsedmarc documentation - Open source DMARC report analyzer and visualizer","Installation","Using the Kibana dashboards","What about mailing lists?","OpenSearch and Grafana","Sample outputs","Splunk","Using parsedmarc"],"titleterms":{"Do":[3,8],"What":[3,8],"_file":12,"access":2,"aggreg":[7,10],"align":3,"analyz":[5,6],"api":0,"archiv":12,"backfil":4,"best":[3,8],"bug":1,"cli":12,"combin":4,"compat":5,"compos":12,"config":[0,12],"configur":[2,12],"content":5,"contribut":1,"countri":6,"csv":10,"dashboard":7,"databas":6,"davmail":2,"depend":6,"dkim":[3,4],"dmarc":[3,5,7],"docker":12,"document":5,"domain":3,"elast":0,"elasticsearch":4,"env":12,"environ":12,"ew":2,"exampl":12,"exchang":6,"failur":[7,10],"featur":5,"field":4,"file":12,"geolite2":6,"grafana":9,"guid":3,"help":12,"inbox":2,"index":4,"indic":0,"instal":[4,6,9],"ip":6,"json":10,"kibana":[4,7],"librari":12,"list":[3,8],"listserv":[3,8],"lookalik":3,"mail":[3,8],"mailbox":12,"mailman":[3,8],"map":12,"maxmind":6,"messag":12,"microsoft":6,"mode":12,"multi":12,"multipl":6,"name":12,"onc":12,"onli":12,"open":5,"opensearch":[0,9],"option":6,"output":10,"owa":2,"parsedmarc":[0,1,2,5,6,12],"pattern":4,"perform":12,"practic":[3,8],"prerequisit":6,"proxi":6,"python":5,"record":[3,4,9],"refer":0,"reload":12,"report":[1,5,6,7,10,12],"resourc":3,"restart":12,"result":4,"retent":[4,9],"run":[2,12],"sampl":10,"save":12,"secret":12,"section":12,"sender":3,"servic":[2,12],"smtp":[7,10],"sourc":5,"specifi":12,"spf":[3,4],"splunk":[0,11],"suffix":12,"support":[3,12],"systemd":[2,12],"t":3,"tabl":0,"tenant":12,"test":6,"tls":[7,10],"tune":12,"type":0,"understand":3,"upgrad":4,"use":[2,6,7,12],"util":0,"valid":3,"variabl":12,"via":12,"visual":5,"web":6,"without":12,"won":3,"workaround":[3,8]}})
\ No newline at end of file
diff --git a/splunk.html b/splunk.html
index 0ff33af4..538df72f 100644
--- a/splunk.html
+++ b/splunk.html
@@ -6,14 +6,14 @@
- Splunk — parsedmarc 10.3.0 documentation
+ Splunk — parsedmarc 10.4.0 documentation
-
+
@@ -85,7 +85,7 @@
silent - bool: Set this to False to output results to STDOUT
output - str: Directory to place JSON and CSV files in. This is required if you set either of the JSON output file options.
+
archive_directory - str: Optional. When set, successfully
+processed report files given as local file/directory path
+arguments are moved into
+<archive_directory>/<year>/<month>/<Aggregate|Failure|SMTP-TLS>/
+(year and month come from the report’s own begin/arrival date, with
+the month zero-padded). A successfully parsed report whose archive
+date can’t be determined is left in place with a logged warning.
+Files that fail to parse as a report are moved to
+<archive_directory>/Invalid/; files that fail for other reasons,
+such as transient I/O errors, are left in place so a later run can
+retry them. An existing destination file is never overwritten; a
+numeric suffix is appended before the extension (e.g.
+report-1.xml). This applies only to direct local file input —
+reports fetched from mailboxes (IMAP, Microsoft Graph, Gmail API,
+Maildir) use [mailbox]archive_folder instead, and mbox files are
+never moved. Files already inside archive_directory are excluded
+from processing, so the archive may safely live inside an input
+directory. A failed move is logged and does not stop the run.
aggregate_json_filename - str: filename for the aggregate
JSON output file
failure_json_filename - str: filename for the failure
@@ -266,28 +288,39 @@ DNS resolvers (Default: silent - bool: Only print errors (Default: True)
fail_on_output_error - bool: Exit with a non-zero status code if
any configured output destination fails while saving/publishing
-reports (Default: False)
log_file - str: Write log messages to a file at this path
n_procs - int: Number of processes to run in parallel when
-parsing report files passed directly as CLI arguments
+parsing report files passed directly as CLI arguments, messages
+in mbox files, and messages from mailbox connections (IMAP,
+Microsoft Graph, Gmail API, Maildir), including watch mode
(Default: 1)
Note
Setting this to a number larger than one can improve
-performance when processing thousands of files
+performance when processing thousands of files or messages
Note
-
n_procs only applies to report files passed directly on the
-command line. Messages from mbox files and from mailbox
-connections (IMAP, Microsoft Graph, Gmail API, Maildir) are
-always processed sequentially.
+
Only parsing is parallelized across worker processes. Fetching
+messages, deduplicating reports, archiving/deleting mailbox
+messages, and saving/publishing to outputs all stay sequential
+in the main process. Each worker process keeps its own DNS/GeoIP
+cache.
mailbox
-
+
reports_folder - str: The mailbox folder (or label for
Gmail) where the incoming reports can be found
(Default: INBOX)
@@ -297,12 +330,46 @@ Gmail) to sort processed emails into (Default: delete - bool: Delete messages after processing them,
instead of archiving them
+
delete_aggregate - bool: Delete aggregate report messages
+after processing them, instead of archiving them
+(Default: the value of delete)
+
delete_failure - bool: Delete failure report messages
+after processing them, instead of archiving them
+(Default: the value of delete)
+
delete_smtp_tls - bool: Delete SMTP TLS report messages
+after processing them, instead of archiving them
+(Default: the value of delete)
+
delete_invalid - bool: Delete messages that could not be
+parsed, instead of archiving them in the Invalid
+subfolder, where they can be inspected for debugging
+(Default: the value of delete)
+
+
Note
+
Each of these four options overrides delete for one kind of
+message only, and the other three keep inheriting delete. So
+delete=True combined with delete_failure=False archives
+failure report messages while deleting processed aggregate and
+SMTP TLS report messages — and unparseable ones, unless
+delete_invalid=False is set as well.
+
+
test - bool: Do not move or delete messages
batch_size - int: Number of messages to read and process
before saving. Default 10. Use 0 for no limit.
-
check_timeout - int: Number of seconds to wait for a IMAP
+
check_timeout - int: Number of seconds to wait for an IMAP
IDLE response or the number of seconds until the next
mail check (Default: 30)
+
max_unsaved_retries - int: How many times a batch of messages
+whose reports could not be saved is retried before its messages
+are moved to the Unsaved archive subfolder instead of being
+retried again (Default: 2, i.e. the initial attempt plus two
+retries). Use 0 to move messages on the first failed save;
+negative values are rejected.
+Failures are counted in memory, so the cap applies across watch-mode
+checks within one long-running process, not across separate one-shot
+runs. See
+Mailbox messages are only archived once the reports are saved
+below.
since - str: Search for messages since certain time. (Examples: 5m|3h|2d|1w)
Acceptable units - {“m”:“minutes”, “h”:“hours”, “d”:“days”, “w”:“weeks”}.
Defaults to 1d if incorrect value is provided.
@@ -986,6 +1053,76 @@ Check current usage (from Management -> Dev Tools -> Console):
Increasing this value increases resource usage.
+
+
Mailbox messages are only archived once the reports are saved
+
parsedmarc processes a mailbox in batches of batch_size messages. Each
+batch is written to every configured output destination before any of
+that batch’s messages are archived or deleted. If any destination reports a
+failure — an Elasticsearch outage, an expired Splunk HEC token, an
+unreachable Kafka broker, a full --output disk — the whole batch is left
+in the reports folder and retried on the next run or watch-mode check, so a
+report is never removed from the mailbox while it exists nowhere else
+(issue #242).
+
This is all-or-nothing per batch: archiving a batch because most
+destinations accepted it would still permanently lose the data for the one
+that didn’t. It also applies regardless of fail_on_output_error, which
+only controls the process exit code. Messages that could not be parsed at
+all carry no report data, so they are filed under Invalid (or deleted per
+delete_invalid) as usual.
+
A destination that is broken rather than briefly unavailable would
+otherwise be retried forever, so retries are capped. Once a message’s batch
+has failed max_unsaved_retries+1 times (three times by default), that
+message is moved to <archive_folder>/Unsaved and stops being retried. A
+message is never deleted on this path, whatever the delete options say.
+To recover after fixing the output destination, either move the messages
+from Archive/Unsaved back into the reports folder, or run parsedmarc once
+with reports_folder=Archive/Unsaved.
+
+
Note
+
The failure counts live in memory, so they are counted per parsedmarc
+process. In watch mode — a long-running process that checks the mailbox
+repeatedly — the cap works as described across checks. A one-shot run
+(cron, systemd timers) attempts each message exactly once and then
+exits, so its counts start over every time and the default cap is never
+reached: messages simply keep being retried on every run, which is the
+safe direction. Set max_unsaved_retries=0 if you want one-shot runs to
+move unsavable messages to Unsaved immediately instead.
+
+
+
Warning
+
Retrying a batch means re-sending it. Output destinations that
+deduplicate — Elasticsearch, OpenSearch, and PostgreSQL, which recognize
+an already-saved report — are unaffected, and S3 is idempotent because
+each report is written to an object key built from its type, date, and
+report ID, so a retry overwrites the same object. Kafka,
+Splunk HEC, syslog, GELF, webhooks, Azure Log Analytics, and the
+--output JSON/CSV files all append unconditionally, so each retry adds
+another copy of every report in the batch. That is why the default retry
+cap is deliberately low: at most three deliveries per report before its
+message is set aside in Unsaved. The summary email covers everything
+parsed in a run, including reports whose batch failed to save, so a report
+retried across runs can also appear in more than one summary email.
+
+
+
Note
+
Not every destination can report a failed delivery. The webhook output
+deliberately logs and swallows its own HTTP and network errors, and the
+syslog and GELF outputs send through Python logging handlers, which
+swallow delivery errors by design — so an unreachable webhook, syslog, or
+GELF endpoint is not treated as a failed save and does not hold a
+batch’s messages back. Failures in Elasticsearch, OpenSearch, Splunk HEC,
+Kafka, S3, PostgreSQL, Azure Log Analytics, and the --output files are
+all detected and do.
+
+
+
Note
+
since interacts with retries: a message that ages out of the configured
+since window stops being fetched, and therefore stops being retried
+automatically. It is never deleted or moved — it simply stays in the
+reports folder until it is processed by a run with a wider (or no)
+since window.
parsedmarc is also importable as a regular Python package, not just a CLI
+tool. The main entry points — parse_report_file(), parse_aggregate_report_xml(),
+parse_aggregate_report_file(), parse_failure_report(), parse_report_email(),
+get_dmarc_reports_from_mbox(), get_dmarc_reports_from_mailbox(), and
+watch_inbox() — are all importable
+directly from the parsedmarc package. See the API reference for
+the full set of modules and members.
+
Each of these functions accepts either individual option keyword arguments
+(offline, nameservers, dns_timeout, etc.) or a single config= keyword
+argument carrying a ParserConfig instance:
When config= is passed, the individual option keyword arguments are
+ignored in favor of the values carried on the ParserConfig instance.
+
Each explicitly constructed ParserConfig owns its own isolated caches
+(IP address info, seen aggregate report IDs, and the reverse DNS map).
+Omitting config= falls back to the process-wide caches shared by every
+call that doesn’t pass one.
+
keep_alive and n_procs are not part of ParserConfig — they control
+process/worker orchestration rather than parsing or enrichment behavior,
+so they are always passed as separate keyword arguments.
For large mailbox imports or backfills, parsedmarc can consume a noticeable amount
@@ -1164,8 +1343,12 @@ imports more predictable:
Reduce mailbox.batch_size to smaller values such as 100-500 instead of
processing a very large message set at once. Smaller batches trade throughput
for lower peak memory use and less sink pressure.
-
Keep n_procs low for mailbox-heavy runs. In practice, 1-2 workers is often
-a safer starting point for large backfills than aggressive parallelism.
+
n_procs now parallelizes parsing for mailbox and mbox runs too, not just
+report files passed directly as CLI arguments. It pays off most when a run
+is bound by DNS/GeoIP enrichment rather than fetching or output. The
+trade-off is memory and DNS load: at most roughly 2*n_procs messages
+are held in flight at once, each worker process keeps its own DNS/GeoIP
+cache, and DNS query volume can multiply by up to n_procs.
Use mailbox.since to process reports in smaller time windows such as 1d,
7d, or another interval that fits the backlog. This makes it easier to catch
up incrementally instead of loading an entire mailbox history in one run.