Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions routes/note/note_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ async def dispatch_reminder(
email_error = ""
if channel == "email":
try:
from routes.email_routes import _get_email_config
from routes.email_routes import _get_email_config, _smtp_ready
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime as _dt
Expand All @@ -311,7 +311,7 @@ async def dispatch_reminder(
# account when no explicit choice is saved.
_acc_id = (settings.get("reminder_email_account_id") or "").strip() or None
cfg = _get_email_config(account_id=_acc_id, owner=owner or "")
if not (cfg.get("smtp_host") and cfg.get("smtp_user") and cfg.get("smtp_password")):
if not _smtp_ready(cfg):
try:
from core.database import SessionLocal as _SL, EmailAccount as _EA
from sqlalchemy import and_, or_
Expand All @@ -324,7 +324,7 @@ async def dispatch_reminder(
q = q.filter(or_(_EA.owner == owner, and_(unowned, same_mailbox)))
for row in q.order_by(_EA.is_default.desc(), _EA.created_at.asc()).all():
trial = _get_email_config(account_id=row.id, owner=owner or "")
if trial.get("smtp_host") and trial.get("smtp_user") and trial.get("smtp_password"):
if _smtp_ready(trial):
cfg = trial
break
finally:
Expand All @@ -347,8 +347,8 @@ async def dispatch_reminder(
missing.append("SMTP host")
if not cfg.get("smtp_user"):
missing.append("SMTP user")
if not cfg.get("smtp_password"):
missing.append("SMTP password")
if not (cfg.get("smtp_password") or cfg.get("oauth_provider")):
missing.append("SMTP credentials")
if not from_addr:
missing.append("from address")
if not recipient:
Expand Down
9 changes: 7 additions & 2 deletions static/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2412,11 +2412,16 @@ async function initReminderSettings() {
// what the Integrations panel manages. Treat the email channel as
// configured if there's at least one account with SMTP set.
let emailAccounts = [];
const smtpAccountReady = (account) => !!(
account.smtp_host
&& account.smtp_user
&& (account.has_smtp_password || account.oauth_provider === 'google')
);
try {
const res = await fetch('/api/email/accounts', { credentials: 'same-origin' });
if (res.ok) {
const d = await res.json();
emailAccounts = (d.accounts || []).filter(a => a.smtp_host && a.smtp_user && a.has_smtp_password);
emailAccounts = (d.accounts || []).filter(smtpAccountReady);
}
} catch (_) {}
let smtpConfigured = emailAccounts.length > 0;
Expand Down Expand Up @@ -2520,7 +2525,7 @@ async function initReminderSettings() {
const res = await fetch('/api/email/accounts', { credentials: 'same-origin' });
if (res.ok) {
const d = await res.json();
emailAccounts = (d.accounts || []).filter(a => a.smtp_host && a.smtp_user && a.has_smtp_password);
emailAccounts = (d.accounts || []).filter(smtpAccountReady);
}
} catch (_) {}
smtpConfigured = emailAccounts.length > 0;
Expand Down
62 changes: 62 additions & 0 deletions tests/test_note_reminder_email_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Regression coverage for passwordless Google OAuth reminder senders."""

import asyncio
from pathlib import Path
from unittest.mock import patch

from routes.note_routes import dispatch_reminder


_REPO = Path(__file__).resolve().parents[1]


def test_dispatch_reminder_sends_with_google_oauth_without_smtp_password():
cfg = {
"account_name": "Workspace",
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"smtp_security": "starttls",
"smtp_user": "alice@example.edu",
"smtp_password": "",
"from_address": "alice@example.edu",
"oauth_provider": "google",
}
sent = []

def fake_send(actual_cfg, sender, recipients, message):
sent.append((actual_cfg, sender, recipients, message))

with (
patch("src.settings.load_settings", return_value={}),
patch("routes.email_routes._get_email_config", return_value=cfg),
patch("routes.email_helpers._send_smtp_message", side_effect=fake_send),
patch("core.database.SessionLocal", side_effect=AssertionError("fallback lookup is not needed")),
):
result = asyncio.run(dispatch_reminder(
"Reminder: Submit report",
"The report is due today.",
note_id="",
owner="alice@example.edu",
queue_browser=False,
settings_override={
"reminder_channel": "email",
"reminder_llm_synthesis": False,
},
))

assert result["email_sent"] is True
assert result["email_error"] == ""
assert len(sent) == 1
actual_cfg, sender, recipients, message = sent[0]
assert actual_cfg is cfg
assert sender == "alice@example.edu"
assert recipients == ["alice@example.edu"]
assert "Subject: Reminder (Odysseus): Submit report" in message


def test_reminder_settings_offer_oauth_smtp_accounts():
source = (_REPO / "static" / "js" / "settings.js").read_text(encoding="utf-8")
helper = source[source.index("const smtpAccountReady"):source.index("const smtpAccountReady") + 260]

assert "account.has_smtp_password || account.oauth_provider === 'google'" in helper
assert source.count(".filter(smtpAccountReady)") == 2