mcpField
E
email-systems

email-systems

github.com/travisjneuman開発106128 回インストール1 日前に更新
リポジトリ

email-systems について

email-systems は、信頼性の高いメール送信フローを構築する Web 開発者向けのガイドです。トランザクションメールとマーケティングメール、テンプレート、配信に加え、キューの再試行や DNS 認証、バウンス処理を扱います。

主な機能

  1. 01トランザクションメールとマーケティングメールの分離
  2. 02React Email と MJML のテンプレートパターン
  3. 03キュー送信と再試行
  4. 04SPF、DKIM、DMARC の設定
  5. 05バウンス、苦情、抑制リストの管理
  6. 06メール配信のテストと調査

活用シーン

  1. 01歓迎メールやパスワード再設定メールの送信
  2. 02注文確認や通知ダイジェストの構築
  3. 03メールが迷惑メールに入る問題の調査
  4. 04メールサービスプロバイダー間の移行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/travisjneuman/.claude --skill email-systems

インストール後、Claude Code で/email-systemsを入力して読み込まれたか確認します。

詳細

メールシステム

概要

このスキルでは、トランザクションメールの送信、テンプレートの設計、到達率の最適化、受信トレイへの配信など、ウェブアプリケーション向けの信頼性の高いメールシステムの構築を扱います。メールサービスプロバイダー(Resend、AWS SES、SendGrid、Postmark)との統合、React Email と MJML を使ったレスポンシブな HTML メールテンプレートの構築、到達率向上のための DNS レコード(SPF、DKIM、DMARC)の設定、メールの種類(トランザクション、通知、マーケティング)の管理、キュー管理、バウンスや苦情への対応について説明します。

ウェルカムメール、パスワードリセット、注文確認、通知ダイジェスト、マーケティングキャンペーン、またはシステムが生成するメールを送信する際に、このスキルを使用してください。また、メールの到達率に関する問題のデバッグや、メールプロバイダー間の移行にも使用してください。


基本原則

  1. トランザクションメールとマーケティングメールは別物です - トランザクションメール(パスワードリセット、領収書)は、即座に確実に届く必要があります。マーケティングメール(ニュースレター、プロモーション)はまとめて送信でき、配信停止リンクを設ける必要があります。同じ送信ドメインで両者を混在させないでください。
  2. HTML メールはウェブ開発とは異なります - メールクライアントは 2004 年当時の HTML をレンダリングします。フレックスボックス、グリッド、最新の CSS は使えません。レイアウトにはテーブルを使い、スタイルはインラインで指定し、複数のクライアントでテストしてください。React Email と MJML を使えば、こうした手間を軽減できます。
  3. 到達率は積み重ねて獲得するものです - SPF、DKIM、DMARC は必須条件であり、保証ではありません。バウンス率(2% 未満)と苦情率(0.1% 未満)を低く維持し、新しい送信ドメインは段階的にウォームアップしてください。
  4. すべてキューに入れてください - リクエストハンドラー内でメールを同期的に送信しないでください。ユーザーのリクエストをブロックせず、失敗時に再試行できるよう、メール送信をキューに入れてください。
  5. 送信前にテストしてください - 開発環境では Mailtrap または Ethereal を使用し、複数のクライアント(Litmus、Email on Acid)でプレビューして、すべてのリンクが機能することを確認してください。

主なパターン

パターン 1:Resend を使った React Email テンプレート

使用する場面: 最新の開発体験と信頼性の高い配信を実現する、型安全なコンポーネントベースのメールテンプレートを構築する場合。

実装:

// emails/welcome.tsx - React Email template
import {
  Html,
  Head,
  Body,
  Container,
  Section,
  Text,
  Button,
  Img,
  Hr,
  Link,
  Preview,
} from "@react-email/components";

interface WelcomeEmailProps {
  userName: string;
  loginUrl: string;
  guideUrl: string;
}

export function WelcomeEmail({ userName, loginUrl, guideUrl }: WelcomeEmailProps) {
  return (
    <Html lang="en">
      <Head />
      <Preview>Welcome to MyApp, {userName}! Here&apos;s how to get started.</Preview>
      <Body style={styles.body}>
        <Container style={styles.container}>
          <Img
            src="https://myapp.com/logo.png"
            width={120}
            height={36}
            alt="MyApp"
          />

          <Section style={styles.section}>
            <Text style={styles.heading}>Welcome, {userName}!</Text>
            <Text style={styles.text}>
              Thanks for signing up. Your account is ready to go.
              Here are three things to get you started:
            </Text>

            <Text style={styles.listItem}>
              <strong>1. Create your first project</strong> - Click "New Project" on your dashboard.
            </Text>
            <Text style={styles.listItem}>
              <strong>2. Invite your team</strong> - Share your workspace with colleagues.
            </Text>
            <Text style={styles.listItem}>
              <strong>3. Explore the guide</strong> - Learn tips and best practices.
            </Text>

            <Section style={styles.buttonContainer}>
              <Button style={styles.button} href={loginUrl}>
                Go to Dashboard
              </Button>
            </Section>
          </Section>

          <Hr style={styles.hr} />

          <Section style={styles.footer}>
            <Text style={styles.footerText}>
              Need help? Reply to this email or visit our{" "}
              <Link href={guideUrl} style={styles.link}>help center</Link>.
            </Text>
            <Text style={styles.footerText}>
              MyApp, Inc. | 123 Street, City, ST 12345
            </Text>
          </Section>
        </Container>
      </Body>
    </Html>
  );
}

