Skip to content
Open
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
53 changes: 45 additions & 8 deletions apps/fumadocs/src/components/provider-catalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ export function ProviderGrid() {
>
<div className="flex items-center gap-3">
<ProviderMark logo={provider.logo} name={provider.name} />
<div>
<div className="min-w-0">
<div className="font-medium">{provider.name}</div>
<div className="text-xs text-fd-muted-foreground">{provider.category}</div>
</div>
</div>
<VerificationPill status={provider.verification.status} />
<code className="mt-3 block text-xs text-fd-muted-foreground">{provider.importPath}</code>
</Link>
))}
Expand All @@ -35,27 +36,63 @@ export function ProviderBadge({ adapter }: { adapter: string }) {
}

return (
<div className="not-prose mb-6 flex w-fit items-center gap-3 rounded-lg border border-fd-border bg-fd-card px-3 py-2 text-sm text-fd-foreground">
<ProviderMark logo={provider.logo} name={provider.name} />
<div>
<div className="font-medium">{provider.name}</div>
<code className="text-xs text-fd-muted-foreground">{provider.importPath}</code>
<div className="not-prose mb-6 max-w-2xl rounded-lg border border-fd-border bg-fd-card px-3 py-3 text-sm text-fd-foreground">
<div className="flex items-center gap-3">
<ProviderMark logo={provider.logo} name={provider.name} />
<div className="min-w-0">
<div className="font-medium">{provider.name}</div>
<code className="text-xs text-fd-muted-foreground">{provider.importPath}</code>
</div>
<VerificationPill className="ml-auto" status={provider.verification.status} />
</div>
<div className="mt-3 border-t border-fd-border pt-3 text-xs leading-5 text-fd-muted-foreground">
{provider.verification.note} If live delivery fails or provider behavior has changed,{" "}
<a className="font-medium text-fd-foreground underline" href="https://github.com/t3dotgg/email-sdk/issues">
open an issue
</a>
.
</div>
</div>
);
}

function VerificationPill({
className,
status,
}: {
className?: string;
status: "verified" | "untested";
}) {
if (status === "verified") {
return (
<span
className={`mt-3 inline-flex w-fit rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-700 dark:text-emerald-300 ${className ?? ""}`}
>
Live verified
</span>
);
}

return (
<span
className={`mt-3 inline-flex w-fit rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 text-[11px] font-medium text-amber-800 dark:text-amber-300 ${className ?? ""}`}
>
Untested live
</span>
);
Comment on lines +66 to +82
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The VerificationPill component concatenates className via a template string (${className ?? ""}). If className is undefined the span gets a trailing space in its class attribute. The rest of the codebase likely uses a cn() / clsx() utility — prefer that pattern to avoid potential whitespace issues. Confidence: 2/5

Suggested change
if (status === "verified") {
return (
<span
className={`mt-3 inline-flex w-fit rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-700 dark:text-emerald-300 ${className ?? ""}`}
>
Live verified
</span>
);
}
return (
<span
className={`mt-3 inline-flex w-fit rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 text-[11px] font-medium text-amber-800 dark:text-amber-300 ${className ?? ""}`}
>
Untested live
</span>
);
if (status === "verified") {
return (
<span
className={[
"mt-3 inline-flex w-fit rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[11px] font-medium text-emerald-700 dark:text-emerald-300",
className,
]
.filter(Boolean)
.join(" ")}
>
Live verified
</span>
);
}
return (
<span
className={[
"mt-3 inline-flex w-fit rounded-full border border-amber-500/30 bg-amber-500/10 px-2 py-0.5 text-[11px] font-medium text-amber-800 dark:text-amber-300",
className,
]
.filter(Boolean)
.join(" ")}
>
Untested live
</span>
);

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!

Fix in Claude Code

}

