Skip to content

feat: added smtp support to email#342

Open
arianpotter wants to merge 5 commits intoOpenpanel-dev:mainfrom
arianpotter:feature/send-email-via-smtp
Open

feat: added smtp support to email#342
arianpotter wants to merge 5 commits intoOpenpanel-dev:mainfrom
arianpotter:feature/send-email-via-smtp

Conversation

@arianpotter
Copy link
Copy Markdown

@arianpotter arianpotter commented Apr 18, 2026

Add SMTP support as alternative to Resend (issue #320)

Overview

Email sending now supports two transports: SMTP (via nodemailer) and Resend. If SMTP_HOST is set, SMTP is used. Otherwise it falls back to Resend. If neither is configured, the email payload is logged to console (existing dev behavior).


How it works

SMTP_HOST set? → render template to HTML → send via nodemailer
↓ no
RESEND_API_KEY set? → send via Resend (unchanged behavior)
↓ no
Log to console (dev fallback)


Implementation details

packages/email/src/index.tsx

  • Added @react-email/render to convert React Email JSX templates to plain HTML strings — required for SMTP since nodemailer doesn't understand JSX
  • Added nodemailer for SMTP transport
  • createSmtpTransport() reads SMTP_HOST/PORT/SECURE/USER/PASS from env
  • SMTP check runs before Resend, so it takes priority when both are configured
  • Unsubscribe headers (List-Unsubscribe) are preserved for both transports

New dependencies

  • nodemailer + @types/nodemailer — SMTP client
  • @react-email/render — HTML renderer for React Email templates

Configuration

Variable Required Default Description
SMTP_HOST Yes (to enable SMTP) SMTP server hostname
SMTP_PORT No 587 SMTP port
SMTP_SECURE No false "true" for TLS (port 465)
SMTP_USER No SMTP auth username
SMTP_PASS No SMTP auth password
EMAIL_SENDER No hello@openpanel.dev From address (shared with Resend)

Both .env.example and self-hosting/.env.template have been updated with the new variables and comments explaining the two options.

Summary by CodeRabbit

  • New Features

    • Added SMTP email delivery as an alternative to Resend.
    • Implemented unsubscribe support via RFC 2369-compliant headers.
  • Configuration

    • New environment options to configure Resend or SMTP (host, port, security, credentials, sender).
    • Sending now requires either SMTP or Resend credentials and prefers SMTP when configured.
  • Documentation

    • Updated self-hosting docs and env guidance to explain both Resend and SMTP options.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 18, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds SMTP as an alternative email delivery backend alongside Resend: updates env templates/examples and docs, adds server-side React Email rendering and Nodemailer deps, and implements conditional routing in sendEmail with shared subject/headers and provider-specific error logs.

Changes

Email delivery (single cohesive change DAG)

Layer / File(s) Summary
Env templates
.env.example, self-hosting/.env.template
Add commented env vars for email delivery options: Resend (RESEND_API_KEY, EMAIL_SENDER) and SMTP (SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS).
Docs (usage & reference)
apps/public/content/docs/self-hosting/self-hosting.mdx, apps/public/content/docs/self-hosting/environment-variables.mdx
Document Option A (Resend) and Option B (SMTP), clarify that SMTP_HOST enables SMTP and takes precedence over RESEND_API_KEY, update quick reference and examples, and note console logging fallback when neither provider is configured.
Dependencies
packages/email/package.json
Add @react-email/render, nodemailer, and @types/nodemailer to support server-side template rendering and SMTP sending.
Core implementation
packages/email/src/index.tsx
sendEmail now precomputes subject and unsubscribe headers, updates missing-credentials guard to require SMTP_HOST or RESEND_API_KEY, and uses provider-specific error messages.
SMTP wiring / helper
packages/email/src/index.tsx
Add non-exported createSmtpTransport() helper; when SMTP_HOST is set, render the React email template to HTML with @react-email/render and send via Nodemailer transport.sendMail({ from, to, subject, html, headers }).
Error/logging behavior
packages/email/src/index.tsx
On failures, log provider-specific messages (“Failed to send email via SMTP” / “Failed to send email via Resend”); return provider result or null on error.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Email as sendEmail
    participant Renderer as `@react-email/render`
    participant SMTP as Nodemailer
    participant Resend as Resend API

    App->>Email: sendEmail(template, to, data)
    Email->>Email: compute subject & unsubscribe headers

    alt SMTP_HOST configured
        Email->>Renderer: render template -> HTML
        Renderer-->>Email: HTML
        Email->>SMTP: transport.sendMail({from,to,subject,html,headers})
        SMTP-->>Email: send result
    else RESEND_API_KEY configured
        Email->>Resend: resend.emails.send({... , subject, headers})
        Resend-->>Email: send result
    else No provider
        Email-->>App: log to console, return null
    end

    Email-->>App: return send result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐇 I hopped through configs, keys in my paw,

SMTP or Resend — both ready to draw.
Templates rendered, headers snug and neat,
Off go the messages — swift little feet. ✨📧

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding SMTP support to the email module alongside existing Resend integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/email/src/index.tsx (1)

76-85: Add a plain-text body for SMTP emails.

The SMTP path currently sends only HTML. @react-email/render exports toPlainText to derive plain text from rendered HTML, which improves compatibility for clients that prefer or require text bodies.

✉️ Proposed plain-text fallback
-import { render } from '@react-email/render';
+import { render, toPlainText } from '@react-email/render';
       const html = await render(
         <template.Component {...(props.data as any)} />,
       );
+      const text = toPlainText(html);
       const transport = createSmtpTransport();
       const res = await transport.sendMail({
         from: FROM,
         to,
         subject,
         html,
+        text,
         headers,
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/email/src/index.tsx` around lines 76 - 85, The SMTP send currently
only sets html; derive a plain-text body and include it in the sendMail call by
importing and using toPlainText from `@react-email/render` to convert the rendered
html into text, then pass that text as the text property to transport.sendMail
(locate where render(...) produces html for template.Component and where
createSmtpTransport() and transport.sendMail({...}) are called and add the text
field).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/email/src/index.tsx`:
- Around line 18-27: The createSmtpTransport function can mis-handle an empty
SMTP_PORT (Number('') === 0) and lacks explicit timeouts; update
createSmtpTransport (where createTransport is called) to use a safe port parse
like: if process.env.SMTP_PORT is set use Number(process.env.SMTP_PORT)
otherwise default to 587, and add nodemailer timeout options (connectionTimeout,
greetingTimeout, socketTimeout) with sensible values to avoid long stalls; also
keep the existing auth logic and secure parsing as-is.

---

Nitpick comments:
In `@packages/email/src/index.tsx`:
- Around line 76-85: The SMTP send currently only sets html; derive a plain-text
body and include it in the sendMail call by importing and using toPlainText from
`@react-email/render` to convert the rendered html into text, then pass that text
as the text property to transport.sendMail (locate where render(...) produces
html for template.Component and where createSmtpTransport() and
transport.sendMail({...}) are called and add the text field).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d780cc6-00cd-4d05-bb13-2c9ceaba19aa

📥 Commits

Reviewing files that changed from the base of the PR and between 0e677ba and 460c642.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • .env.example
  • packages/email/package.json
  • packages/email/src/index.tsx
  • self-hosting/.env.template

Comment thread packages/email/src/index.tsx
EMAIL_SENDER="$EMAIL_SENDER"
# Option A - Resend
RESEND_API_KEY="$RESEND_API_KEY"
# Option B - SMTP (takes priority over Resend if SMTP_HOST is set)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

does not make sense to add these here if we dont support it in out setup (self-hosting/quiz.ts)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sine I added the description to self-hosting.mdx (8229720)
does it make more sense? or shall we add it to the setup as well?

@lindesvard
Copy link
Copy Markdown
Contributor

Thanks for the contribution, first of all, have you tested this locally?

Also, could you add a section to our docs? First we could add the envs here https://github.com/Openpanel-dev/openpanel/blob/main/apps/public/content/docs/self-hosting/environment-variables.mdx and then we could add a little section here as well https://github.com/Openpanel-dev/openpanel/blob/main/apps/public/content/docs/self-hosting/self-hosting.mdx#e-mail

@ondrej-langr
Copy link
Copy Markdown

why not merge the values SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER and SMTP_PASS to one variable? it would be string containing the URL and let it parse with standard URL where all necessary properties are present (https://developer.mozilla.org/en-US/docs/Web/API/URL)?

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/public/content/docs/self-hosting/environment-variables.mdx`:
- Around line 373-384: The docs for SMTP_USER must explicitly state that SMTP
authentication only occurs when both SMTP_USER and SMTP_PASS are set because in
packages/email/src/index.tsx the auth object is created only when the condition
SMTP_USER && SMTP_PASS is true; update the SMTP_USER section to note that
leaving SMTP_PASS unset (or vice versa) will silently disable authentication and
that both variables are required for authenticated SMTP connections.
- Around line 399-402: Update the existing Callout that mentions "SMTP takes
priority" to explicitly state that if SMTP_HOST is set and an SMTP send attempt
fails, the system will not automatically fall back to using RESEND_API_KEY
(emails may be silently dropped); mention that to use Resend instead either
unset SMTP_HOST or implement an application-level fallback/retry, and include
the SMTP_HOST and RESEND_API_KEY variable names in the note so readers can find
the relevant config.

In `@apps/public/content/docs/self-hosting/self-hosting.mdx`:
- Around line 137-139: Update the Callout to explicitly state that SMTP_HOST
takes precedence and that if the SMTP transport (see send function in
packages/email/src/index.tsx which returns null on error) fails during send, the
code does not fall back to Resend even if RESEND_API_KEY is set; add a single
clear sentence clarifying "no automatic Resend fallback on SMTP failure" and
reference SMTP_HOST and RESEND_API_KEY so readers know both variables won't
provide a backup transport in that failure case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bb086453-9922-4411-b0dc-b5c55ae25e89

📥 Commits

Reviewing files that changed from the base of the PR and between ea34965 and 3347986.

📒 Files selected for processing (2)
  • apps/public/content/docs/self-hosting/environment-variables.mdx
  • apps/public/content/docs/self-hosting/self-hosting.mdx

Comment thread apps/public/content/docs/self-hosting/environment-variables.mdx
Comment thread apps/public/content/docs/self-hosting/environment-variables.mdx
Comment thread apps/public/content/docs/self-hosting/self-hosting.mdx
@arianpotter arianpotter force-pushed the feature/send-email-via-smtp branch from 3347986 to db46fc8 Compare May 2, 2026 18:36
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.env.example (1)

20-25: ⚡ Quick win

Add SMTP precedence note here for consistency.

Line 20 says “pick one”, but runtime behavior uses SMTP first when both are present. Adding that note here avoids operator confusion.

Suggested diff
-# EMAIL (pick one)
+# EMAIL (pick one; SMTP takes priority if SMTP_HOST is set)
 # Option A - Resend
 # RESEND_API_KEY=""
 # EMAIL_SENDER="hello@openpanel.dev"
 # Option B - SMTP
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 20 - 25, Update the .env.example comment block for
the email configuration to clearly state that SMTP takes precedence when both
options are set: reference the RESEND_API_KEY and SMTP_HOST/EMAIL_SENDER
variables and modify the “pick one” text to indicate runtime behavior (SMTP will
be used first if both RESEND and SMTP credentials are present), so operators
understand which provider will be selected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.env.example:
- Around line 20-25: Update the .env.example comment block for the email
configuration to clearly state that SMTP takes precedence when both options are
set: reference the RESEND_API_KEY and SMTP_HOST/EMAIL_SENDER variables and
modify the “pick one” text to indicate runtime behavior (SMTP will be used first
if both RESEND and SMTP credentials are present), so operators understand which
provider will be selected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a304fa00-f0d6-4d0f-ab84-1719a6ae687d

📥 Commits

Reviewing files that changed from the base of the PR and between 3347986 and db46fc8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • .env.example
  • apps/public/content/docs/self-hosting/environment-variables.mdx
  • apps/public/content/docs/self-hosting/self-hosting.mdx
  • packages/email/package.json
  • packages/email/src/index.tsx
  • self-hosting/.env.template
✅ Files skipped from review due to trivial changes (3)
  • packages/email/package.json
  • apps/public/content/docs/self-hosting/self-hosting.mdx
  • apps/public/content/docs/self-hosting/environment-variables.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/email/src/index.tsx

@arianpotter arianpotter force-pushed the feature/send-email-via-smtp branch from db46fc8 to 8229720 Compare May 2, 2026 18:39
@arianpotter
Copy link
Copy Markdown
Author

Thanks for the contribution, first of all, have you tested this locally?

Also, could you add a section to our docs? First we could add the envs here https://github.com/Openpanel-dev/openpanel/blob/main/apps/public/content/docs/self-hosting/environment-variables.mdx and then we could add a little section here as well https://github.com/Openpanel-dev/openpanel/blob/main/apps/public/content/docs/self-hosting/self-hosting.mdx#e-mail

Sorry for the late reaction. Yes I already have tested it locally with mailpit.
I have updated both of the documents that you mentioned.

@arianpotter
Copy link
Copy Markdown
Author

why not merge the values SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER and SMTP_PASS to one variable? it would be string containing the URL and let it parse with standard URL where all necessary properties are present (https://developer.mozilla.org/en-US/docs/Web/API/URL)?

It is not a bad idea in general. However, we add some room for error in case the password contains special characters.
So I am not sure how much added value this would be. I leave it up to @lindesvard to decide whether its needed or not.

@arianpotter arianpotter requested a review from lindesvard May 2, 2026 18:58
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.

4 participants