Skip to content

feat(email): add SMTP support for self-hosted platform emails#3696

Open
xid-ryan wants to merge 10 commits intosimstudioai:mainfrom
xid-ryan:feature/smtp-app-email-support
Open

feat(email): add SMTP support for self-hosted platform emails#3696
xid-ryan wants to merge 10 commits intosimstudioai:mainfrom
xid-ryan:feature/smtp-app-email-support

Conversation

@xid-ryan
Copy link

Summary

  • add SMTP as an app-level email backend for platform emails while preserving the existing Resend and Azure fallback flow
  • expose SMTP runtime settings through the app env surface, Helm values, and app secret wiring for self-hosted deployments
  • document provider precedence and add targeted tests for SMTP fallback, SMTP-only config, and invalid SMTP settings

Verification

  • bun run --cwd apps/sim test -- lib/messaging/email/mailer.test.ts
  • bun run --cwd apps/sim type-check
  • helm template sim ./helm/sim --set-string app.env.NEXT_PUBLIC_APP_URL=http://example.com --set-string app.env.BETTER_AUTH_URL=http://example.com --set-string app.env.BETTER_AUTH_SECRET=0123456789abcdef0123456789abcdef --set-string app.env.ENCRYPTION_KEY=0123456789abcdef0123456789abcdef --set-string app.env.INTERNAL_API_SECRET=0123456789abcdef0123456789abcdef --set-string realtime.env.BETTER_AUTH_SECRET=0123456789abcdef0123456789abcdef --set-string postgresql.auth.password=Validpass123 --set-string app.env.SMTP_HOST=smtp.example.com --set-string app.env.SMTP_PORT=587 --set-string app.env.SMTP_SECURE=TLS --set-string app.env.SMTP_USERNAME=smtp-user --set-string app.env.SMTP_PASSWORD=smtp-password

wickedev and others added 10 commits February 9, 2026 05:57
- ANTHROPIC_BASE_URL: allows custom Anthropic API proxy endpoint
- USE_SERVER_KEYS / NEXT_PUBLIC_USE_SERVER_KEYS: enables server-configured
  rotating API keys for self-hosted deployments, removing the need for
  users to enter API keys in the UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SMTP support to the shared mailer so self-hosted deployments can send auth and notification emails without relying on Resend or Azure.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Expose SMTP configuration through the app env surface so self-hosted operators can wire a custom mail server without patching code.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Document the new SMTP settings and provider precedence so operators know how platform email delivery behaves in self-hosted environments.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add SMTP-related values to the Helm chart so deployments can configure custom mail servers through standard release configuration.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Route SMTP credentials through app secrets so Helm deployments can consume them without leaking usernames or passwords into inline environment values.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@vercel
Copy link

vercel bot commented Mar 21, 2026

@wickedev is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@cursor
Copy link

cursor bot commented Mar 21, 2026

PR Summary

Medium Risk
Touches outbound email delivery and provider precedence, so misconfiguration could break notifications or weaken TLS expectations; changes are largely additive and guarded by config validation and tests.

Overview
Adds SMTP as an additional platform email backend (via nodemailer) with validated SMTP_* env config, provider ordering (Resend -> Azure Communication Services -> SMTP), and warnings when multiple providers are configured.

Extends self-hosted surfaces to support SMTP credentials end-to-end (docs, .env.example, env schema, Helm values/schema, secrets/external-secret wiring) and adds targeted tests for SMTP-only mode, fallback behavior, and invalid SMTP settings.

Also introduces a self-hosted USE_SERVER_KEYS/NEXT_PUBLIC_USE_SERVER_KEYS feature flag to hide/skip BYOK API-key requirements for hosted Anthropic models, plus an optional ANTHROPIC_BASE_URL override for the Anthropic SDK, and adds a GitHub Actions workflow to build/push the app image to ECR.

Written by Cursor Bugbot for commit 965ae39. This will update automatically on new commits. Configure here.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 21, 2026

Greptile Summary

This PR adds SMTP as a third platform email backend alongside the existing Resend and Azure Communication Services providers, and introduces server-managed API keys (isServerKeysEnabled) for self-hosted Anthropic/Claude usage. The email layer is cleanly refactored into an EmailProvider[] abstraction with ordered fallback, and the Helm chart is updated to wire SMTP credentials as Kubernetes secrets.

