refactor(ux): consolidate BMAD skills, update design system, and clean up Prisma generated client
This commit is contained in:
@@ -1,45 +1,123 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { getSystemConfig } from './config';
|
||||
|
||||
export interface InlineAttachment {
|
||||
filename: string
|
||||
content: Buffer
|
||||
cid: string
|
||||
}
|
||||
|
||||
interface MailOptions {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
attachments?: InlineAttachment[];
|
||||
}
|
||||
|
||||
export async function sendEmail({ to, subject, html }: MailOptions) {
|
||||
const config = await getSystemConfig();
|
||||
interface MailResult {
|
||||
success: boolean;
|
||||
messageId?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
type EmailProvider = 'auto' | 'resend' | 'smtp';
|
||||
|
||||
/**
|
||||
* Send email.
|
||||
* - 'auto': try Resend first (if key set), fall back to SMTP on failure
|
||||
* - 'smtp': force SMTP only
|
||||
* - 'resend': force Resend only
|
||||
* Supports inline image attachments via cid: references in HTML.
|
||||
*/
|
||||
export async function sendEmail({ to, subject, html, attachments }: MailOptions, provider: EmailProvider = 'auto'): Promise<MailResult> {
|
||||
const config = await getSystemConfig();
|
||||
const resendKey = config.RESEND_API_KEY || process.env.RESEND_API_KEY;
|
||||
|
||||
// Force SMTP
|
||||
if (provider === 'smtp') {
|
||||
return sendViaSMTP(config, { to, subject, html, attachments });
|
||||
}
|
||||
|
||||
// Force Resend (no fallback)
|
||||
if (provider === 'resend') {
|
||||
if (!resendKey) return { success: false, error: 'No Resend API key configured' };
|
||||
return sendViaResend(resendKey, { to, subject, html, attachments });
|
||||
}
|
||||
|
||||
// Auto: try Resend, fall back to SMTP
|
||||
if (resendKey) {
|
||||
const result = await sendViaResend(resendKey, { to, subject, html, attachments });
|
||||
if (result.success) return result;
|
||||
|
||||
console.warn('[Mail] Resend failed, falling back to SMTP:', result.error);
|
||||
return sendViaSMTP(config, { to, subject, html, attachments });
|
||||
}
|
||||
|
||||
return sendViaSMTP(config, { to, subject, html, attachments });
|
||||
}
|
||||
|
||||
async function sendViaResend(apiKey: string, { to, subject, html, attachments }: MailOptions): Promise<MailResult> {
|
||||
try {
|
||||
const { Resend } = await import('resend');
|
||||
const resend = new Resend(apiKey);
|
||||
|
||||
const from = process.env.NEXTAUTH_URL
|
||||
? `Memento <noreply@${new URL(process.env.NEXTAUTH_URL).hostname}>`
|
||||
: 'Memento <onboarding@resend.dev>';
|
||||
|
||||
// Resend supports attachments with inline content
|
||||
const resendAttachments = attachments?.map(att => ({
|
||||
filename: att.filename,
|
||||
content: att.content.toString('base64'),
|
||||
content_type: att.filename.endsWith('.png') ? 'image/png' : 'image/jpeg',
|
||||
disposition: 'inline' as const,
|
||||
content_id: att.cid,
|
||||
}));
|
||||
|
||||
const { data, error } = await resend.emails.send({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
attachments: resendAttachments,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
return { success: true, messageId: data?.id };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: `Resend: ${error.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
async function sendViaSMTP(config: Record<string, string>, { to, subject, html, attachments }: MailOptions): Promise<MailResult> {
|
||||
const host = config.SMTP_HOST || process.env.SMTP_HOST;
|
||||
const port = parseInt(config.SMTP_PORT || process.env.SMTP_PORT || '587');
|
||||
const user = (config.SMTP_USER || process.env.SMTP_USER || '').trim();
|
||||
const pass = (config.SMTP_PASS || process.env.SMTP_PASS || '').trim();
|
||||
const from = config.SMTP_FROM || process.env.SMTP_FROM || 'noreply@memento.app';
|
||||
|
||||
// Options de sécurité
|
||||
const forceSecure = config.SMTP_SECURE === 'true'; // Forcé par l'admin
|
||||
const isPort465 = port === 465;
|
||||
// Si secure n'est pas forcé, on déduit du port (465 = secure, autres = starttls)
|
||||
const secure = forceSecure || isPort465;
|
||||
|
||||
if (!host) {
|
||||
return { success: false, error: 'SMTP host is not configured' };
|
||||
}
|
||||
|
||||
const forceSecure = config.SMTP_SECURE === 'true';
|
||||
const isPort465 = port === 465;
|
||||
const secure = forceSecure || isPort465;
|
||||
const ignoreCerts = config.SMTP_IGNORE_CERT === 'true';
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: host || undefined,
|
||||
port: port || undefined,
|
||||
secure: secure || false,
|
||||
host,
|
||||
port,
|
||||
secure,
|
||||
auth: { user, pass },
|
||||
// Force IPv4 pour éviter les problèmes de résolution DNS/Docker
|
||||
family: 4,
|
||||
// Force AUTH LOGIN pour meilleure compatibilité (Mailcow, Exchange) vs PLAIN par défaut
|
||||
authMethod: 'LOGIN',
|
||||
// Timeout généreux
|
||||
connectionTimeout: 10000,
|
||||
tls: {
|
||||
// Si on ignore les certs, on autorise tout.
|
||||
// Sinon on laisse les défauts stricts de Node.
|
||||
rejectUnauthorized: !ignoreCerts,
|
||||
// Compatibilité vieux serveurs si besoin (optionnel, activé si ignoreCerts pour maximiser les chances)
|
||||
ciphers: ignoreCerts ? 'SSLv3' : undefined
|
||||
}
|
||||
} as any);
|
||||
@@ -47,19 +125,23 @@ export async function sendEmail({ to, subject, html }: MailOptions) {
|
||||
try {
|
||||
await transporter.verify();
|
||||
|
||||
// Build nodemailer inline attachments with cid
|
||||
const smtpAttachments = attachments?.map(att => ({
|
||||
filename: att.filename,
|
||||
content: att.content,
|
||||
cid: att.cid,
|
||||
}));
|
||||
|
||||
const info = await transporter.sendMail({
|
||||
from: `"Memento App" <${from}>`,
|
||||
from: `"Memento" <${from}>`,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
attachments: smtpAttachments,
|
||||
});
|
||||
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (error: any) {
|
||||
console.error("❌ Erreur SMTP:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Erreur envoi: ${error.message} (Code: ${error.code})`
|
||||
};
|
||||
console.error('SMTP error:', error);
|
||||
return { success: false, error: `SMTP: ${error.message} (Code: ${error.code})` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user