141 lines
5.1 KiB
Python
141 lines
5.1 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
|
|
message['bcc'] = 'kontakt@kinderfotos-erding.de'
|
|
|
|
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
|