Key findings:

  • Spurious startup warning (helm/sim/values.yaml + mailer.ts): SMTP_PORT defaults to "587" and SMTP_SECURE to "TLS" in values.yaml. Because skipValidation: true is set on createEnv, empty SMTP_HOST="" stays as a falsy string, not undefined. The early-return guard in getSmtpConfig() uses && across all four fields — so a non-empty portValue of "587" prevents the silent skip, and every pod that leaves SMTP_HOST blank will log a WARN about "host or port is missing" on every startup. The fix is to gate the silent return solely on host being absent.
  • sendBatchEmails bypasses emailProviders: The function still guards the Resend batch path with if (resend) directly rather than querying emailProviders. Functionally correct today, but creates a maintenance inconsistency with the new abstraction.
  • AWS account ID hardcoded in .github/workflows/build-ecr.yml: ECR_REGISTRY contains a literal AWS account ID that should come from a GitHub Actions secret.
  • Global test mock (packages/testing/src/mocks/env.mock.ts): Adding full SMTP defaults to defaultMockEnv means any test suite that uses createEnvMock() without overrides will now boot the mailer with Resend + SMTP (two providers), triggering the multiple-provider warning at module load time and potentially affecting unrelated tests.

Confidence Score: 3/5

  • Safe to merge after fixing the spurious SMTP startup warning; other findings are style/polish issues.
  • The SMTP fallback logic and Helm secret wiring are sound, and targeted tests cover the new paths. However, the default SMTP_PORT value in values.yaml will produce a misleading WARN log on every production pod start for the common case of not using SMTP — this is a visible operational issue that should be fixed before shipping. The other findings (batch inconsistency, hardcoded account ID, global mock widening) are lower severity.
  • helm/sim/values.yaml and apps/sim/lib/messaging/email/mailer.ts (getSmtpConfig early-return logic); packages/testing/src/mocks/env.mock.ts (global mock widening)

Important Files Changed

Filename Overview
apps/sim/lib/messaging/email/mailer.ts Core change: adds SMTP as a third email provider using a clean EmailProvider[] abstraction with proper fallback; sendBatchEmails still directly references the resend variable instead of emailProviders (style inconsistency).
apps/sim/lib/messaging/email/mailer.test.ts Good test coverage for SMTP fallback, SMTP-only config, invalid ports, and provider precedence; uses vi.resetModules + vi.doMock correctly for dynamic env overrides.
helm/sim/values.yaml SMTP fields correctly added; however default values for SMTP_PORT ("587") and SMTP_SECURE ("TLS") will always be injected into the pod, causing a spurious startup warning in deployments that do not configure SMTP_HOST.
helm/sim/templates/deployment-app.yaml SMTP_USERNAME and SMTP_PASSWORD correctly excluded from the plain-text env range and routed through the Kubernetes secret; kindIs "invalid" guard avoids rendering nil values.
packages/testing/src/mocks/env.mock.ts Adding full SMTP credentials to the global default mock means all tests that use createEnvMock() now boot the mailer with two providers (Resend + SMTP), potentially triggering unexpected multiple-provider warnings in unrelated test suites.
.github/workflows/build-ecr.yml New ECR build workflow looks correct, but the AWS account ID is hardcoded in ECR_REGISTRY rather than referenced from a GitHub secret.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[sendEmail called] --> B{emailProviders empty?}
    B -- Yes --> C[Return success, log-only mode]
    B -- No --> D[Try Resend]
    D -- Success --> E[Return via Resend]
    D -- Fail --> F[Try Azure ACS]
    F -- Success --> G[Return via Azure ACS]
    F -- Fail --> H[Try SMTP]
    H -- Success --> I[Return via SMTP]
    H -- Fail --> J[Return failure, all providers failed]

    subgraph smtpInit[SMTP Initialization]
        K[Read SMTP env vars] --> L{SMTP_HOST set?}
        L -- No --> M[Return null, no SMTP]
        L -- Yes --> N{SMTP_PORT valid?}
        N -- No --> O[Warn and return null]
        N -- Yes --> P[Build SmtpConfig]
        P --> Q[createSmtpTransporter via nodemailer]
    end

    Q --> B
Loading

Comments Outside Diff (1)

  1. apps/sim/lib/messaging/email/mailer.ts, line 476-483 (link)

    P2 sendBatchEmails bypasses the new emailProviders abstraction

    The sendBatchEmails function still reaches directly into the module-level resend variable rather than going through the emailProviders array introduced by this PR. For example, the Resend batch path is gated on if (resend) rather than checking whether the first provider is Resend. While the current behaviour is functionally correct (the fallback to individual sendEmail() calls does use emailProviders), it creates an inconsistency: adding a new provider that supports batch sending would require edits in two places, and future maintainers may not realise the coupling.

    Consider using emailProviders here as well, or at minimum add a comment explaining why this function intentionally bypasses the abstraction.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Last reviewed commit: "fix(helm): keep SMTP..."

