68:def send_one(self, campaign_id: int, campaign_recipient_id: int): 69- """Process one recipient: build message, send through Gmail, record real outcome.""" 70- with SyncSession() as db: 71- campaign = db.get(Campaign, campaign_id) 72- if campaign is None: 73- return {"ok": False, "status": "cancelled", "reason": "campaign deleted"} 74- if campaign.status in ("paused", "cancelled", "completed"): 75- return {"ok": False, "status": "cancelled", "reason": f"campaign {campaign.status}"} 76- 77- cr = db.get(CampaignRecipient, campaign_recipient_id) 78- if cr is None: 79- return {"ok": False, "status": "cancelled", "reason": "campaign_recipient deleted"} 80- if cr.status in ("sent", "suppressed", "perm_failed"): 81- return {"ok": True, "status": cr.status, "reason": "already finalized"} 82- 83- job = (db.execute(select(SendJob) 84- .where(SendJob.campaign_recipient_id == cr.id, 85- SendJob.campaign_id == campaign.id))).scalar_one_or_none() 86- 87- # Re-check suppression at send time (recipient may have unsubscribed since enqueue) 88- sup = (db.execute(select(SuppressionEntry) 89- .where(SuppressionEntry.email == cr.email))).scalar_one_or_none() 90- if sup: 91- cr.status = "suppressed" 92- campaign.suppressed_count += 1 93- cr.last_error = "Suppressed at send time" 94- db.add(EmailEvent(campaign_id=campaign.id, campaign_recipient_id=cr.id, 95- event_type="suppressed", detail="Suppressed at send time")) 96- db.commit() 97- _publish_progress(campaign.id) 98- return {"ok": True, "status": "suppressed", "reason": "suppression hit"} 99- 100- account = db.get(GmailAccount, cr.assigned_account_id) if cr.assigned_account_id else None 101- if account is None or not account.is_active or account.status != "authorized": 102- cr.status = "skipped" 103- campaign.skipped_count += 1 104- cr.last_error = "Assigned account unavailable/unauthorized" 105- db.commit() 106- _publish_progress(campaign.id) 107- return {"ok": False, "status": "skipped", "reason": "account unavailable"} 108-