from __future__ import annotations import argparse import json import sys from pathlib import Path from app.mailer.campaign.entries import EntryLoadError from app.mailer.campaign.loader import CampaignLoadError, load_campaign_config from app.mailer.messages.builder import build_campaign_messages from app.mailer.messages.models import CampaignBuildReport def _print_report(report: CampaignBuildReport, *, verbose: bool = False) -> None: print(f"Campaign: {report.campaign_name} ({report.campaign_id})") print(f"Campaign file: {report.campaign_file}") print(f"Entries: {report.entries_count}") print( "Build: " f"built={report.built_count}, " f"failed={report.build_failed_count}, " f"queueable={report.queueable_count}" ) print( "Validation: " f"ready={report.ready_count}, " f"warning={report.warning_count}, " f"needs_review={report.needs_review_count}, " f"blocked={report.blocked_count}, " f"excluded={report.excluded_count}, " f"inactive={report.inactive_count}" ) for message in report.messages: print("---") label = message.entry_id or f"#{message.entry_index}" eml = f", eml={message.eml_path}" if message.eml_path else "" print( f"Entry {label}: " f"build={message.build_status.value}, " f"validation={message.validation_status.value}, " f"send={message.send_status.value}, " f"imap={message.imap_status.value}, " f"attachments={message.attachment_count}{eml}" ) if message.subject: print(f" Subject: {message.subject}") if message.to: print(" To: " + ", ".join(item.email for item in message.to)) for issue in message.issues: behavior = f", behavior={issue.behavior}" if issue.behavior else "" source = f", source={issue.source}" if issue.source else "" print(f" [{issue.severity}] {issue.code}{behavior}{source}: {issue.message}") if verbose: for attachment in message.attachments: print( f" - attachment {attachment.attachment_id or ''}: " f"{attachment.status}, matches={len(attachment.matches)}, " f"zip={attachment.zip_enabled}, filter={attachment.directory}/{attachment.file_filter}" ) for match in attachment.matches: print(f" {match}") def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Build campaign message drafts and review statuses without sending.") parser.add_argument("--campaign", required=True, help="Path to campaign.json") parser.add_argument("--output-dir", default=None, help="Optional directory for generated .eml files") parser.add_argument("--write-eml", action="store_true", help="Write generated messages as .eml files") parser.add_argument("--json", action="store_true", help="Output machine-readable JSON report") parser.add_argument("--verbose", "-v", action="store_true", help="Print attachment details") args = parser.parse_args(argv) campaign_path = Path(args.campaign).resolve() output_dir = Path(args.output_dir).resolve() if args.output_dir else None write_eml = args.write_eml or output_dir is not None try: config = load_campaign_config(campaign_path) result = build_campaign_messages( config, campaign_file=campaign_path, output_dir=output_dir, write_eml=write_eml, ) except (CampaignLoadError, EntryLoadError, ValueError, OSError) as exc: print(f"Error: {exc}", file=sys.stderr) return 2 if args.json: print(json.dumps(result.report.model_dump(mode="json", by_alias=True), ensure_ascii=False, indent=2)) else: _print_report(result.report, verbose=args.verbose) return 0 if result.report.build_failed_count == 0 else 1 if __name__ == "__main__": raise SystemExit(main())