from __future__ import annotations import argparse import json from time import sleep from app.db.bootstrap import create_all_tables from app.db.models import CampaignJob, JobQueueStatus, JobSendStatus from app.db.session import SessionLocal from app.mailer.sending.jobs import append_sent_for_job, send_campaign_job from app.security.api_keys import authenticate_api_key from app.settings import settings def main() -> None: parser = argparse.ArgumentParser(description="Process queued campaign jobs directly, without a Celery worker.") parser.add_argument("--campaign-id", required=True) parser.add_argument("--api-key", default=settings.dev_bootstrap_api_key) parser.add_argument("--limit", type=int, default=0, help="Maximum jobs to process; 0 means all queued jobs") parser.add_argument("--dry-run", action="store_true", help="Validate/send path without SMTP delivery or status mutation to SENT") parser.add_argument("--no-rate-limit", action="store_true") parser.add_argument("--append-sent", action="store_true", help="After successful SMTP delivery, immediately run IMAP append-to-Sent in this CLI process") parser.add_argument("--json", action="store_true") args = parser.parse_args() create_all_tables() results = [] with SessionLocal() as session: api_key = authenticate_api_key(session, args.api_key) if not api_key: raise SystemExit("Invalid API key") query = ( session.query(CampaignJob) .filter( CampaignJob.tenant_id == api_key.tenant_id, CampaignJob.campaign_id == args.campaign_id, CampaignJob.queue_status == JobQueueStatus.QUEUED.value, CampaignJob.send_status.in_([JobSendStatus.QUEUED.value, JobSendStatus.FAILED_TEMPORARY.value]), ) .order_by(CampaignJob.entry_index.asc()) ) if args.limit > 0: query = query.limit(args.limit) jobs = query.all() for job in jobs: try: result = send_campaign_job(session, job_id=job.id, dry_run=args.dry_run, use_rate_limit=not args.no_rate_limit) result_dict = result.as_dict() if args.append_sent and result.status == "sent": append_result = append_sent_for_job(session, job_id=job.id, dry_run=args.dry_run) result_dict["imap_append"] = append_result.as_dict() results.append(result_dict) if not args.json: line = f"{job.entry_index}: {result.status} ({job.recipient_email})" if "imap_append" in result_dict: line += f"; IMAP: {result_dict['imap_append']['status']}" print(line) except Exception as exc: results.append({"job_id": job.id, "status": "error", "error": str(exc)}) if not args.json: print(f"{job.entry_index}: ERROR {exc} ({job.recipient_email})") # Continue with the next job; individual attempts/statuses are recorded. sleep(0.1) if args.json: print(json.dumps({"processed": len(results), "results": results}, indent=2)) elif not jobs: print("No queued jobs found") if __name__ == "__main__": main()