mcpField
E
email-notifications

email-notifications

github.com/jmsktm开发1066 次安装41 周前更新
代码仓库

关于 email-notifications

email-notifications 是帮助开发者在 Next.js 应用中实现邮件功能的技能,涵盖服务商配置、React Email 模板、触发条件、偏好设置、测试和送达跟踪。

主要功能

  1. 01配置 Resend、SendGrid 和 SMTP
  2. 02创建 React Email 模板
  3. 03设置注册与订单邮件触发
  4. 04处理邮件偏好与退订
  5. 05测试邮件并跟踪互动

适用场景

  1. 01发送注册欢迎邮件
  2. 02构建订单确认邮件
  3. 03创建定时邮件活动
  4. 04排查送达问题并预览模板

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jmsktm/claude-settings --skill email-notifications

安装后在 Claude Code 中输入/email-notifications确认已加载。

详细介绍

电子邮件通知技能

概述

此技能可帮助你在 Next.js 应用中实现电子邮件通知。从事务性邮件到定时营销活动,涵盖可靠发送邮件所需的一切。

电子邮件服务提供商

Resend(推荐用于 Next.js)

安装:

npm install resend

设置:

// lib/email.ts
import { Resend } from 'resend'

export const resend = new Resend(process.env.RESEND_API_KEY)

发送邮件:

// app/api/email/send/route.ts
import { resend } from '@/lib/email'

export async function POST(request: Request) {
  const { to, subject, html } = await request.json()

  try {
    const { data, error } = await resend.emails.send({
      from: '[email protected]',
      to,
      subject,
      html,
    })

    if (error) {
      return Response.json({ error }, { status: 400 })
    }

    return Response.json({ data })
  } catch (error) {
    return Response.json({ error }, { status: 500 })
  }
}
SendGrid

安装:

npm install @sendgrid/mail

设置:

// lib/sendgrid.ts
import sgMail from '@sendgrid/mail'

sgMail.setApiKey(process.env.SENDGRID_API_KEY!)

export async function sendEmail({
  to,
  subject,
  html,
}: {
  to: string
  subject: string
  html: string
}) {
  await sgMail.send({
    from: '[email protected]',
    to,
    subject,
    html,
  })
}
Nodemailer(SMTP)

安装:

npm install nodemailer

设置:

// lib/nodemailer.ts
import nodemailer from 'nodemailer'

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: parseInt(process.env.SMTP_PORT!),
  secure: true,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASSWORD,
  },
})

export async function sendEmail({
  to,
  subject,
  html,
}: {
  to: string
  subject: string
  html: string
}) {
  await transporter.sendMail({
    from: process.env.EMAIL_FROM,
    to,
    subject,
    html,
  })
}

React Email(电子邮件模板)

安装
npm install react-email @react-email/components
npm install -D @react-email/tailwind
创建电子邮件模板
// emails/welcome.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Link,
  Preview,
  Section,
  Text,
  Tailwind,
} from '@react-email/components'

interface WelcomeEmailProps {
  name: string
  loginUrl: string
}

export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to our platform, {name}!</Preview>
      <Tailwind>
        <Body className="bg-gray-100 font-sans">
          <Container className="bg-white mx-auto my-8 p-8 rounded-lg shadow-lg max-w-2xl">
            <Heading className="text-2xl font-bold text-gray-900 mb-4">
              Welcome, {name}!
            </Heading>

            <Text className="text-gray-700 mb-4">
              We're excited to have you on board. Get started by logging in to your account.
            </Text>

            <Section className="text-center my-8">
              <Button
                href={loginUrl}
                className="bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold"
              >
                Log In to Your Account
              </Button>
            </Section>

            <Text className="text-gray-600 text-sm">
              If you didn't create this account, you can safely ignore this email.
            </Text>

            <Text className="text-gray-500 text-xs mt-8">
              © 2024 Your Company. All rights reserved.
              <br />
              <Link href="https://yourcompany.com/unsubscribe">Unsubscribe</Link>
            </Text>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  )
}

