/workspace/bulkmailer/app/services/gmail_service.py
/workspace/bulkmailer/app/tasks/send_tasks.py
/workspace/bulkmailer/app/models/__init__.py
/workspace/bulkmailer/app/api/routers/accounts.py
/workspace/bulkmailer/app/api/routers/test_send.py
---
/workspace/bulkmailer/app/services/gmail_service.py:177:                 client_config: dict, sending_method: str = "api", token_expiry=None):
/workspace/bulkmailer/app/services/gmail_service.py:180:        self.method = sending_method if sending_method in ("api", "smtp") else "api"
/workspace/bulkmailer/app/services/gmail_service.py:257:        sending_method=account.sending_method,
/workspace/bulkmailer/app/tasks/send_tasks.py:277:            method=account.sending_method,
/workspace/bulkmailer/app/models/__init__.py:71:    sending_method = Column(String(20), default="api")  # api|smtp
/workspace/bulkmailer/app/api/routers/campaigns.py:690:                      "daily_limit": a.daily_limit, "sending_method": a.sending_method}
/workspace/bulkmailer/app/api/routers/auth.py:186:        account.sending_method = account.sending_method or "api"
/workspace/bulkmailer/app/api/routers/auth.py:204:            status="authorized", sending_method="api",
/workspace/bulkmailer/app/api/routers/dashboard.py:40:                       "sending_method": a.sending_method}
/workspace/bulkmailer/app/api/routers/accounts.py:19:    sending_method: str = "api"     # api | smtp
/workspace/bulkmailer/app/api/routers/accounts.py:31:        "sending_method": account.sending_method,
/workspace/bulkmailer/app/api/routers/accounts.py:236:    if payload.sending_method in ("api", "smtp"):
/workspace/bulkmailer/app/api/routers/accounts.py:237:        account.sending_method = payload.sending_method
/workspace/bulkmailer/app/api/routers/test_send.py:170:    return account.sending_method or "api"
/workspace/bulkmailer/app/api/routers/test_send.py:182:                          "sending_method": a.sending_method} for a in rows]}
---gmail_service.py head---
"""Real Google OAuth 2.0 + Gmail API integration.

- Authorization code flow with state verification (CSRF protection on the OAuth handshake).
- Tokens stored encrypted (Fernet) via app.core.encryption.
- Refresh handled transparently; never logs secrets.
- Sending via Gmail API (users.messages.send) or Gmail SMTP with XOAUTH2 (no passwords ever).
"""
import base64
import json
import logging
import secrets
from datetime import datetime, timedelta, timezone
from email.mime.image import MIMEImage
from email.utils import formatdate
from urllib.parse import urlencode

import google.auth.transport.requests
import googleapiclient.discovery
import googleapiclient.errors
import requests
from google.oauth2 import credentials as gcred
from google_auth_oauthlib.flow import Flow
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseUpload

from app.core import encryption
from app.core.config import settings

log = logging.getLogger("gmail")

# Minimum scope required to send mail through the user's own account.
GMAIL_SCOPES = [
    "https://www.googleapis.com/auth/gmail.send",
    "https://www.googleapis.com/auth/gmail.compose",
]
USERINFO_SCOPE = "openid https://www.googleapis.com/auth/userinfo.email"
ALL_SCOPES = GMAIL_SCOPES + [USERINFO_SCOPE]

GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
GOOGLE_REVOKE_URL = "https://oauth2.googleapis.com/revoke"
GOOGLE_USERINFO_URL = "https://openidconnect.googleapis.com/v1/userinfo"


class GmailOauthError(Exception):
    pass


# ---------------------------------------------------------------------------
# OAuth client configuration resolution
# ---------------------------------------------------------------------------
def resolve_client_config(config=None) -> dict:
    """Return {client_id, client_secret, redirect_uri} for OAuth flows.

    Priority: explicit config dict (per-user imported config) > DB row > env file path > error.
    """
    if config and config.client_id:
        return {
            "client_id": config.client_id,
            "client_secret": config.client_secret,  # decrypted property
