106:def validate_rows(records: List[dict], suppression: set, bounced: set,
107-                  existing_emails: Optional[set] = None, max_recipients: int = 2000) -> ImportReport:
108-    """Validate parsed rows against all rules. Returns an ImportReport."""
109-    rpt = ImportReport()
110-    seen: set = set()
111-    for rec in records:
112-        rpt.rows_seen += 1
113-        email = (rec.get("email") or rec.get("customer_email") or "").strip().lower()
114-        row = rec.get("_row")
115-        base = {k: v for k, v in rec.items() if k not in ("_row", "custom_fields")}
116-        if not email:
117-            rpt.invalid.append({**base, "reason": "Missing email address", "row": row})
118-            continue
119-        if not is_valid_email(email):
120-            rpt.invalid.append({**base, "email": email, "reason": "Invalid email syntax", "row": row})
121-            continue
122-        if email in seen or (existing_emails and email in existing_emails):
123-            rpt.duplicates.append({**base, "email": email, "reason": "Duplicate address in import", "row": row})
124-            continue
125-        if email in suppression:
126-            rpt.suppressed.append({**base, "email": email, "reason": "On suppression list", "row": row})
127-            continue
128-        if email in bounced:
129-            rpt.bounced.append({**base, "email": email, "reason": "Previously bounced", "row": row})
130-            continue
131-        seen.add(email)
132-        rpt.valid.append({
133-            "email": email,
134-            "first_name": (rec.get("first_name") or "").strip(),
135-            "last_name": (rec.get("last_name") or "").strip(),
136-            "company_name": (rec.get("company_name") or "").strip(),
137-            "company_phone": (rec.get("company_phone") or "").strip(),
138-            "customer_email": (rec.get("customer_email") or rec.get("email") or "").strip(),
139-            "custom_fields": rec.get("custom_fields", {}),
140-        })
141-    if len(rpt.valid) > max_recipients:
142-        rpt.errors.append(f"Import exceeds maximum of {max_recipients} recipients; only first {max_recipients} kept.")
143-        rpt.valid = rpt.valid[:max_recipients]
144-    return rpt
145-
146-
async def import_commit(list_id: int = Form(...), file: UploadFile = File(...),
                        user: User = Depends(get_current_user_csrf),
                        db: AsyncSession = Depends(get_db), request: Request = None):
    """Commit an import into the given list (same validation as preview)."""
    rl = await db.get(RecipientList, list_id)
    if not rl or rl.user_id != user.id:
        raise HTTPException(404, "Recipient list not found")
    content = await file.read()
    if len(content) > settings.MAX_UPLOAD_MB * 1024 * 1024:
        raise HTTPException(413, "File too large")
    fname = (file.filename or "").lower()
    if fname.endswith(".json"):
        raise HTTPException(400, "JSON recipient files are not accepted. Use CSV or TXT.")
    if not (fname.endswith(".csv") or fname.endswith(".txt")):
        raise HTTPException(400, "Only .csv and .txt files are accepted")

    if fname.endswith(".txt"):
        lines = recipient_service.parse_txt(content.decode("utf-8", errors="replace"))
        records = []
        for ln in lines:
            parts = [p.strip() for p in ln.split(",")]
            rec = {"email": parts[0]}
            if len(parts) > 1:
                rec["first_name"] = parts[1]
            if len(parts) > 2:
                rec["last_name"] = parts[2]
            records.append(rec)
    else:
        records, _, _ = recipient_service.parse_csv(content.decode("utf-8", errors="replace"))

    sup, bounced = await _suppression_and_bounces(db)
    existing = set((await db.execute(select(Recipient.email).where(
        Recipient.list_id == list_id))).scalars().all())
    rpt = recipient_service.validate_rows(records, sup, bounced, existing,
                                          settings.MAX_RECIPIENTS_PER_CAMPAIGN)
    for v in rpt.valid:
        db.add(Recipient(list_id=list_id, email=v["email"], first_name=v["first_name"],
                         last_name=v["last_name"], company_name=v["company_name"],
                         company_phone=v["company_phone"],
                         customer_email=v["customer_email"] or v["email"],
                         custom_fields=v.get("custom_fields", {}),
                         unsub_token=recipient_service.make_unsub_token()))
    rl.row_count += len(rpt.valid)
    rl.source = "txt" if fname.endswith(".txt") else "csv"
    await db.commit()
    await log_audit(db, user.id, "recipients.import", "recipient_list", list_id,
                    rpt.summary(), request.client.host if request else None)
    return {"ok": True, **rpt.summary(), "list_id": list_id}

