email-sending

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Email Sending

邮件发送

Node.js (Nodemailer)

Node.js(Nodemailer)

typescript
import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: 587,
  secure: false, // true for 465
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});

await transporter.sendMail({
  from: '"My App" <noreply@myapp.com>',
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<h1>Welcome</h1><p>Thanks for signing up.</p>',
});
typescript
import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: 587,
  secure: false, // true for 465
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});

await transporter.sendMail({
  from: '"My App" <noreply@myapp.com>',
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<h1>Welcome</h1><p>Thanks for signing up.</p>',
});

Transactional Email Services

事务性邮件服务

Resend (recommended for simplicity)

Resend(推荐用于简单场景)

typescript
import { Resend } from 'resend';

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

await resend.emails.send({
  from: 'My App <noreply@myapp.com>',
  to: ['user@example.com'],
  subject: 'Welcome!',
  html: '<h1>Welcome</h1>',
});
typescript
import { Resend } from 'resend';

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

await resend.emails.send({
  from: 'My App <noreply@myapp.com>',
  to: ['user@example.com'],
  subject: 'Welcome!',
  html: '<h1>Welcome</h1>',
});

SendGrid

SendGrid

typescript
import sgMail from '@sendgrid/mail';
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);

await sgMail.send({
  to: 'user@example.com',
  from: 'noreply@myapp.com',
  templateId: 'd-abc123',        // Dynamic template
  dynamicTemplateData: { name: 'John', link: resetUrl },
});
typescript
import sgMail from '@sendgrid/mail';
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);

await sgMail.send({
  to: 'user@example.com',
  from: 'noreply@myapp.com',
  templateId: 'd-abc123',        // Dynamic template
  dynamicTemplateData: { name: 'John', link: resetUrl },
});

Amazon SES (via AWS SDK v3)

Amazon SES(通过AWS SDK v3)

typescript
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';

const ses = new SESv2Client({ region: 'us-east-1' });
await ses.send(new SendEmailCommand({
  FromEmailAddress: 'noreply@myapp.com',
  Destination: { ToAddresses: ['user@example.com'] },
  Content: {
    Simple: {
      Subject: { Data: 'Welcome!' },
      Body: { Html: { Data: htmlContent } },
    },
  },
}));
typescript
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';

const ses = new SESv2Client({ region: 'us-east-1' });
await ses.send(new SendEmailCommand({
  FromEmailAddress: 'noreply@myapp.com',
  Destination: { ToAddresses: ['user@example.com'] },
  Content: {
    Simple: {
      Subject: { Data: 'Welcome!' },
      Body: { Html: { Data: htmlContent } },
    },
  },
}));

Email Templates

邮件模板

React Email (recommended for React projects)

React Email(推荐用于React项目)

tsx
// emails/welcome.tsx
import { Html, Head, Body, Container, Text, Button } from '@react-email/components';