export default WelcomeEmail
渲染电子邮件模板
import { render } from '@react-email/components'
import { WelcomeEmail } from '@/emails/welcome'
import { resend } from '@/lib/email'

export async function sendWelcomeEmail(name: string, email: string) {
  const html = render(
    <WelcomeEmail
      name={name}
      loginUrl={`${process.env.NEXT_PUBLIC_APP_URL}/login`}
    />
  )

  await resend.emails.send({
    from: '[email protected]',
    to: email,
    subject: `Welcome, ${name}!`,
    html,
  })
}
预览电子邮件(开发环境)
# Add script to package.json
{
  "scripts": {
    "email:dev": "email dev -p 3001"
  }
}

# Run preview server
npm run email:dev
# Visit http://localhost:3001

常见电子邮件模板

密码重置
// emails/password-reset.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Section,
  Text,
  Tailwind,
} from '@react-email/components'

interface PasswordResetEmailProps {
  name: string
  resetUrl: string
}

export function PasswordResetEmail({ name, resetUrl }: PasswordResetEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Reset your password</Preview>
      <Tailwind>
        <Body className="bg-gray-100 font-sans">
          <Container className="bg-white mx-auto my-8 p-8 rounded-lg max-w-2xl">
            <Heading className="text-2xl font-bold mb-4">
              Reset Your Password
            </Heading>

            <Text className="text-gray-700 mb-4">
              Hi {name}, we received a request to reset your password.
            </Text>

            <Section className="text-center my-8">
              <Button
                href={resetUrl}
                className="bg-blue-600 text-white px-6 py-3 rounded-lg"
              >
                Reset Password
              </Button>
            </Section>

            <Text className="text-gray-600 text-sm">
              This link will expire in 1 hour. If you didn't request this, you can safely ignore this email.
            </Text>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  )
}
订单确认
// emails/order-confirmation.tsx
interface OrderConfirmationEmailProps {
  name: string
  orderId: string
  items: Array<{ name: string; price: number; quantity: number }>
  total: number
}

export function OrderConfirmationEmail({
  name,
  orderId,
  items,
  total,
}: OrderConfirmationEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Order confirmation #{orderId}</Preview>
      <Tailwind>
        <Body className="bg-gray-100 font-sans">
          <Container className="bg-white mx-auto my-8 p-8 rounded-lg max-w-2xl">
            <Heading className="text-2xl font-bold mb-4">
              Order Confirmed!
            </Heading>

            <Text>Hi {name}, thanks for your order!</Text>
            <Text className="text-gray-600">Order #{orderId}</Text>

            <Section className="my-8">
              {items.map((item, index) => (
                <div key={index} className="flex justify-between py-2 border-b">
                  <Text>
                    {item.quantity}x {item.name}
                  </Text>
                  <Text>${item.price.toFixed(2)}</Text>
                </div>
              ))}
              <div className="flex justify-between py-4 font-bold">
                <Text>Total</Text>
                <Text>${total.toFixed(2)}</Text>
              </div>
            </Section>

            <Button href={`${process.env.NEXT_PUBLIC_APP_URL}/orders/${orderId}`}>
              View Order Details
            </Button>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  )
}

电子邮件触发器

注册时发送欢迎邮件
// app/api/auth/signup/route.ts
import { sendWelcomeEmail } from '@/lib/emails/send'

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

  // Create user
  const user = await createUser({ email, name })

  // Send welcome email (async, don't await)
  sendWelcomeEmail(name, email).catch((error) => {
    console.error('Failed to send welcome email:', error)
  })

  return Response.json({ user })
}
订单确认
// lib/emails/send.ts
import { render } from '@react-email/components'
import { OrderConfirmationEmail } from '@/emails/order-confirmation'
import { resend } from '@/lib/email'