function ProviderMark({ logo, name }: { logo: string; name: string }) {
if (!logo) {
return (
<span className="grid size-9 place-items-center rounded-md border border-fd-border bg-fd-muted text-xs font-medium">
<span className="grid size-9 shrink-0 place-items-center rounded-md border border-fd-border bg-fd-muted text-xs font-medium">
SMTP
</span>
);
}

return (
<span className="grid size-9 place-items-center rounded-md border border-fd-border bg-white p-1.5">
<span className="grid size-9 shrink-0 place-items-center rounded-md border border-fd-border bg-white p-1.5">
<img
alt={`${name} logo`}
className="size-full object-contain"
Expand Down
60 changes: 60 additions & 0 deletions apps/fumadocs/src/lib/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export const providers = [
logo: "https://cdn.simpleicons.org/resend",
sidebarLogo: "https://cdn.simpleicons.org/resend/white",
category: "Popular APIs",
verification: {
status: "verified",
note: "Live delivery was verified with Resend's testing sender. Sending to arbitrary recipients still requires a verified Resend domain.",
},
},
{
name: "Postmark",
Expand All @@ -15,6 +19,10 @@ export const providers = [
docs: "/docs/adapters/postmark",
logo: "https://www.google.com/s2/favicons?domain=postmarkapp.com&sz=64",
category: "Popular APIs",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "SendGrid",
Expand All @@ -23,6 +31,10 @@ export const providers = [
docs: "/docs/adapters/sendgrid",
logo: "https://www.google.com/s2/favicons?domain=sendgrid.com&sz=64",
category: "Popular APIs",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "Mailgun",
Expand All @@ -31,6 +43,10 @@ export const providers = [
docs: "/docs/adapters/mailgun",
logo: "https://cdn.simpleicons.org/mailgun",
category: "Popular APIs",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "MailerSend",
Expand All @@ -39,6 +55,10 @@ export const providers = [
docs: "/docs/adapters/mailersend",
logo: "https://www.google.com/s2/favicons?domain=mailersend.com&sz=64",
category: "Popular APIs",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "Brevo",
Expand All @@ -47,6 +67,10 @@ export const providers = [
docs: "/docs/adapters/brevo",
logo: "https://cdn.simpleicons.org/brevo",
category: "Popular APIs",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "Mailchimp Transactional",
Expand All @@ -55,6 +79,10 @@ export const providers = [
docs: "/docs/adapters/mailchimp",
logo: "https://cdn.simpleicons.org/mailchimp",
category: "Popular APIs",
verification: {
status: "untested",
note: "Adapter contract is covered by tests. Live Mailchimp Transactional delivery is not verified because the provider is paid or account-gated.",
},
},
{
name: "SparkPost",
Expand All @@ -63,6 +91,10 @@ export const providers = [
docs: "/docs/adapters/sparkpost",
logo: "https://cdn.simpleicons.org/sparkpost",
category: "Infrastructure",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "Mailtrap",
Expand All @@ -71,6 +103,10 @@ export const providers = [
docs: "/docs/adapters/mailtrap",
logo: "https://cdn.simpleicons.org/mailtrap",
category: "Infrastructure",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "Scaleway",
Expand All @@ -79,6 +115,10 @@ export const providers = [
docs: "/docs/adapters/scaleway",
logo: "https://cdn.simpleicons.org/scaleway",
category: "Infrastructure",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "ZeptoMail",
Expand All @@ -87,6 +127,10 @@ export const providers = [
docs: "/docs/adapters/zeptomail",
logo: "https://cdn.simpleicons.org/zoho",
category: "Infrastructure",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "MailPace",
Expand All @@ -95,6 +139,10 @@ export const providers = [
docs: "/docs/adapters/mailpace",
logo: "https://www.google.com/s2/favicons?domain=mailpace.com&sz=64",
category: "Infrastructure",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "Loops",
Expand All @@ -103,6 +151,10 @@ export const providers = [
docs: "/docs/adapters/loops",
logo: "https://cdn.simpleicons.org/loops",
category: "Product-led",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "Plunk",
Expand All @@ -111,6 +163,10 @@ export const providers = [
docs: "/docs/adapters/plunk",
logo: "https://www.google.com/s2/favicons?domain=useplunk.com&sz=64",
category: "Product-led",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live provider delivery has not been verified yet.",
},
},
{
name: "SMTP",
Expand All @@ -119,6 +175,10 @@ export const providers = [
docs: "/docs/adapters/smtp",
logo: "",
category: "Transport",
verification: {
status: "untested",
note: "Adapter contract is covered by tests, but live transport delivery has not been verified yet.",
},
},
] as const;

Expand Down
60 changes: 60 additions & 0 deletions packages/email-sdk/.env.live.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copy to .env or export these in your shell before running live provider smoke tests.
# Do not commit real provider keys.

LIVE_EMAIL_TO=leodoesdev@gmail.com
LIVE_EMAIL_FROM=
LIVE_EMAIL_SUBJECT_PREFIX=Email SDK live smoke

# Optional per-adapter verified senders. Use these when each provider has a different
# verified domain or sandbox sender.
# LIVE_EMAIL_FROM_RESEND=
# LIVE_EMAIL_FROM_POSTMARK=
# LIVE_EMAIL_FROM_SENDGRID=
# LIVE_EMAIL_FROM_MAILGUN=
# LIVE_EMAIL_FROM_MAILERSEND=
# LIVE_EMAIL_FROM_BREVO=
# LIVE_EMAIL_FROM_MAILCHIMP=
# LIVE_EMAIL_FROM_SPARKPOST=
# LIVE_EMAIL_FROM_LOOPS=
# LIVE_EMAIL_FROM_PLUNK=
# LIVE_EMAIL_FROM_MAILTRAP=
# LIVE_EMAIL_FROM_SCALEWAY=
# LIVE_EMAIL_FROM_ZEPTOMAIL=
# LIVE_EMAIL_FROM_MAILPACE=

# Optional comma-separated adapter filter:
# LIVE_EMAIL_ADAPTERS=resend,postmark,sendgrid,mailgun,mailersend,brevo,mailchimp,sparkpost,loops,plunk,mailtrap,scaleway,zeptomail,mailpace

RESEND_API_KEY=

POSTMARK_SERVER_TOKEN=
POSTMARK_MESSAGE_STREAM=outbound

SENDGRID_API_KEY=

MAILGUN_API_KEY=
MAILGUN_DOMAIN=
MAILGUN_BASE_URL=

MAILERSEND_API_KEY=

BREVO_API_KEY=

MAILCHIMP_API_KEY=

SPARKPOST_API_KEY=

LOOPS_API_KEY=
LOOPS_TRANSACTIONAL_ID=

PLUNK_API_KEY=

MAILTRAP_API_KEY=

SCALEWAY_SECRET_KEY=
SCALEWAY_PROJECT_ID=
SCALEWAY_REGION=fr-par

ZEPTOMAIL_TOKEN=

MAILPACE_API_KEY=
35 changes: 35 additions & 0 deletions packages/email-sdk/live-verification.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"providers": {
"resend": {
"status": "verified",
"verifiedAt": "2026-05-30",
"scope": "Resend testing sender to account-owner recipient",
"messageId": "1415dfa2-2170-46bf-8537-2a625b752f96",
"notes": [
"A previous successful Resend smoke returned message ID 808d95ee-bf2e-4a40-a389-3ef912f9422a.",
"Sending to arbitrary recipients returned Resend's testing-mode restriction.",
"Domain verification is still required for production recipient coverage."
]
},
"postmark": { "status": "untested" },
"sendgrid": { "status": "untested" },
"mailgun": { "status": "untested" },
"mailersend": { "status": "untested" },
"brevo": { "status": "untested" },
"mailchimp": {
"status": "untested",
"notes": ["Live Mailchimp Transactional delivery was skipped because the provider is paid or account-gated."]
},
"sparkpost": { "status": "untested" },
"loops": {
"status": "untested",
"notes": ["The current adapter requires both LOOPS_API_KEY and LOOPS_TRANSACTIONAL_ID."]
},
"plunk": { "status": "untested" },
"mailtrap": { "status": "untested" },
"scaleway": { "status": "untested" },
"zeptomail": { "status": "untested" },
"mailpace": { "status": "untested" },
"smtp": { "status": "untested" }
}
}
1 change: 1 addition & 0 deletions packages/email-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.json && chmod +x dist/cli.js",
"check-types": "tsc -p tsconfig.json --noEmit",
"live:smoke": "bun run src/live-smoke.ts",
"test": "bun test"
},
"devDependencies": {
Expand Down
Loading