Skip to content

Commit da849d5

Browse files
committed
refactor: harmonization pass 2 — MailReport, injectable console, grade exit, ruff clean
- Rename FullReport → MailReport; FullReport kept as deprecated alias - assessor: add logger, keyword-only assess(), --timeout/-T flag - reporter: _console/console pattern, highlight=False, print_full_report(report, *, console=None) - print_verdict(report, *, console=None) — extracts actions/grade internally - Grade D/F → exit 1 in check command - verdict: add LOW and INFO to VerdictSeverity - Add constants.py (SMTP_DEFAULT_PORT, DNS_TIMEOUT, SMTP_TIMEOUT, HTTP_TIMEOUT) - Add pytest-mock to dev deps - ruff: fix E402 (logger placement), F401 (unused imports)
1 parent 52437d1 commit da849d5

13 files changed

Lines changed: 287 additions & 137 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ MTA-STS, TLSRPT, DNSSEC, SMTP diagnostics, and DNS blacklists for a given domain
2424
mailvalidator/
2525
├── cli.py → Typer CLI entry point; all sub-commands defined here
2626
├── assessor.py → assess() orchestrates the full check pipeline
27-
├── models.py → All dataclasses (CheckResult, Status, *Result, FullReport)
27+
├── models.py → All dataclasses (CheckResult, Status, *Result, MailReport; FullReport is a deprecated alias)
2828
├── dns_utils.py → Shared DNS helpers
2929
├── reporter.py → Rich rendering for each result type + save_report()
3030
├── verdict.py → Security verdict extraction: severity mapping, action deduplication

mailvalidator/assessor.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from __future__ import annotations
1111

12+
import logging
1213
import socket
1314
from typing import Callable
1415

@@ -22,7 +23,9 @@
2223
from mailvalidator.checks.smtp import check_smtp
2324
from mailvalidator.checks.spf import check_spf
2425
from mailvalidator.checks.tlsrpt import check_tlsrpt
25-
from mailvalidator.models import FullReport, MXRecord, SMTPDiagResult
26+
from mailvalidator.models import MailReport, MXRecord, SMTPDiagResult
27+
28+
logger = logging.getLogger("mailvalidator")
2629

2730

2831
def _resolve_mx_ips(records: list[MXRecord]) -> list[str]:
@@ -48,8 +51,9 @@ def assess(
4851
run_smtp: bool = True,
4952
run_dnssec: bool = True,
5053
progress_cb: Callable[[str], None] | None = None,
51-
) -> FullReport:
52-
"""Run all mail server checks for *domain* and return a :class:`~mailvalidator.models.FullReport`.
54+
timeout: float = 5.0,
55+
) -> MailReport:
56+
"""Run all mail server checks for *domain* and return a :class:`~mailvalidator.models.MailReport`.
5357
5458
:param domain: The target domain name to assess (e.g. ``"example.com"``).
5559
:param smtp_port: TCP port used for SMTP diagnostics. Defaults to ``25``.
@@ -63,16 +67,17 @@ def assess(
6367
:param progress_cb: Optional callable invoked with a short status string
6468
before each check group. Useful for driving a progress spinner in
6569
the CLI.
66-
:returns: Populated :class:`~mailvalidator.models.FullReport`; individual
70+
:param timeout: DNS/network timeout in seconds. Defaults to ``5.0``.
71+
:returns: Populated :class:`~mailvalidator.models.MailReport`; individual
6772
fields are ``None`` when the corresponding check was skipped.
68-
:rtype: ~mailvalidator.models.FullReport
73+
:rtype: ~mailvalidator.models.MailReport
6974
"""
7075

7176
def _cb(msg: str) -> None:
7277
if progress_cb:
7378
progress_cb(msg)
7479

75-
report = FullReport(domain=domain)
80+
report = MailReport(domain=domain)
7681

7782
_cb("Checking MX records…")
7883
report.mx = check_mx(domain)

mailvalidator/cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
from mailvalidator import __version__
3535
from mailvalidator.assessor import assess
36+
from mailvalidator.verdict import calculate_grade, extract_verdict_actions
3637
from mailvalidator.checks.bimi import check_bimi
3738
from mailvalidator.checks.blacklist import check_blacklist
3839
from mailvalidator.checks.dkim import check_dkim
@@ -218,6 +219,10 @@ def cmd_check(
218219
bool,
219220
typer.Option("--json", help="Output results as JSON."),
220221
] = False,
222+
timeout: Annotated[
223+
float,
224+
typer.Option("--timeout", "-T", help="DNS/network timeout in seconds."),
225+
] = 5.0,
221226
) -> None:
222227
"""Run all mail server checks for DOMAIN and print a full report."""
223228
with Progress(
@@ -237,6 +242,7 @@ def _progress_cb(msg: str) -> None:
237242
run_blacklist=not no_blacklist,
238243
run_dnssec=not no_dnssec,
239244
progress_cb=_progress_cb,
245+
timeout=timeout,
240246
)
241247

242248
if json_output:
@@ -245,6 +251,11 @@ def _progress_cb(msg: str) -> None:
245251

246252
print_full_report(report)
247253

254+
actions = extract_verdict_actions(report)
255+
grade = calculate_grade(actions)
256+
if grade.letter in ("D", "F"):
257+
raise typer.Exit(code=1)
258+
248259
if output:
249260
try:
250261
save_report(output)

mailvalidator/constants.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""mailvalidator runtime constants."""
2+
3+
SMTP_DEFAULT_PORT: int = 25
4+
DNS_TIMEOUT: float = 5.0
5+
SMTP_TIMEOUT: float = 10.0
6+
HTTP_TIMEOUT: float = 10.0

mailvalidator/models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ class DNSSECResult:
310310

311311

312312
@dataclass
313-
class FullReport:
313+
class MailReport:
314314
"""Aggregated result of all checks run by :func:`mailvalidator.assessor.assess`.
315315
316316
Individual fields are ``None`` or empty list when the corresponding check
@@ -342,3 +342,6 @@ class FullReport:
342342
blacklist: BlacklistResult | None = None
343343
dnssec_domain: DNSSECResult | None = None
344344
dnssec_mx: DNSSECResult | None = None
345+
346+
347+
FullReport = MailReport # deprecated alias – use MailReport

0 commit comments

Comments
 (0)