const styles = {
  body: {
    backgroundColor: "#f6f9fc",
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
  },
  container: {
    backgroundColor: "#ffffff",
    margin: "0 auto",
    padding: "20px 0 48px",
    maxWidth: "600px",
    borderRadius: "8px",
  },
  section: { padding: "0 48px" },
  heading: { fontSize: "24px", fontWeight: "bold" as const, marginBottom: "16px" },
  text: { fontSize: "16px", lineHeight: "26px", color: "#404040" },
  listItem: { fontSize: "15px", lineHeight: "24px", color: "#404040", marginBottom: "8px" },
  buttonContainer: { textAlign: "center" as const, marginTop: "24px", marginBottom: "24px" },
  button: {
    backgroundColor: "#0066ff",
    borderRadius: "6px",
    color: "#ffffff",
    fontSize: "16px",
    fontWeight: "bold" as const,
    textDecoration: "none",
    textAlign: "center" as const,
    display: "inline-block",
    padding: "12px 24px",
  },
  hr: { borderColor: "#e6ebf1", margin: "32px 0" },
  footer: { padding: "0 48px" },
  footerText: { fontSize: "12px", color: "#8898aa", lineHeight: "20px" },
  link: { color: "#0066ff" },
};

// Default props for preview
WelcomeEmail.PreviewProps = {
  userName: "Jane",
  loginUrl: "https://myapp.com/dashboard",
  guideUrl: "https://myapp.com/guide",
} satisfies WelcomeEmailProps;

export default WelcomeEmail;
// lib/email.ts - Email sending service
import { Resend } from "resend";
import { WelcomeEmail } from "@/emails/welcome";
import { PasswordResetEmail } from "@/emails/password-reset";

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

// Type-safe email sending
type EmailTemplate =
  | { template: "welcome"; props: { userName: string; loginUrl: string; guideUrl: string } }
  | { template: "password-reset"; props: { resetUrl: string; expiresIn: string } }
  | { template: "invoice"; props: { invoiceUrl: string; amount: string; dueDate: string } };

const templates: Record<string, React.FC<Record<string, unknown>>> = {
  welcome: WelcomeEmail,
  "password-reset": PasswordResetEmail,
};

const subjects: Record<string, (props: Record<string, unknown>) => string> = {
  welcome: () => "Welcome to MyApp!",
  "password-reset": () => "Reset your password",
  invoice: (p) => `Invoice for $${p.amount}`,
};

async function sendEmail(
  to: string,
  email: EmailTemplate
): Promise<{ id: string }> {
  const Template = templates[email.template];
  const subject = subjects[email.template](email.props);

  const { data, error } = await resend.emails.send({
    from: "MyApp <[email protected]>",
    to,
    subject,
    react: Template(email.props),
  });

  if (error) {
    throw new EmailSendError(error.message, { to, template: email.template });
  }

  return { id: data!.id };
}

export { sendEmail };

理由: React Email は、コンポーネントベースのメールテンプレート、TypeScript による型安全性、開発中のホットリロード、メール対応 HTML への自動変換を提供します。Resend は、高い到達率、シンプルな API、React Email との標準統合を提供します。型安全なテンプレート定義により、必要なプロパティが欠けたメールの送信を防げます。


パターン 2:再試行ロジック付きのメールキュー

使用する場面: 本番環境でメールを送信する場合全般。リクエストハンドラー内でメールを同期的に送信しないでください。

実装:

// jobs/email.ts - Background email processing
import { Queue, Worker } from "bullmq";
import { Redis } from "ioredis";
import { sendEmail } from "@/lib/email";

const connection = new Redis(process.env.REDIS_URL!, { maxRetriesPerRequest: null });

// Define the email queue
const emailQueue = new Queue("email", {
  connection,
  defaultJobOptions: {
    attempts: 3,
    backoff: {
      type: "exponential",
      delay: 60_000, // 1 min, then 2 min, then 4 min
    },
    removeOnComplete: { age: 7 * 24 * 60 * 60 }, // Keep completed jobs for 7 days
    removeOnFail: { age: 30 * 24 * 60 * 60 }, // Keep failed jobs for 30 days
  },
});

