if data.template_sequence_mode not in VALID_ENUMS["template_sequence_mode"]: errors.append("template_sequence_mode must be repeat|stop|random") if data.phone_selection not in VALID_ENUMS["phone_selection"]: errors.append("phone_selection must be round_robin|random|fixed") if data.recipient_list_id: rl = await db.get(RecipientList, data.recipient_list_id) if not rl or rl.user_id != user.id: errors.append("Recipient list not found") else: rc = (await db.execute(select(func.count()).select_from(Recipient) .where(Recipient.list_id == rl.id, Recipient.is_valid == True))).scalar() or 0 # noqa: E712 if rc == 0: errors.append("Recipient list is empty") if rc > cfg.MAX_RECIPIENTS_PER_CAMPAIGN: errors.append(f"Recipient list has {rc} entries; max {cfg.MAX_RECIPIENTS_PER_CAMPAIGN}") for sid in data.subject_set_ids or []: s = await db.get(SubjectLine, sid) if not s or s.user_id != user.id: errors.append(f"Subject line {sid} not found") if not (data.subject_set_ids or []): errors.append("Select at least one subject line") for tid in data.template_ids or []: t = await db.get(EmailTemplate, tid) if not t or t.user_id != user.id: errors.append(f"Template {tid} not found") if not (data.template_ids or []): errors.append("Select at least one HTML template") if not data.account_ids: errors.append("Select at least one Gmail account") elif len(data.account_ids) > cfg.MAX_GMAIL_ACCOUNTS_PER_CAMPAIGN: errors.append(f"Max {cfg.MAX_GMAIL_ACCOUNTS_PER_CAMPAIGN} accounts per campaign") else: for aid in data.account_ids: acc = await db.get(GmailAccount, aid) if not acc or acc.user_id != user.id: errors.append(f"Gmail account {aid} not found") elif acc.status != "authorized" or not acc.is_active: errors.append(f"Account {acc.email} is not authorized/active") if data.attach_mode == "pdf": if not (data.attach_pdf or data.attach_pdf_template_id or data.attachment_id): errors.append("PDF attachment mode requires a template or attachment to convert") if data.attach_mode in ("file", "image") and not data.attachment_id: errors.append("File/image attachment mode requires selecting an attachment") if len(data.rotation_weights or {}) > 0 and data.rotation_strategy != "weighted":