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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@
"dependencies": {
"@auth/pg-adapter": "^1.11.3",
"@aws-sdk/client-s3": "^3.1123.0",
"@bitbaum/ai-kit": "^0.6.2",
"@bitbaum/mail-kit": "^0.1.0",
"@fleet/ai-forms": "github:bitbaum/ai-forms#v0.1.2",
"@sentry/nextjs": "^10.73.0",
"@tiptap/core": "^3.30.6",
Expand All @@ -110,7 +112,6 @@
"@tiptap/pm": "^3.30.6",
"@tiptap/react": "^3.30.6",
"@tiptap/starter-kit": "^3.30.6",
"@bitbaum/ai-kit": "^0.6.2",
"bcryptjs": "^2.4.3",
"busboy": "^1.6.0",
"clsx": "^2.1.1",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ allowBuilds:
unrs-resolver: true
minimumReleaseAgeExclude:
- '@bitbaum/ai-kit'
- '@bitbaum/mail-kit'
overrides:
'@swc/helpers': 0.5.23
'@opentelemetry/core': ^2.8.0
Expand Down
69 changes: 25 additions & 44 deletions src/lib/email/resend.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,52 @@
/**
* Resend API client
* Resend transport — thin adapter over @bitbaum/mail-kit.
*
* The fleet's standard transactional-email provider (see RESEND_API_KEY shared
* across bitbaum apps). Deliberately a plain fetch to Resend's HTTP API rather
* than the `resend` SDK: we need exactly one endpoint (POST /emails) and no
* attachments, so a dependency buys nothing here.
* The fleet's one email layer (shared RESEND_API_KEY across bitbaum apps).
* mail-kit owns the HTTP call, timeout, and the "configured-looking but dead"
* guards (placeholder key, sandbox sender); this file only adapts its
* SendResult to evig's SendEmailResult and preserves the throw-on-failure
* contract that sendEmail() uses to fall back to SMTP.
*
* Sender: only `fleetcrown.orangecat.ch` is verified in the Resend account, so
* like surf-your-life and vitareba we send as <app>@fleetcrown.orangecat.ch
* until evig gets its own verified domain.
* until evig gets its own verified domain (RESEND_CONFIG.FROM reads
* RESEND_FROM with that convention as the default).
*/

import { sendMail, mailHealth, isMailConfigured } from '@bitbaum/mail-kit';
import { RESEND_CONFIG } from '@/config/email';
import { logger } from '@/lib/logger';
import type { EmailContent, SendEmailResult, TestEmailResult } from './types';

const RESEND_API_URL = 'https://api.resend.com';

export function isResendEnabled(): boolean {
return Boolean(RESEND_CONFIG.API_KEY);
return isMailConfigured();
}

export async function sendViaResend(to: string, content: EmailContent): Promise<SendEmailResult> {
const res = await fetch(`${RESEND_API_URL}/emails`, {
method: 'POST',
headers: {
Authorization: `Bearer ${RESEND_CONFIG.API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: RESEND_CONFIG.FROM,
to: [to],
subject: content.subject,
html: content.html,
text: content.text,
}),
const result = await sendMail({
to,
from: RESEND_CONFIG.FROM,
subject: content.subject,
html: content.html,
text: content.text,
});

if (!res.ok) {
const detail = await res.text().catch(() => '');
if (!result.sent) {
// Throw instead of returning failure: sendEmail() treats a thrown resend
// error as "fall back to SMTP", mirroring the existing Listmonk pattern.
throw new Error(`Resend send failed (${res.status}): ${detail.slice(0, 200)}`);
throw new Error(`Resend send failed: ${result.error}`);
}

const body = (await res.json()) as { id?: string };
logger.info('Email sent via Resend', { messageId: body.id, to });
return { success: true, messageId: body.id };
logger.info('Email sent via Resend', { messageId: result.id, to });
return { success: true, messageId: result.id };
}

/**
* Connection test for the diagnostics endpoint: an authenticated read against
* /domains proves key validity without sending anything.
* Connection test for the diagnostics endpoint: mail-kit's health probe does
* an authenticated read against /domains — proves key validity without
* sending anything.
*/
export async function testResendConnection(): Promise<TestEmailResult> {
try {
const res = await fetch(`${RESEND_API_URL}/domains`, {
headers: { Authorization: `Bearer ${RESEND_CONFIG.API_KEY}` },
});
if (!res.ok) {
return { success: false, error: `Resend API returned ${res.status}` };
}
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
const health = await mailHealth();
return health.ok ? { success: true } : { success: false, error: health.error };
}
Loading