// Queue an email (call this from your application code)
async function queueEmail(
  to: string,
  template: EmailTemplate,
  options?: { delay?: number; priority?: number }
): Promise<string> {
  const job = await emailQueue.add(
    template.template,
    { to, template },
    {
      delay: options?.delay,
      priority: options?.priority ?? 0,
    }
  );
  return job.id!;
}

// Process emails in the background
const emailWorker = new Worker(
  "email",
  async (job) => {
    const { to, template } = job.data;

    try {
      const result = await sendEmail(to, template);
      return result;
    } catch (error) {
      // Log for monitoring
      console.error(`Email send failed (attempt ${job.attemptsMade + 1}/${job.opts.attempts}):`, {
        to,
        template: template.template,
        error: (error as Error).message,
      });
      throw error; // BullMQ will retry based on backoff config
    }
  },
  {
    connection,
    concurrency: 10, // Process 10 emails concurrently
    limiter: {
      max: 100, // Max 100 emails per minute (respect ESP rate limits)
      duration: 60_000,
    },
  }
);

// Listen for permanent failures
emailWorker.on("failed", (job, error) => {
  if (job && job.attemptsMade >= (job.opts.attempts ?? 3)) {
    // All retries exhausted - alert and log
    alertOps(`Email permanently failed: ${job.data.to} - ${error.message}`);
  }
});

export { queueEmail };
// Usage in application code
async function handleUserSignup(user: User) {
  // Create user in database (synchronous, in request)
  await db.user.create({ data: user });

  // Queue welcome email (async, background)
  await queueEmail(user.email, {
    template: "welcome",
    props: {
      userName: user.name,
      loginUrl: `${process.env.APP_URL}/dashboard`,
      guideUrl: `${process.env.APP_URL}/guide`,
    },
  });
}

理由: メールをキューに入れることで、配信処理をユーザーのリクエストから切り離せます。メールプロバイダーの応答が遅い場合や停止している場合でも、ユーザーのサインアップ処理は成功します。指数バックオフで一時的な障害に対応できます。レート制限により、ESP のクォータ超過を防げます。ジョブを保持することで、配信問題のデバッグが可能になります。


パターン 3:到達率向上のための DNS 設定

使用する場面: 新しい送信ドメインを設定する場合や、メールの到達率に関する問題を診断する場合。

実装:

# SPF Record - Authorize sending servers
# Add to DNS as TXT record on your domain
myapp.com.  TXT  "v=spf1 include:_spf.resend.com include:amazonses.com ~all"

# DKIM Record - Cryptographic email signing
# Provider-specific, usually a CNAME record
resend._domainkey.myapp.com.  CNAME  resend._domainkey.example.com.

# DMARC Record - Alignment policy
# Start with p=none (monitor), move to p=quarantine, then p=reject
_dmarc.myapp.com.  TXT  "v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100; adkim=s; aspf=s"

# Return-Path / Bounce domain (custom envelope sender)
bounce.myapp.com.  MX  10  feedback-smtp.us-east-1.amazonses.com.
bounce.myapp.com.  TXT  "v=spf1 include:amazonses.com ~all"
// Verify DNS configuration programmatically
import { resolveTxt, resolveCname, resolveMx } from "dns/promises";

async function verifyEmailDNS(domain: string): Promise<Record<string, boolean>> {
  const results: Record<string, boolean> = {};

  // Check SPF
  try {
    const txt = await resolveTxt(domain);
    const spf = txt.flat().find((r) => r.startsWith("v=spf1"));
    results.spf = !!spf;
  } catch {
    results.spf = false;
  }

  // Check DMARC
  try {
    const txt = await resolveTxt(`_dmarc.${domain}`);
    const dmarc = txt.flat().find((r) => r.startsWith("v=DMARC1"));
    results.dmarc = !!dmarc;
  } catch {
    results.dmarc = false;
  }

  // Check DKIM (provider-specific selector)
  try {
    const cname = await resolveCname(`resend._domainkey.${domain}`);
    results.dkim = cname.length > 0;
  } catch {
    results.dkim = false;
  }

  return results;
}

理由: メール認証(SPF、DKIM、DMARC)により、受信側のメールサーバーにメールの正当性を証明できます。これらが設定されていないと、メールは迷惑メールフォルダーに振り分けられます。SPF は、ドメインからの送信を許可するサーバーを指定します。DKIM は、メッセージに暗号署名を付与します。DMARC は、認証されていないメールをどう扱うか受信側に指示します。


パターン 4:バウンスと苦情への対応

使用する場面: 送信者の評判と到達率を長期的に維持する場合。

実装:

