01MVP 标识01MVP
包文档基础设施服务Email

Email

邮件发送服务,支持多服务商和模板系统

@mono/email

邮件发送服务包,提供多邮件服务商抽象、模板支持和类型安全的 API。基于工厂模式,轻松切换邮件服务商。

特性

  • 多服务商支持 - 支持 Resend,可扩展 SendGrid、SMTP 等
  • 类型安全 - 完整的 TypeScript 类型定义
  • 简洁 API - 工厂模式创建服务商,统一发送接口
  • 错误处理 - 结构化的错误响应
  • 灵活配置 - 自定义发件人、回复地址等

安装

pnpm add @mono/email

快速开始

import { createEmailProvider } from '@mono/email';

// 创建 Resend 服务商实例
const emailProvider = createEmailProvider('resend', {
  apiKey: process.env.RESEND_API_KEY!,
  defaultFrom: 'noreply@yourdomain.com',
});

// 发送邮件
const result = await emailProvider.send({
  to: 'user@example.com',
  subject: '欢迎加入!',
  html: '<h1>欢迎</h1><p>感谢您的注册!</p>',
});

if (result.success) {
  console.log(`邮件已发送,ID: ${result.messageId}`);
} else {
  console.error(`发送失败: ${result.error}`);
}

API 参考

createEmailProvider(type, config)

创建邮件服务商实例。

参数:

  • type: 'resend' - 服务商类型
  • config.apiKey: string - API 密钥
  • config.defaultFrom?: string - 默认发件人

返回: EmailProvider

const provider = createEmailProvider('resend', {
  apiKey: process.env.RESEND_API_KEY!,
  defaultFrom: 'noreply@yourdomain.com',
});

sendEmail(provider, options)

辅助函数,使用指定服务商发送邮件。

import { createEmailProvider, sendEmail } from '@mono/email';

const provider = createEmailProvider('resend', { apiKey: '...' });

await sendEmail(provider, {
  to: 'user@example.com',
  subject: '通知',
  html: '<p>内容</p>',
});

EmailOptions

字段类型必填说明
tostring | string[]收件人
subjectstring主题
htmlstringHTML 内容
textstring纯文本内容
fromstring发件人(覆盖默认)
replyTostring回复地址

EmailResponse

字段类型说明
successboolean是否成功
messageIdstring?邮件 ID
errorstring?错误信息

实战示例

示例 1: 验证码邮件

// lib/email.ts
import { createEmailProvider } from '@mono/email';

export const emailProvider = createEmailProvider('resend', {
  apiKey: process.env.RESEND_API_KEY!,
  defaultFrom: 'auth@yourdomain.com',
});

export async function sendVerificationEmail(email: string, code: string) {
  return emailProvider.send({
    to: email,
    subject: '邮箱验证码',
    html: `
      <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
        <h2>邮箱验证</h2>
        <p>您的验证码是:</p>
        <div style="font-size: 32px; font-weight: bold; padding: 16px;
                    background: #f5f5f5; border-radius: 8px; text-align: center;">
          ${code}
        </div>
        <p style="color: #666;">验证码 10 分钟内有效,请勿泄露给他人。</p>
      </div>
    `,
  });
}

示例 2: 密码重置邮件

// lib/email.ts
export async function sendResetPasswordEmail(email: string, resetUrl: string) {
  return emailProvider.send({
    to: email,
    subject: '密码重置',
    html: `
      <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
        <h2>密码重置</h2>
        <p>您请求了密码重置,点击下方按钮设置新密码:</p>
        <a href="${resetUrl}"
           style="display: inline-block; padding: 12px 24px;
                  background: #000; color: #fff; text-decoration: none;
                  border-radius: 8px;">
          重置密码
        </a>
        <p style="color: #666; margin-top: 16px;">
          如果您没有请求密码重置,请忽略此邮件。链接 1 小时内有效。
        </p>
      </div>
    `,
  });
}

示例 3: 联系表单 API 路由

// app/api/contact/route.ts
import { emailProvider } from '@/lib/email';
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const { name, email, message } = await req.json();

  const result = await emailProvider.send({
    to: 'admin@yourdomain.com',
    subject: `联系表单: ${name}`,
    html: `
      <p><strong>姓名:</strong> ${name}</p>
      <p><strong>邮箱:</strong> ${email}</p>
      <p><strong>消息:</strong></p>
      <p>${message}</p>
    `,
    replyTo: email,
  });

  if (!result.success) {
    return NextResponse.json({ error: '发送失败' }, { status: 500 });
  }

  return NextResponse.json({ success: true });
}

示例 4: 邀请成员邮件