export function WelcomeEmail({ name, url }: { name: string; url: string }) {
  return (
    <Html>
      <Head />
      <Body style={{ fontFamily: 'sans-serif' }}>
        <Container>
          <Text>Welcome, {name}!</Text>
          <Button href={url} style={{ background: '#000', color: '#fff', padding: '12px 20px' }}>
            Get Started
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

// Render to HTML string
import { render } from '@react-email/render';
const html = await render(<WelcomeEmail name="John" url="https://app.com" />);
tsx
// emails/welcome.tsx
import { Html, Head, Body, Container, Text, Button } from '@react-email/components';

export function WelcomeEmail({ name, url }: { name: string; url: string }) {
  return (
    <Html>
      <Head />
      <Body style={{ fontFamily: 'sans-serif' }}>
        <Container>
          <Text>Welcome, {name}!</Text>
          <Button href={url} style={{ background: '#000', color: '#fff', padding: '12px 20px' }}>
            Get Started
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

// Render to HTML string
import { render } from '@react-email/render';
const html = await render(<WelcomeEmail name="John" url="https://app.com" />);

MJML (framework-agnostic)

MJML(跨框架通用)

typescript
import mjml2html from 'mjml';

const { html } = mjml2html(`
  <mjml>
    <mj-body>
      <mj-section>
        <mj-column>
          <mj-text>Welcome, {{name}}!</mj-text>
          <mj-button href="{{url}}">Get Started</mj-button>
        </mj-column>
      </mj-section>
    </mj-body>
  </mjml>
`);
typescript
import mjml2html from 'mjml';

const { html } = mjml2html(`
  <mjml>
    <mj-body>
      <mj-section>
        <mj-column>
          <mj-text>Welcome, {{name}}!</mj-text>
          <mj-button href="{{url}}">Get Started</mj-button>
        </mj-column>
      </mj-section>
    </mj-body>
  </mjml>
`);

Python

Python

python
from email.message import EmailMessage
import aiosmtplib

msg = EmailMessage()
msg["From"] = "noreply@myapp.com"
msg["To"] = "user@example.com"
msg["Subject"] = "Welcome!"
msg.set_content("Welcome!", subtype="html")

await aiosmtplib.send(msg, hostname=SMTP_HOST, port=587,
    username=SMTP_USER, password=SMTP_PASS, start_tls=True)
python
from email.message import EmailMessage
import aiosmtplib

msg = EmailMessage()
msg["From"] = "noreply@myapp.com"
msg["To"] = "user@example.com"
msg["Subject"] = "Welcome!"
msg.set_content("Welcome!", subtype="html")

await aiosmtplib.send(msg, hostname=SMTP_HOST, port=587,
    username=SMTP_USER, password=SMTP_PASS, start_tls=True)

Java (Spring Mail)

Java(Spring Mail)

java
@Service
public class EmailService {
    @Autowired private JavaMailSender mailSender;
    @Autowired private TemplateEngine templateEngine; // Thymeleaf

    public void sendWelcome(String to, String name) {
        var ctx = new Context();
        ctx.setVariable("name", name);
        String html = templateEngine.process("welcome", ctx);

        var message = mailSender.createMimeMessage();
        var helper = new MimeMessageHelper(message, true);
        helper.setTo(to);
        helper.setFrom("noreply@myapp.com");
        helper.setSubject("Welcome!");
        helper.setText(html, true);
        mailSender.send(message);
    }
}
java
@Service
public class EmailService {
    @Autowired private JavaMailSender mailSender;
    @Autowired private TemplateEngine templateEngine; // Thymeleaf

    public void sendWelcome(String to, String name) {
        var ctx = new Context();
        ctx.setVariable("name", name);
        String html = templateEngine.process("welcome", ctx);

        var message = mailSender.createMimeMessage();
        var helper = new MimeMessageHelper(message, true);
        helper.setTo(to);
        helper.setFrom("noreply@myapp.com");
        helper.setSubject("Welcome!");
        helper.setText(html, true);
        mailSender.send(message);
    }
}

Anti-Patterns

反模式

Anti-PatternFix
Sending email in request handlerUse background job queue (BullMQ, Celery)
No retry on transient failureRetry with exponential backoff (3 attempts)
Hardcoded from addressUse env var, match verified domain
HTML without plain text fallbackAlways include both HTML and text versions
No unsubscribe headerAdd
List-Unsubscribe
header (required by Gmail/Yahoo)
Sending from unverified domainSet up SPF, DKIM, DMARC records
反模式修复方案
在请求处理器中发送邮件使用后台任务队列(BullMQ、Celery)
临时失败时不重试使用指数退避策略重试(3次尝试)
硬编码发件人地址使用环境变量,匹配已验证域名
仅使用HTML格式而不提供纯文本备选始终同时包含HTML和纯文本版本
未添加退订头添加
List-Unsubscribe
头(Gmail/Yahoo要求)
从未验证域名发送邮件配置SPF、DKIM、DMARC记录

Production Checklist

生产环境检查清单

  • SPF, DKIM, DMARC DNS records configured
  • Domain verified with email provider
  • List-Unsubscribe
    header on marketing emails
  • Bounce and complaint handling (webhook)
  • Email sending via background queue (not in request)
  • Rate limiting to stay within provider limits
  • Plain text fallback for all HTML emails
  • Template preview/testing before deploy
  • 已配置SPF、DKIM、DMARC DNS记录
  • 域名已通过邮件服务提供商验证
  • 营销邮件已添加
    List-Unsubscribe
  • 已处理退回和投诉(通过Webhook)
  • 通过后台队列发送邮件(而非在请求中发送)
  • 已设置速率限制以符合服务提供商的限制
  • 所有HTML邮件均提供纯文本备选
  • 部署前已预览/测试模板