// Webhook handler for bounces and complaints (SES/Resend/SendGrid)
async function handleEmailEvent(event: EmailEvent): Promise<void> {
  switch (event.type) {
    case "bounce": {
      const { email, bounceType } = event;

      if (bounceType === "permanent") {
        // Hard bounce - never send to this address again
        await db.emailSuppression.upsert({
          where: { email },
          create: { email, reason: "hard_bounce", suppressedAt: new Date() },
          update: { reason: "hard_bounce", suppressedAt: new Date() },
        });

        // Update user record
        await db.user.updateMany({
          where: { email },
          data: { emailVerified: false, emailBounced: true },
        });
      } else {
        // Soft bounce - retry later, suppress after 3 soft bounces
        const count = await db.emailEvent.count({
          where: { email, type: "soft_bounce", createdAt: { gte: thirtyDaysAgo() } },
        });

        if (count >= 3) {
          await db.emailSuppression.create({
            data: { email, reason: "repeated_soft_bounce", suppressedAt: new Date() },
          });
        }
      }
      break;
    }

    case "complaint": {
      // Spam complaint - suppress immediately
      await db.emailSuppression.upsert({
        where: { email: event.email },
        create: { email: event.email, reason: "complaint", suppressedAt: new Date() },
        update: { reason: "complaint", suppressedAt: new Date() },
      });

      // Remove from all marketing lists
      await db.marketingSubscription.updateMany({
        where: { email: event.email },
        data: { unsubscribedAt: new Date(), reason: "spam_complaint" },
      });
      break;
    }
  }
}

// Check suppression list before sending
async function canSendTo(email: string): Promise<boolean> {
  const suppressed = await db.emailSuppression.findUnique({
    where: { email },
  });
  return !suppressed;
}

理由: ESP はバウンス率と苦情率を監視しています。しきい値(バウンス率が 5% 超、苦情率が 0.1% 超)を超えると、送信が停止されることがあります。抑制リストを使うと、送信に適さないことが分かっているアドレスへの送信を防ぎ、送信者の評判を守るとともに、正当なメールが受信トレイに届くようにできます。


メールプロバイダーの比較

プロバイダー最適な用途料金React Email主な機能
Resend開発者に使いやすいトランザクションメール1日100通まで無料、50,000通で月額$20ネイティブ対応優れた開発者体験、React Email チーム
AWS SES大量配信、コスト最適化1,000通あたり$0.10レンダリング経由大規模利用時に最安
SendGridマーケティングとトランザクションメール1日100通まで無料レンダリング経由マーケティング自動化
Postmark信頼性の高いトランザクションメール10,000通で月額$15レンダリング経由トランザクションメールの到達率が最良

アンチパターン

アンチパターン問題点より良い方法
リクエストハンドラー内でメールを送信するユーザーへの応答が遅れ、再試行もできませんバックグラウンドワーカーを使ったキュー
トランザクションメールとマーケティングメールで同じドメインを使うマーケティングメールの評価がパスワードリセットメールに影響しますサブドメインを分けます(mail.app.com と news.app.com)
抑制リストがないバウンスが繰り返されると送信者の評価が損なわれます送信前に必ず抑制リストを確認し、維持します
メール HTML で <div> レイアウトを使うOutlook や Gmail で表示が崩れます<table> レイアウト、または React Email/MJML を使います
通知メールに配信停止リンクがないCAN-SPAM 違反や迷惑メールの苦情につながりますワンクリック配信停止ヘッダーと、見える場所にリンクを設置します
実在するメールアドレスでテストする実在する人への迷惑メール送信や苦情につながりますMailtrap、Ethereal、または Resend のテストモードを使います
no-reply アドレスから送信するユーザーが返信できず、よそよそしい印象になります監視されている返信先アドレスを使います

チェックリスト

  • 送信ドメインに SPF レコードを設定済み
  • DKIM 署名を有効化し、検証済み
  • DMARC ポリシーを設定済み(p=none から開始し、p=quarantine へ段階的に移行)
  • Gmail、Outlook、Apple Mail、Yahoo でメールテンプレートをテスト済み
  • メールは再試行ロジックを備えたキューを通じて送信
  • バウンスと苦情の Webhook を設定済み
  • 送信前に毎回、抑制リストを確認
  • すべての通知メール/マーケティングメールに配信停止リンクを設置
  • トランザクションメールとマーケティングメールで送信ドメインを分離
  • 開発環境でテストモード(Mailtrap/Ethereal)を使用
  • ESP のクォータ内に収まるようレート制限を設定
  • Return-Path/バウンスドメインを設定済み

関連リソース

  • スキル: authentication-patterns(パスワードリセットメール)、payment-integration(請求書/領収書メール)
  • スキル: event-driven-architecture(非同期イベントとしてのメール)
  • ルール: docs/reference/stacks/react-typescript.md(React Email のコンポーネントパターン)

コメント