Comment on lines +16 to +17
steps:
- uses: actions/checkout@v4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 AWS account ID and ECR registry URL hardcoded

The ECR registry URL 310455165573.dkr.ecr.us-west-2.amazonaws.com exposes the AWS account ID directly in the repository's source history. It is safer to store this in a GitHub Actions secret (e.g., ${{ secrets.ECR_REGISTRY }}) so the account ID is not committed to version control.

Suggested change
steps:
- uses: actions/checkout@v4
ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }}

Comment on lines +19 to +23
SMTP_HOST: 'smtp.test.sim.ai',
SMTP_PORT: '587',
SMTP_SECURE: 'TLS',
SMTP_USERNAME: 'smtp-user',
SMTP_PASSWORD: 'smtp-password',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Adding SMTP to the global default mock may break existing email tests

The defaultMockEnv object is used by every test across the monorepo that calls createEnvMock(). The existing default already includes RESEND_API_KEY, so adding a fully-configured SMTP block means the mailer module will now boot with two providers (Resend + SMTP) in any test that uses this default mock. This triggers warnOnMultipleProviders() at module-load time, which could:

  1. Cause tests that assert on logger output to see an unexpected warn call about "Multiple email providers configured".
  2. Change the observable emailProviders count from 1 to 2, silently altering test behaviour for any test that relies on hasEmailService() or the fallback path.

Consider keeping the global default to a single minimal provider (e.g., just RESEND_API_KEY), and adding full SMTP credentials only in test suites that specifically exercise SMTP behaviour.

Comment on lines +106 to +107
SMTP_PORT: "587" # SMTP port (587 for STARTTLS, 465 for SSL)
SMTP_SECURE: "TLS" # SMTP security mode: TLS, SSL, or None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Spurious warning logged for every deployment without SMTP

The default values SMTP_PORT: "587" and SMTP_SECURE: "TLS" are always rendered into the pod's environment via the range loop in deployment-app.yaml. When SMTP_HOST is left blank (the default), getSmtpConfig() in mailer.ts receives an empty host but a non-empty portValue of "587". This means the silent early-return guard:

if (!host && !portValue && !username && !password) {
  return null  // silent skip
}

…is not triggered, because portValue is truthy. Execution then falls through to the next guard which emits logger.warn('SMTP configuration ignored because host or port is missing') — this warning fires on every pod start for deployments that don't configure SMTP at all.

Note: skipValidation: true is set in env.ts, so empty-string values are NOT coerced to undefined by Zod; SMTP_HOST="" remains a falsy empty string.

Suggested fix (in getSmtpConfig()): check for a missing host first and return null silently, since the presence of a host is the minimal signal that the operator intended to configure SMTP:

// If host is absent, SMTP is simply not configured — return silently.
if (!host) {
  return null
}
if (!portValue) {
  logger.warn('SMTP configuration ignored because port is missing')
  return null
}

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

EMAIL_DOMAIN: "" # Domain for sending emails (fallback when FROM_EMAIL_ADDRESS not set)
SMTP_HOST: "" # SMTP hostname for app-level outgoing email (alternative to Resend/Azure)
SMTP_PORT: "587" # SMTP port (587 for STARTTLS, 465 for SSL)
SMTP_SECURE: "TLS" # SMTP security mode: TLS, SSL, or None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Helm defaults cause spurious SMTP warning on startup

Medium Severity

The default Helm values set SMTP_PORT: "587" and SMTP_SECURE: "TLS" while leaving SMTP_HOST: "". In getSmtpConfig(), the early-return check !host && !portValue && !username && !password evaluates to false because portValue is truthy ("587"). This causes the function to fall through to the next check, which logs a warning: "SMTP configuration ignored because host or port is missing". Every default Helm deployment that hasn't configured SMTP will see this spurious warning on every startup.

Additional Locations (1)
Fix in Cursor Fix in Web

env:
AWS_REGION: us-west-2
ECR_REGISTRY: 310455165573.dkr.ecr.us-west-2.amazonaws.com
ECR_REPOSITORY: sim-app
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ECR workflow exposes AWS account ID in public repo

Low Severity

The new build-ecr.yml workflow hardcodes the AWS account ID (310455165573) and ECR repository details directly in the file. This is unrelated to the SMTP feature described in the PR and exposes internal infrastructure details in a public repository. It also runs on every push to main, building and pushing a :latest tag without a commit SHA, which means image provenance is lost.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants