Skip to content

Commit cdc05d2

Browse files
committed
feat: custom passwordless email delivery via shared email service
Implement a custom SuperTokens EmailDeliveryOverrideInput for the passwordless recipe that delegates to the shared email_service instead of using SuperTokens' built-in SMTP transporter. This ensures SMTP_USE_TLS is honoured identically for OTP and application emails (invitations, password resets, etc.). Previously, the SuperTokens SMTPSettings only exposed a flag (SSL), and when secure=false it still silently attempted STARTTLS — diverging from the shared email service which only calls starttls() when SMTP_USE_TLS=true. - Add PasswordlessEmailService class implementing the SuperTokens EmailDeliveryInterface for passwordless OTP emails - Add get_passwordless_email_delivery() helper that returns EmailDeliveryConfig when an email provider is configured - Wire email_delivery into passwordless.init() call - OTP emails now go through _send_via_smtp / _send_via_sendgrid with the same TLS/SSL/auth logic as all other emails - Falls back gracefully (logs + skips) when no provider is configured Fixes #4559
1 parent 77f3ec1 commit cdc05d2

1 file changed

Lines changed: 104 additions & 11 deletions

File tree

api/oss/src/core/auth/supertokens/config.py

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
1-
from typing import Dict, List, Any
1+
from typing import Any, Dict, List
22
from urllib.parse import urlparse
33

4-
from supertokens_python import init, InputAppInfo, SupertokensConfig
4+
from supertokens_python import InputAppInfo, SupertokensConfig, init
5+
from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig
56
from supertokens_python.recipe import (
67
emailpassword,
78
passwordless,
89
session,
910
thirdparty,
1011
)
12+
from supertokens_python.recipe.emailpassword.types import InputFormField
1113
from supertokens_python.recipe.emailpassword.utils import (
12-
InputSignUpFeature,
1314
InputOverrideConfig as EmailPasswordInputOverrideConfig,
14-
)
15-
from supertokens_python.recipe.emailpassword.types import (
16-
InputFormField,
15+
InputSignUpFeature,
1716
)
1817
from supertokens_python.recipe.passwordless import (
1918
ContactEmailOnlyConfig,
2019
InputOverrideConfig as PasswordlessInputOverrideConfig,
2120
)
22-
from supertokens_python.recipe.thirdparty import (
23-
ProviderInput,
24-
ProviderConfig,
25-
ProviderClientConfig,
26-
InputOverrideConfig as ThirdPartyInputOverrideConfig,
21+
from supertokens_python.recipe.passwordless.types import (
22+
PasswordlessLoginEmailTemplateVars,
2723
)
2824
from supertokens_python.recipe.session import (
2925
InputOverrideConfig as SessionInputOverrideConfig,
3026
)
27+
from supertokens_python.recipe.thirdparty import (
28+
InputOverrideConfig as ThirdPartyInputOverrideConfig,
29+
ProviderClientConfig,
30+
ProviderConfig,
31+
ProviderInput,
32+
)
3133

3234
from oss.src.utils.env import env
3335
from oss.src.utils.common import is_ee
@@ -50,6 +52,96 @@
5052
log = get_module_logger(__name__)
5153

5254

55+
# ---------------------------------------------------------------------------
56+
# Passwordless OTP email delivery — delegates to the shared email service so
57+
# that SMTP_USE_TLS / SendGrid behaviour is identical for OTP and app emails.
58+
# ---------------------------------------------------------------------------
59+
60+
61+
class PasswordlessEmailService:
62+
"""Custom SuperTokens email delivery for passwordless OTP emails.
63+
64+
Implements ``EmailDeliveryInterface[PasswordlessLoginEmailTemplateVars]``
65+
by delegating to the shared ``email_service`` so TLS/SSL settings are
66+
honoured consistently.
67+
"""
68+
69+
async def send_email(
70+
self,
71+
template_vars: PasswordlessLoginEmailTemplateVars,
72+
user_context: Dict[str, Any],
73+
) -> None:
74+
from oss.src.services import email_service
75+
76+
otp = template_vars.user_input_code or ""
77+
code_lifetime = template_vars.code_life_time
78+
minutes = max(1, code_lifetime // 60)
79+
80+
html_content = (
81+
f"<p>Hello,</p>"
82+
f"<p>Your one-time login code is:</p>"
83+
f"<h2 style=\"font-family:monospace;letter-spacing:0.15em\">{otp}</h2>"
84+
f"<p>This code expires in {minutes} minute(s).</p>"
85+
f"<p>If you did not request this code, you can safely ignore this email.</p>"
86+
f"<p>Thank you for using Agenta!</p>"
87+
)
88+
89+
from_address = env.smtp.from_address or env.sendgrid.from_address
90+
if not from_address:
91+
log.warning(
92+
"Passwordless OTP email skipped — no sender address configured "
93+
"(set SMTP_FROM_ADDRESS or SENDGRID_FROM_ADDRESS)"
94+
)
95+
return
96+
97+
if not email_service._USE_SMTP and not email_service._USE_SENDGRID:
98+
log.info(
99+
"Passwordless OTP email delivery disabled — no email provider configured"
100+
)
101+
return
102+
103+
try:
104+
if email_service._USE_SMTP:
105+
email_service._send_via_smtp(
106+
to_email=template_vars.email,
107+
subject="Your Agenta login code",
108+
html_content=html_content,
109+
from_email=from_address,
110+
)
111+
else:
112+
email_service._send_via_sendgrid(
113+
to_email=template_vars.email,
114+
subject="Your Agenta login code",
115+
html_content=html_content,
116+
from_email=from_address,
117+
)
118+
log.info("Passwordless OTP email sent to %s", template_vars.email)
119+
except Exception:
120+
log.error(
121+
"Failed to send passwordless OTP email to %s",
122+
template_vars.email,
123+
exc_info=True,
124+
)
125+
126+
127+
def get_passwordless_email_delivery() -> EmailDeliveryConfig | None:
128+
"""Return email delivery config for the passwordless recipe.
129+
130+
Routes OTP emails through the shared ``email_service`` so that
131+
``SMTP_USE_TLS`` and the SendGrid fallback behave identically to
132+
application emails (invitations, password resets, etc.).
133+
134+
Returns ``None`` when no email provider is configured — SuperTokens
135+
will fall back to its default behaviour.
136+
"""
137+
from oss.src.services import email_service
138+
139+
if not email_service._USE_SMTP and not email_service._USE_SENDGRID:
140+
return None
141+
142+
return EmailDeliveryConfig(service=PasswordlessEmailService())
143+
144+
53145
def get_supertokens_config() -> Dict[str, Any]:
54146
"""Get SuperTokens configuration from environment."""
55147
return {
@@ -398,6 +490,7 @@ def init_supertokens():
398490
passwordless.init(
399491
flow_type="USER_INPUT_CODE",
400492
contact_config=ContactEmailOnlyConfig(),
493+
email_delivery=get_passwordless_email_delivery(),
401494
override=PasswordlessInputOverrideConfig(
402495
apis=override_passwordless_apis,
403496
functions=override_passwordless_functions,

0 commit comments

Comments
 (0)