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:
Celes Renata
2026-04-16 02:37:40 +00:00
parent 9aae57f3e1
commit c4666c071b
4 changed files with 90 additions and 61 deletions
+25 -45
View File
@@ -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