feat: wire Gmail SMTP notifications with app password
Replaced the Gmail API (OAuth2) notification delivery with plain SMTP using a Gmail app password. Much simpler setup — no Google Cloud project, no OAuth2 flow, no extra dependencies. - Rewrote _send_gmail() to use smtplib with smtp.gmail.com:587 TLS - Added stonks-gmail-secrets to Helm chart (GMAIL_SENDER, GMAIL_RECIPIENT, GMAIL_APP_PASSWORD) - Added gmail secret to trading-engine deployment - Updated runmefirst.sh to read gmail.app from kube dir - Sender/recipient: celes@celestium.life
This commit is contained in:
@@ -1,20 +1,23 @@
|
||||
"""Notification dispatch for the autonomous trading engine.
|
||||
|
||||
Handles actual delivery of notifications via AWS SNS (SMS) and Gmail API
|
||||
Handles actual delivery of notifications via AWS SNS (SMS) and Gmail SMTP
|
||||
(email), with rate limiting via Redis, retry with exponential backoff,
|
||||
and persistence to the notifications table.
|
||||
|
||||
Task 31: Wire notification dispatch.
|
||||
|
||||
boto3 and google-api-python-client are optional dependencies — the module
|
||||
logs a warning and degrades gracefully if they are not installed.
|
||||
Gmail uses SMTP with an app password (smtp.gmail.com:587 STARTTLS).
|
||||
boto3 is an optional dependency for SNS — the module degrades gracefully
|
||||
if not installed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import smtplib
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from services.shared.config import TradingConfig
|
||||
from services.shared.redis_keys import trading_notification_rate_key
|
||||
@@ -30,19 +33,6 @@ except ImportError:
|
||||
_HAS_BOTO3 = False
|
||||
logger.info("boto3 not installed — SNS notifications disabled")
|
||||
|
||||
# Conditionally import Google API client
|
||||
try:
|
||||
import base64
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build as google_build
|
||||
|
||||
_HAS_GOOGLE = True
|
||||
except ImportError:
|
||||
_HAS_GOOGLE = False
|
||||
logger.info("google-api-python-client not installed — Gmail notifications disabled")
|
||||
|
||||
|
||||
class NotificationDispatcher:
|
||||
"""Routes notification delivery to enabled channels.
|
||||
@@ -178,46 +168,36 @@ class NotificationDispatcher:
|
||||
)
|
||||
|
||||
async def _send_gmail(self, event_type: str, message: str) -> None:
|
||||
"""Send email via Gmail API."""
|
||||
if not _HAS_GOOGLE:
|
||||
raise RuntimeError(
|
||||
"google-api-python-client not installed — cannot send Gmail notification"
|
||||
)
|
||||
|
||||
"""Send email via Gmail SMTP with app password (TLS on port 587)."""
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(None, self._send_gmail_sync, event_type, message)
|
||||
|
||||
def _send_gmail_sync(self, event_type: str, message: str) -> None:
|
||||
"""Synchronous Gmail send (runs in executor)."""
|
||||
"""Synchronous Gmail SMTP send (runs in executor)."""
|
||||
import os
|
||||
|
||||
refresh_token = os.getenv("GMAIL_REFRESH_TOKEN", "")
|
||||
client_id = os.getenv("GMAIL_CLIENT_ID", "")
|
||||
client_secret = os.getenv("GMAIL_CLIENT_SECRET", "")
|
||||
sender = self.config.gmail_sender or os.getenv("GMAIL_SENDER", "")
|
||||
recipient = self.config.gmail_recipient or os.getenv("GMAIL_RECIPIENT", "")
|
||||
app_password = os.getenv("GMAIL_APP_PASSWORD", "")
|
||||
|
||||
if not all([refresh_token, client_id, client_secret]):
|
||||
raise RuntimeError("Gmail OAuth2 credentials not configured")
|
||||
|
||||
creds = Credentials(
|
||||
token=None,
|
||||
refresh_token=refresh_token,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
)
|
||||
|
||||
service = google_build("gmail", "v1", credentials=creds)
|
||||
if not all([sender, recipient, app_password]):
|
||||
raise RuntimeError(
|
||||
"Gmail SMTP not configured — need GMAIL_SENDER, GMAIL_RECIPIENT, "
|
||||
"and GMAIL_APP_PASSWORD env vars"
|
||||
)
|
||||
|
||||
subject = f"[Stonks Alert] {event_type.replace('_', ' ').title()}"
|
||||
mime_msg = MIMEText(message)
|
||||
mime_msg["to"] = self.config.gmail_recipient
|
||||
mime_msg["from"] = self.config.gmail_sender or "me"
|
||||
mime_msg["subject"] = subject
|
||||
mime_msg["To"] = recipient
|
||||
mime_msg["From"] = sender
|
||||
mime_msg["Subject"] = subject
|
||||
|
||||
raw = base64.urlsafe_b64encode(mime_msg.as_bytes()).decode()
|
||||
service.users().messages().send(
|
||||
userId="me", body={"raw": raw}
|
||||
).execute()
|
||||
with smtplib.SMTP("smtp.gmail.com", 587) as server:
|
||||
server.starttls()
|
||||
server.login(sender, app_password)
|
||||
server.send_message(mime_msg)
|
||||
|
||||
logger.info("Email sent: %s → %s (%s)", sender, recipient, event_type)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Rate limiting via Redis
|
||||
|
||||
Reference in New Issue
Block a user