import os import json import logging import datetime from typing import Optional, List, Dict, Any from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import Flow from googleapiclient.discovery import build from google.auth.transport.requests import Request from sqlalchemy.orm import Session from database import GmailToken import base64 from email.mime.text import MIMEText logger = logging.getLogger("gmail-service") # Scopes required for sending emails SCOPES = ['https://www.googleapis.com/auth/gmail.send'] class GmailService: def __init__(self, db: Session): self.db = db self.client_id = os.getenv("google_fotograf_client_id") self.client_secret = os.getenv("google_fotograf_secret") # Redirect URI - must match what was configured in Google Console # We try to detect the public URL, fallback to duckdns self.redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", "https://floke-ai.duckdns.org/fotograf-de-api/api/auth/callback") def _get_client_config(self) -> Dict[str, Any]: return { "web": { "client_id": self.client_id, "project_id": "fotograf-tool", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": self.client_secret, "redirect_uris": [self.redirect_uri] } } def get_auth_url(self) -> str: flow = Flow.from_client_config( self._get_client_config(), scopes=SCOPES, redirect_uri=self.redirect_uri ) auth_url, _ = flow.authorization_url(prompt='consent', access_type='offline') return auth_url def handle_callback(self, code: str): flow = Flow.from_client_config( self._get_client_config(), scopes=SCOPES, redirect_uri=self.redirect_uri ) flow.fetch_token(code=code) credentials = flow.credentials self._save_token(credentials) return credentials def _save_token(self, credentials): token_data = { 'token': credentials.token, 'refresh_token': credentials.refresh_token, 'token_uri': credentials.token_uri, 'client_id': credentials.client_id, 'client_secret': credentials.client_secret, 'scopes': credentials.scopes } db_token = self.db.query(GmailToken).first() if not db_token: db_token = GmailToken(token_json=json.dumps(token_data)) self.db.add(db_token) else: db_token.token_json = json.dumps(token_data) self.db.commit() logger.info("Gmail OAuth token saved to database.") def get_credentials(self) -> Optional[Credentials]: db_token = self.db.query(GmailToken).first() if not db_token: return None token_data = json.loads(db_token.token_json) creds = Credentials.from_authorized_user_info(token_data, SCOPES) if creds and creds.expired and creds.refresh_token: logger.info("Gmail token expired, refreshing...") creds.refresh(Request()) self._save_token(creds) return creds def is_authenticated(self) -> bool: try: creds = self.get_credentials() return creds is not None and creds.valid except Exception as e: logger.error(f"Auth check failed: {e}") return False def send_email(self, to: str, subject: str, body_html: str) -> bool: creds = self.get_credentials() if not creds: logger.error("Cannot send email: Not authenticated.") return False try: service = build('gmail', 'v1', credentials=creds) message = MIMEText(body_html, 'html') message['to'] = to message['subject'] = subject raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode() send_result = service.users().messages().send( userId='me', body={'raw': raw_message} ).execute() logger.info(f"Email sent to {to}. Message ID: {send_result['id']}") return True except Exception as e: logger.error(f"Failed to send email to {to}: {e}") return False