Files
Brancheneinstufung2/fotograf-de-scraper/backend/gmail_service.py
Floke 9b4f80a44f [34588f42] Sec: DEV_MODE_EMAIL_RECIPIENT Implementierung
- E-Mail-Service so konfiguriert, dass alle ausgehenden E-Mails an eine definierte Test-E-Mail-Adresse umgeleitet werden, wenn DEV_MODE_EMAIL_RECIPIENT gesetzt ist.
2026-04-17 20:27:24 +00:00

140 lines
5.0 KiB
Python

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:
# DEV MODE OVERRIDE
dev_email = os.getenv("DEV_MODE_EMAIL_RECIPIENT")
original_to = to
if dev_email:
logger.warning(f"⚠️ DEV MODE ACTIVE: Redirecting email originally intended for {original_to} to {dev_email}")
to = dev_email
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()
if dev_email:
logger.info(f"Test-Email sent to {to} (Original target: {original_to}). Message ID: {send_result['id']}")
else:
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