export async function sendOrderConfirmation(order: Order) {
  const html = render(
    <OrderConfirmationEmail
      name={order.customerName}
      orderId={order.id}
      items={order.items}
      total={order.total}
    />
  )

  await resend.emails.send({
    from: '[email protected]',
    to: order.customerEmail,
    subject: `Order Confirmation #${order.id}`,
    html,
  })
}

定时发送邮件

每日摘要
// app/api/cron/daily-digest/route.ts
import { sendDailyDigest } from '@/lib/emails/send'

export async function GET(request: Request) {
  // Verify cron secret
  const authHeader = request.headers.get('authorization')
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return new Response('Unauthorized', { status: 401 })
  }

  // Get users who opted in for daily digest
  const users = await db.users.findMany({
    where: { emailPreferences: { dailyDigest: true } },
  })

  // Send emails in batches
  for (const user of users) {
    await sendDailyDigest(user)
  }

  return Response.json({ sent: users.length })
}
Vercel Cron 配置
// vercel.json
{
  "crons": [
    {
      "path": "/api/cron/daily-digest",
      "schedule": "0 9 * * *"
    },
    {
      "path": "/api/cron/weekly-summary",
      "schedule": "0 9 * * 1"
    }
  ]
}

电子邮件队列(后台任务)

使用 Supabase Edge Functions
// supabase/functions/send-email/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { Resend } from 'npm:resend'

const resend = new Resend(Deno.env.get('RESEND_API_KEY'))

serve(async (req) => {
  const { to, subject, html } = await req.json()

  const { data, error } = await resend.emails.send({
    from: '[email protected]',
    to,
    subject,
    html,
  })

  return new Response(JSON.stringify({ data, error }), {
    headers: { 'Content-Type': 'application/json' },
  })
})
将邮件加入发送队列
import { createClient } from '@/lib/supabase/server'

export async function queueEmail({
  to,
  subject,
  html,
}: {
  to: string
  subject: string
  html: string
}) {
  const supabase = createClient()

  await supabase.from('email_queue').insert({
    to,
    subject,
    html,
    status: 'pending',
    created_at: new Date().toISOString(),
  })
}

// Process queue with cron
export async function processEmailQueue() {
  const supabase = createClient()

  const { data: emails } = await supabase
    .from('email_queue')
    .select('*')
    .eq('status', 'pending')
    .limit(10)

  for (const email of emails || []) {
    try {
      await resend.emails.send({
        from: '[email protected]',
        to: email.to,
        subject: email.subject,
        html: email.html,
      })

      await supabase
        .from('email_queue')
        .update({ status: 'sent', sent_at: new Date().toISOString() })
        .eq('id', email.id)
    } catch (error) {
      await supabase
        .from('email_queue')
        .update({
          status: 'failed',
          error: error.message,
          attempts: email.attempts + 1,
        })
        .eq('id', email.id)
    }
  }
}

电子邮件偏好设置

用户电子邮件设置
// Database schema
type EmailPreferences = {
  marketing: boolean
  productUpdates: boolean
  weeklyDigest: boolean
  orderUpdates: boolean
}

// Update preferences
export async function updateEmailPreferences(
  userId: string,
  preferences: Partial<EmailPreferences>
) {
  await db.users.update({
    where: { id: userId },
    data: { emailPreferences: preferences },
  })
}

// Check before sending
export async function canSendEmail(
  userId: string,
  emailType: keyof EmailPreferences
): Promise<boolean> {
  const user = await db.users.findUnique({
    where: { id: userId },
    select: { emailPreferences: true },
  })

  return user?.emailPreferences[emailType] ?? false
}
取消订阅链接
// app/api/unsubscribe/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const token = searchParams.get('token')

  // Verify token and get user
  const userId = await verifyUnsubscribeToken(token)

  // Unsubscribe from all marketing emails
  await db.users.update({
    where: { id: userId },
    data: {
      emailPreferences: {
        marketing: false,
        productUpdates: false,
      },
    },
  })

  return new Response('You have been unsubscribed', { status: 200 })
}