// app/api/invite/route.ts
import { emailProvider } from '@/lib/email';
import { auth } from '@mono/auth';
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const session = await auth.api.getSession({ headers: req.headers });
  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { email, role } = await req.json();

  const inviteUrl = `${process.env.NEXT_PUBLIC_APP_URL}/invite?token=xxx`;

  const result = await emailProvider.send({
    to: email,
    subject: `${session.user.name} 邀请您加入团队`,
    html: `
      <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
        <h2>团队邀请</h2>
        <p>${session.user.name} 邀请您以 <strong>${role}</strong> 身份加入团队。</p>
        <a href="${inviteUrl}"
           style="display: inline-block; padding: 12px 24px;
                  background: #000; color: #fff; text-decoration: none;
                  border-radius: 8px; margin: 16px 0;">
          接受邀请
        </a>
        <p style="color: #666;">邀请链接 7 天内有效。</p>
      </div>
    `,
  });

  if (!result.success) {
    return NextResponse.json({ error: '邮件发送失败' }, { status: 500 });
  }

  return NextResponse.json({ success: true });
}

示例 5: 批量通知

import { emailProvider } from '@/lib/email';

interface Recipient {
  email: string;
  name: string;
}

export async function sendBatchNotification(
  recipients: Recipient[],
  title: string,
  content: string,
) {
  const results = await Promise.allSettled(
    recipients.map((r) =>
      emailProvider.send({
        to: r.email,
        subject: title,
        html: `
          <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
            <p>Hi ${r.name},</p>
            ${content}
          </div>
        `,
      }),
    ),
  );

  const succeeded = results.filter(
    (r) => r.status === 'fulfilled' && r.value.success,
  ).length;

  return { total: recipients.length, succeeded, failed: recipients.length - succeeded };
}

示例 6: 自定义服务商

import type { EmailProvider, EmailOptions, EmailResponse } from '@mono/email';

// 实现自定义 SMTP 服务商
class SmtpProvider implements EmailProvider {
  private transporter: any;

  constructor(config: { host: string; port: number; auth: { user: string; pass: string } }) {
    // 使用 nodemailer 等库初始化
    // this.transporter = nodemailer.createTransport(config);
  }

  async send(options: EmailOptions): Promise<EmailResponse> {
    try {
      // 发送邮件逻辑
      return { success: true, messageId: 'xxx' };
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  }
}

// 使用自定义服务商
const smtpProvider = new SmtpProvider({
  host: 'smtp.example.com',
  port: 587,
  auth: { user: 'user', pass: 'pass' },
});

await smtpProvider.send({
  to: 'user@example.com',
  subject: 'Test',
  html: '<p>Hello</p>',
});

环境变量

# Resend API 密钥(必需)
RESEND_API_KEY=re_xxxxxxxxxxxxx

# 默认发件人(可选)
EMAIL_DEFAULT_FROM=noreply@yourdomain.com

集成指南

1. 初始化服务

// lib/email.ts
import { createEmailProvider } from '@mono/email';

export const emailProvider = createEmailProvider('resend', {
  apiKey: process.env.RESEND_API_KEY!,
  defaultFrom: process.env.EMAIL_DEFAULT_FROM || 'noreply@yourdomain.com',
});

2. 与认证系统集成

// lib/auth/email.ts
import { emailProvider } from '@/lib/email';

export const emailHandlers = {
  sendVerificationEmail: async (email: string, url: string) => {
    await emailProvider.send({
      to: email,
      subject: '验证您的邮箱',
      html: `<a href="${url}">点击验证</a>`,
    });
  },
  sendResetPasswordEmail: async (email: string, url: string) => {
    await emailProvider.send({
      to: email,
      subject: '重置密码',
      html: `<a href="${url}">重置密码</a>`,
    });
  },
};

3. DNS 配置

在 Resend 中添加域名后,配置以下 DNS 记录:

类型名称
MX@feedback-smtp.region.amazonses.com
TXT@v=spf1 include:amazonses.com ~all
CNAMEresend._domainkey由 Resend 提供

最佳实践

  1. 统一初始化 - 在 lib/email.ts 中创建单例实例
  2. 使用环境变量 - 不要硬编码 API 密钥和发件人
  3. 错误处理 - 始终检查 result.success
  4. 双格式内容 - 同时提供 htmltext 提高兼容性
  5. 域名验证 - 确保发件人域名配置了 SPF/DKIM/DMARC

故障排查

邮件发送失败

  1. 检查 RESEND_API_KEY 是否正确
  2. 确认发件人域名已在 Resend 验证
  3. 检查收件人地址格式

邮件进入垃圾箱

  1. 完成 SPF、DKIM、DMARC 配置
  2. 避免邮件中过多图片或可疑链接
  3. 使用认证域名发送

批量发送限制

Resend 免费版限制 100 封/天,建议:

  • 生产环境使用付费版本
  • 实现发送队列避免突发大量请求
  • 监控发送失败率

相关文档

许可证

MIT