Add domain management, reports, settings, and upload templates

- Implemented domain management page with a list of monitored domains and their DMARC, SPF, and DKIM statuses.
- Created reports page with filtering options for domain, report type, and date range, displaying DMARC reports.
- Developed settings page for IMAP configuration and DMARC policy management, including form validation and feedback.
- Added upload page for DMARC report files with drag-and-drop functionality and file type validation.
- Integrated Alpine.js for interactivity and dynamic data handling across all templates.
This commit is contained in:
Christian Krakau-Louis
2025-04-20 18:38:29 +02:00
parent f910cb0ba4
commit 69f8438a36
48 changed files with 3449 additions and 1029 deletions
+86 -3
View File
@@ -1,5 +1,6 @@
from typing import Dict, List, Any
from typing import Dict, List, Any, Optional
import threading
from datetime import datetime, timedelta
class ReportStore:
"""
@@ -29,6 +30,8 @@ class ReportStore:
self.domain_reports: Dict[str, List[Dict[str, Any]]] = {}
# Domain -> summary stats
self.domain_summary: Dict[str, Dict[str, Any]] = {}
# Domain -> sources (sending IPs)
self.domain_sources: Dict[str, Dict[str, Dict[str, Any]]] = {}
def add_report(self, report: Dict[str, Any]) -> None:
"""
@@ -48,6 +51,7 @@ class ReportStore:
"failed_count": 0,
"reports_processed": 0,
}
self.domain_sources[domain] = {}
# Add the new report
self.domain_reports[domain].append(report)
@@ -59,6 +63,28 @@ class ReportStore:
self.domain_summary[domain]["failed_count"] += summary.get("failed_count", 0)
self.domain_summary[domain]["reports_processed"] += 1
# Set policy from the latest report
if "policy" in report:
self.domain_summary[domain]["policy"] = report["policy"]
# Update source data
report_records = report.get("records", [])
for record in report_records:
source_ip = record.get("source_ip", "unknown")
if source_ip not in self.domain_sources[domain]:
self.domain_sources[domain][source_ip] = {
"count": 0,
"spf_result": "unknown",
"dkim_result": "unknown",
"disposition": "none"
}
# Update source counts and results
self.domain_sources[domain][source_ip]["count"] += record.get("count", 0)
self.domain_sources[domain][source_ip]["spf_result"] = record.get("spf", "unknown")
self.domain_sources[domain][source_ip]["dkim_result"] = record.get("dkim", "unknown")
self.domain_sources[domain][source_ip]["disposition"] = record.get("disposition", "none")
# Calculate compliance rate (percentage of passing emails)
if self.domain_summary[domain]["total_count"] > 0:
pass_rate = (
@@ -96,14 +122,71 @@ class ReportStore:
"""
return self.domain_summary
def get_domain_reports(self, domain: str) -> List[Dict[str, Any]]:
def get_domain_reports(self, domain: str, limit: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Get all reports for a domain
Args:
domain: Domain name
limit: Optional limit on number of reports to return
Returns:
List of reports or empty list if domain not found
"""
return self.domain_reports.get(domain, [])
reports = self.domain_reports.get(domain, [])
# Sort reports by date (most recent first)
sorted_reports = sorted(
reports,
key=lambda r: r.get("end_date", 0),
reverse=True
)
# Calculate pass rate for each report
for report in sorted_reports:
total = report.get("summary", {}).get("total_count", 0)
passed = report.get("summary", {}).get("passed_count", 0)
if total > 0:
report["pass_rate"] = round((passed / total) * 100, 1)
else:
report["pass_rate"] = 0
# Apply limit if provided
if limit is not None:
return sorted_reports[:limit]
return sorted_reports
def get_domain_sources(self, domain: str, days: int = 30) -> List[Dict[str, Any]]:
"""
Get sending sources for a domain
Args:
domain: Domain name
days: Number of days to look back
Returns:
List of source entries or empty list if domain not found
"""
if domain not in self.domain_sources:
return []
# For Milestone 1, we don't filter by date
# In a future milestone, we'll add date-based filtering
sources = []
for ip, data in self.domain_sources[domain].items():
source_entry = {
"source_ip": ip,
**data
}
sources.append(source_entry)
# Sort sources by count (highest first)
return sorted(sources, key=lambda s: s["count"], reverse=True)
def clear(self) -> None:
"""
Clear all data in the store
"""
self.domain_reports = {}
self.domain_summary = {}
self.domain_sources = {}