// Generate unsubscribe link
function getUnsubscribeUrl(userId: string): string {
  const token = generateUnsubscribeToken(userId)
  return `${process.env.NEXT_PUBLIC_APP_URL}/api/unsubscribe?token=${token}`
}

电子邮件测试

在本地测试邮件
// lib/email-test.ts
import { sendWelcomeEmail } from '@/lib/emails/send'

async function testEmail() {
  await sendWelcomeEmail('Test User', '[email protected]')
  console.log('Test email sent!')
}

testEmail()
在浏览器中预览电子邮件
// app/email-preview/[template]/page.tsx
import { WelcomeEmail } from '@/emails/welcome'

export default function EmailPreview({
  params,
}: {
  params: { template: string }
}) {
  const templates = {
    welcome: <WelcomeEmail name="John Doe" loginUrl="/login" />,
    // Add more templates
  }

  return (
    <div className="p-8">
      {templates[params.template]}
    </div>
  )
}

电子邮件分析

跟踪邮件打开情况
// emails/welcome.tsx
export function WelcomeEmail({ name, userId }: { name: string; userId: string }) {
  const trackingPixel = `${process.env.NEXT_PUBLIC_APP_URL}/api/email/track/open?userId=${userId}&email=welcome`

  return (
    <Html>
      {/* Email content */}
      <img src={trackingPixel} width="1" height="1" alt="" />
    </Html>
  )
}

// app/api/email/track/open/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const userId = searchParams.get('userId')
  const emailType = searchParams.get('email')

  // Track email open
  await db.emailEvents.create({
    data: {
      userId,
      emailType,
      event: 'open',
      timestamp: new Date(),
    },
  })

  // Return 1x1 transparent pixel
  const pixel = Buffer.from(
    'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
    'base64'
  )

  return new Response(pixel, {
    headers: {
      'Content-Type': 'image/gif',
      'Cache-Control': 'no-store',
    },
  })
}
跟踪链接点击情况
// Create tracked link
function createTrackedLink(url: string, userId: string, emailType: string): string {
  return `${process.env.NEXT_PUBLIC_APP_URL}/api/email/track/click?url=${encodeURIComponent(url)}&userId=${userId}&email=${emailType}`
}

// Handle click tracking
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const url = searchParams.get('url')
  const userId = searchParams.get('userId')
  const emailType = searchParams.get('email')

  // Track click
  await db.emailEvents.create({
    data: {
      userId,
      emailType,
      event: 'click',
      metadata: { url },
      timestamp: new Date(),
    },
  })

  // Redirect to actual URL
  return Response.redirect(url!, 302)
}

最佳实践清单

  • 使用事务性电子邮件服务提供商(Resend/SendGrid)
  • 使用 React Email 创建电子邮件模板
  • 实现取消订阅链接
  • 尊重用户的电子邮件偏好设置
  • 将邮件加入队列以便在后台处理
  • 添加电子邮件跟踪(打开、点击)
  • 发送前测试邮件
  • 使用合适的发件人地址
  • 包含纯文本版本
  • 处理退信和投诉
  • 为失败情况添加重试逻辑
  • 监控邮件送达率
  • 遵守电子邮件法规(CAN-SPAM、GDPR)
  • 使用电子邮件身份验证(SPF、DKIM、DMARC)

何时使用此技能

在以下情况下调用此技能:

  • 设置电子邮件通知
  • 创建电子邮件模板
  • 实现事务性邮件
  • 构建电子邮件营销活动
  • 设置定时邮件
  • 实现电子邮件偏好设置
  • 调试邮件送达问题
  • 添加电子邮件跟踪
  • 创建取消订阅流程
  • 测试电子邮件模板

评论

支持 Markdown