        "size_bytes": att.size_bytes,
        "file_sha256": att.file_sha256,
        "description": att.description,
        "is_html_attachment": bool(att.is_html_attachment),
        "created_at": att.created_at.isoformat() if att.created_at else None,
    }


async def _get_own(db: AsyncSession, user: User, att_id: int) -> Attachment:
=== logs list ===
async def list_logs(campaign_id: int = Query(0),
                    outcome: str = Query("", description="submitted|accepted|temp_failed|perm_failed|skipped"),
                    search: str = Query("", description="email or message id fragment"),
                    page: int = Query(1, ge=1),
                    page_size: int = Query(50, ge=1, le=200),
                    db: AsyncSession = Depends(get_db),
                    user: User = Depends(get_current_user)):
    """Unified send log: SendAttempt rows joined with CampaignRecipient for context."""
    q = (select(SendAttempt, CampaignRecipient)
         .join(CampaignRecipient, SendAttempt.campaign_recipient_id == CampaignRecipient.id)
         .join(Campaign, Campaign.id == SendAttempt.campaign_id)
         .where(Campaign.user_id == user.id))
    if campaign_id:
        q = q.where(SendAttempt.campaign_id == campaign_id)
    if outcome:
        q = q.where(SendAttempt.outcome == outcome)
    if search:
        like = f"%{search}%"
        q = q.where(or_(CampaignRecipient.email.ilike(like),
                        SendAttempt.message_id.ilike(like)))
    total = (await db.execute(
        select(func.count()).select_from(q.subquery()))).scalar() or 0
    rows = (await db.execute(
        q.order_by(SendAttempt.id.desc()).offset((page - 1) * page_size).limit(page_size)
    )).all()
    out = []
    for a, cr in rows:
        d = _ser_attempt(a)
        d["email"] = cr.email
        d["recipient_status"] = cr.status
        out.append(d)
=== settings keys ===
32:    "amount_min": {"type": "float", "min": 0, "max": 100000, "default": cfg.AMOUNT_MIN,
34:    "amount_max": {"type": "float", "min": 0, "max": 100000, "default": cfg.AMOUNT_MAX,
36:    "shipping_min": {"type": "float", "min": 0, "max": 10000, "default": cfg.SHIPPING_MIN,
38:    "shipping_max": {"type": "float", "min": 0, "max": 10000, "default": cfg.SHIPPING_MAX,
40:    "invoice_format": {"type": "str", "max_len": 80, "default": cfg.INVOICE_NUMBER_FORMAT,
43:    "invoice_prefix": {"type": "str", "max_len": 12, "default": "INV",
45:    "default_daily_limit": {"type": "int", "min": 1, "max": 2000, "default": 500,
47:    "default_campaign_limit": {"type": "int", "min": 1, "max": 2000, "default": 200,
49:    "default_max_per_account": {"type": "int", "min": 1, "max": 2000, "default": 200,
51:    "default_send_interval": {"type": "float", "min": 0.5, "max": 3600, "default": 1.0,
53:    "default_rate_per_minute": {"type": "int", "min": 1, "max": 600, "default": 60,
55:    "attachment_kind": {"type": "str", "max_len": 20, "default": "invoice",
58:    "unsubscribe_enabled": {"type": "bool", "default": True,
60:    "unsubscribe_base_url": {"type": "str", "max_len": 300, "default": "",
63:    "company_name": {"type": "str", "max_len": 200, "default": "",
65:    "test_email_default": {"type": "str", "max_len": 320, "default": "",
