+ 作るログイン
図を読み込んでいます…
1 / 14
メールのしくみ ― DNS から SPF・DKIM・DMARC、Resend の運用まで / 全11章

メールのしくみ 11:運用とつまずき

発行
アニメモ公式
版
第1版
改訂
コマ
全14

送り始めてから、評判と利用者を守るためにやることを説明します。リンクと開封の追跡を切る理由、冪等キーと DB の記録、DB とメールを1つのトランザクションにしない理由、再送とトークン、Webhook とその署名、テスト用のアドレス、よくあるつまずき(From を gmail.com にする・判定結果の読み方)、Animemo で確かめることまで。シリーズの最終回です。

送り始めてからが、もう1つの本番

9本目で、Resend に任せられないことは2つある、と説明しました。1つ目の DNS の設定は、10本目で済ませました。

この作品は、2つ目の運用です。メールを送り始めてから、評判と利用者を守るためにやることを見ていきます。

大きく分けると、次の3つです。

やることなぜ
リンクを書き換えさせない認証や招待のリンクを壊さないため
二重に送らない。メールは取り消せないと知っておく利用者を混乱させないため
届かない宛先・嫌がる宛先に送り続けない評判を守るため(8本目)

最後に、よくあるつまずきと、Animemo で実際に確かめることをまとめます。このシリーズの最終回です。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 1 / 14
  • 送り始めてからが、もう1つの本番
  • DNS の設定
  • 運用
  • 10本目で済んだ
  • この作品
  • リンクを書き換えさせない
  • 二重に送らない・取り消せないと知る
  • 届かない宛先に送り続けない
  • 借りられないものの2つ目。評判と利用者を守る運用。

認証や招待のメールでは、リンクの追跡を切る

メール配信サービスの多くにはクリック追跡の機能があります。Resend にもあります。メールの中のリンクを、いったん計測用のドメインを経由する URL に書き換え、誰がいつクリックしたかを記録するものです。宣伝メールの効果を測るには便利ですが、登録確認や招待のメールでは切っておきます。

理由の1つは、書き換えでリンクの見た目と行き先のドメインが違う状態になることです。8本目で見たとおり、これはフィッシングメールの典型的な形で、迷惑メール判定で疑われやすくなります。受け取った人から見ても、「animemo.app のメールなのに、リンクは知らないドメイン」は不安を誘います。

もう1つは、経由先の都合で認証用のリンクが壊れることがあるためです。Supabase の資料も、認証メールではリンク追跡を無効にするよう勧めています。Animemo の手順書にも「リンク追跡は認証リンクを書き換えることがあるため無効にする」とあります。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 2 / 14
  • 認証や招待のメールでは、リンクの追跡を切る
  • 元のリンク
  • https://animemo.app/invite/…(招待・確認のリンク)
  • クリック追跡を有効にすると書き換わる
  • https://(計測用のドメイン)/click/…
  • 表示と行き先のドメインが違う = フィッシングと同じ形(8本目)。 経由先の都合で、認証のリンクが壊れることもある。
  • 追跡はリンクを別ドメイン経由に書き換える。認証のリンクではそれが害になる。

開封の追跡も、認証のメールには要らない

もう1つの追跡の機能が、開封の追跡です。

仕組みは単純です。メールの本文に、目に見えない 1×1 の小さな画像を埋め込んでおきます。受け取った人がメールを開くと、メールソフトがその画像を取りに来るので、「開いた」と数えられます。

ただし、この数え方は正確ではありません。画像を自動で読み込まない設定の人は、開いても数えられません。逆に、プライバシー保護のために画像を先読みするメールソフトでは、開いていなくても数えられることがあります。

登録確認やパスワード再設定のメールで、開いたかどうかを知る必要はほとんどありません。必要のない追跡は切っておくのが、受け取る人にとっても親切です。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 3 / 14
  • 開封の追跡も、認証のメールには要らない
  • 開くと画像を取りに来る → 開いたと数える 画像を読まない設定の人は 数えられない。 認証メールで知る必要も無い。
  • ↑ 1×1 の見えない画像
  • 見えない小さな画像を読み込ませて数える仕組み。必要が無ければ切っておく。

「送れたか分からない」ときのために、冪等キーを付ける

API で送るとき、通信が途中で切れて返事が来ないことがあります。このとき、Resend はメールを受け取ったのか、受け取っていないのか、こちらからは分かりません。

受け取っていなかったなら、送り直す必要があります。でも、実は受け取っていたなら、送り直すと太郎さんに同じ招待が2通届きます。

これを解くのが冪等キー(Idempotency Key)です。「冪等」は、同じ操作を何度繰り返しても結果が1回と同じ、という意味です。

Idempotency-Key: organization-invite/42

リクエストにこのヘッダーを付けておくと、Resend は同じキーのリクエストを覚えていて、2回目以降は新しく送らず、1回目の結果を返します。だから、返事が来なければ安心して同じキーで送り直せます。

キーには、招待の ID のようにそのメールに固有の値を使います。Animemo の手順書では organization-invite/{invitation-id} のような形が提案されています。冪等キーは、HTTP API だけでなく SMTP の入口でも使えます。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 4 / 14
  • 「送れたか分からない」ときのために、冪等キーを付ける
  • 1回目(応答が来ない)
  • Fastify
  • Resend
  • 受信トレイ
  • 招待を送りたい
  • 1通だけ
  • もう一度(同じキー)
  • Idempotency-Key: organization-invite/42
  • 同じキーで2回頼んでも、メールは1通だけ。

キーの記憶は24時間。だから DB にも記録を残す

Resend が冪等キーを覚えているのは 24時間です。それを過ぎると、同じキーでも新しいメールとして送られます。

そこで、Animemo の手順書では、招待の行に送信の記録も持たせる設計になっています。

列中身
provider_message_idResend が返したメールの id
last_sent_at最後に送った時刻
delivery_status配送の状態(届いた・戻った・苦情・失敗)

この記録があれば、24時間を過ぎた後でも「もう送ったか」を自分で判断できます。また、次のページ以降で見る Webhook の知らせが届いたとき、provider_message_id で「どの招待のメールの話か」を照らし合わせられます。

外部のサービスが覚えていてくれることに頼り切らず、大事なことは自分の DB にも残す。外部サービスと組み合わせるときの基本です。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 5 / 14
  • キーの記憶は24時間。だから DB にも記録を残す
  • invitations の行(手順書の案)
  • provider_message_id 4ef9…(Resend が返した id)
  • last_sent_at 2026-09-24 10:00
  • delivery_status delivered / bounced / complained / failed
  • 24時間を過ぎた再送や、後から届く知らせとの照合は、この記録で行う
  • 送ったメールの id・送った時刻・配送の状態を、招待の行と一緒に持つ。

DB の保存とメール送信を、1つのトランザクションにしない

招待メールの処理は、「① 招待を DB に保存する」と「② メールを送る」の2段階です。ここで、「両方まとめて1つのトランザクションにして、メールが失敗したら DB も巻き戻そう」と考えたくなります。

これはやめておきます。メールは、一度送ったら取り消せないからです。

たとえば「メールは相手に届いたが、Resend からの返事が来る前に通信が切れた」とします。コードは失敗だと判断して DB を巻き戻します。すると太郎さんの手元には、DB に存在しない招待のリンクが届いていることになります。リンクを開いても「招待が見つかりません」になります。

そこで順番を分けます。招待は先に DB に確定させます。メールの送信に失敗したら、招待は残したまま「招待は作成したが、メールの送信に失敗しました。再送できます」と画面に出します。再送は冪等キーで安全に行えます。

DB は巻き戻せても、外の世界に出たものは巻き戻せません。これはメールに限らず、決済や外部 API を呼ぶ処理全般に当てはまる考え方です。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 6 / 14
  • DB の保存とメール送信を、1つのトランザクションにしない
  • ① 招待を保存
  • ② メールを送る
  • 招待は残す
  • DB に書く
  • Resend へ
  • 「再送できます」と出す
  • 1つにまとめると、「DB は巻き戻ったのに、 メールだけは届いていた」が起こりうる
  • メールは取り消せない。招待は先に確定させ、送信の失敗は再送で扱う。

再送するときは、リンクの鍵(トークン)を新しくする

招待メールを再送するときにも、気をつけることがあります。Animemo の手順書では、再送のたびにトークンを新しくし、古いトークンを無効にする方針になっています。

招待のリンク(/invite/:token)に含まれるトークンは、「このリンクを持っている人は招待を受けられる」という鍵です。再送のたびに同じトークンを使い回すと、1通目のメールがどこかに転送されていたり、誤った宛先に届いていたりした場合でも、そのリンクが有効なまま残ります。

新しいトークンにすれば、有効なリンクは常に最新の1つだけになります。

もう1つの約束として、DB にはトークンの平文を保存せず、SHA-256 のハッシュだけを置きます。6本目で見たハッシュの性質(ハッシュから元の値は作れない)のおかげで、万一 DB の中身が漏れても、そこからリンクを作ることはできません。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 7 / 14
  • 再送するときは、リンクの鍵(トークン)を新しくする
  • 1通目のリンク: /invite/aaa…
  • 有効なリンクは常に1つ。 DB にはトークンの SHA-256 だけを置く (平文は残さない)。
  • 再送のリンク: /invite/bbb…
  • 古いメールのリンクは使えなくする。どこかに転送されていても安全。

届かなかった・迷惑と言われた、を受け取る

送ったメールがその後どうなったかは、Resend が Webhook で知らせてくれます。Webhook は、何か起きたときに相手のサービスが、こちらの URL へ HTTP で通知を送ってくる仕組みです。

知らせ意味こちらの対応
email.delivered相手のサーバーが受け取った配送の状態を「届いた」に
email.delivery_delayed一時的に届かず、再送中様子を見る
email.bounced届かなかった(宛先が無いなど)その宛先へは自動で送らない
email.complained受け取った人が迷惑メールとして報告したその宛先へは送らない

大事なのは下の2つです。8本目で見たとおり、存在しない宛先や、迷惑だと言った人に送り続けると、評判が下がり、他の人あてのメールまで届かなくなります。Animemo の手順書にも「恒久的なバウンスと苦情の宛先には自動で再送しない」とあります。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 8 / 14
  • 届かなかった・迷惑と言われた、を受け取る
  • Resend
  • Webhook
  • Fastify
  • 送らない宛先
  • 配送の状態を更新
  • もう自動では送らない
  • email.delivered
  • email.bounced
  • email.complained
  • email.delivery_delayed
  • 届く知らせの例
  • Resend が Webhook で知らせてくれる。バウンスと苦情の宛先には送らない。

Webhook は、署名を確かめてから読む

Webhook を受け取る URL は、インターネットに公開されています。つまり、誰でもそこに偽の知らせを送れます。「このアドレスは bounced だった」という嘘の知らせで、正しい利用者へのメールを止めさせることもできてしまいます。

そこで、Resend は知らせに署名を付けて送ってきます(Resend は Svix という仕組みを使っていて、svix-id・svix-timestamp・svix-signature のヘッダーが付きます)。こちらは、Resend の管理画面で発行される秘密の値を使って署名を確かめ、合わないものは捨てます。

考え方は、6本目の DKIM や、Animemo の Stripe の Webhook とまったく同じです。そして、Stripe のときと同じ注意がここでも必要です。署名は、届いたままの本文(rawBody)に対して確かめます。 JSON として読み込んでから整形し直すと、1文字でも変われば署名は合わなくなります。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 9 / 14
  • Webhook は、署名を確かめてから読む
  • 偽の知らせ
  • 署名の検証
  • 処理
  • 「bounced」と嘘をつく
  • 秘密の値で確かめる
  • 検証には、届いたままの本文(rawBody)を使う。 整形し直すと署名が合わなくなる
  • URL は誰でも叩ける。署名が合わない知らせは捨てる。Stripe の Webhook と同じ。

テストでは、でたらめなアドレスに送らない

動作確認のとき、[email protected] のような適当なアドレスに送りたくなります。でも、実在しない宛先に送ると、それは本物のバウンスとして数えられ、8本目の評判が下がります。

Resend には、試験用のアドレスが用意されています。

宛先起きること
[email protected]届いた扱いになる
[email protected]バウンスした扱いになる
[email protected]苦情が来た扱いになる

これらを使えば、評判を傷つけずに、この作品で見た Webhook の処理まで一通り試せます。

手元で Supabase を動かしているときは、Supabase CLI に付いている Mailpit で、送られたメールをブラウザで見られます。実際には外へ送られないので、何通送っても大丈夫です。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 10 / 14
  • テストでは、でたらめなアドレスに送らない
  • [email protected] など → 本物のバウンスになり、評判が下がる
  • [email protected] → 届いた扱い
  • 手元では Supabase CLI の Mailpit で見る
  • [email protected] → バウンス扱い
  • [email protected] → 苦情扱い
  • 実在しない宛先は本物のバウンスになる。Resend の試験用アドレスを使う。

From を @gmail.com にはできない

ここからは、よくあるつまずきです。

「差出人を自分の @gmail.com のアドレスにして Resend から送りたい」は、できません。

まず Resend 自体が、DNS で確認済みのドメインからしか送らせてくれません。gmail.com の掲示板に書けるのは Google だけなので、あなたが確認を通すことはできません。

仮に別の方法で送れたとしても、7本目の DMARC で止まります。From: のドメインは gmail.com なのに、SPF も DKIM も gmail.com として合格させる手段がありません。一致が取れないので、迷惑メール行きか、拒否になります。

これは不便に見えますが、4〜7本目で作った仕組みが正しく働いている結果です。自分が持っていないドメインを名乗れないことこそが、なりすましを防いでいます。メールを送るなら、自分のドメインを持つのが出発点です。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 11 / 14
  • From を @gmail.com にはできない
  • Resend
  • 確認済みのドメインからしか送らない
  • From: [email protected]
  • 仮に送れても、gmail.com の持ち主は Google。 DMARC の一致が取れない(7本目)。
  • 持っていないドメインは、SPF も DKIM も揃えられない。

迷惑メールに入ったら、まず判定結果を読む

設定したはずなのに迷惑メールフォルダに入る。そんなときに当て推量で設定をいじる前に、受け取った側の判定結果を見ます。

Gmail でそのメールを開き、メニューから「メッセージのソースを表示」を選ぶと、生のヘッダーの中に次のような行があります。

Authentication-Results: mx.google.com;
  spf=pass smtp.mailfrom=send.mail.animemo.app;
  dkim=pass [email protected] header.s=resend;
  dmarc=pass (p=NONE) header.from=mail.animemo.app
表示次に見る場所
spf=failsend.mail の TXT。SPF の行が2つになっていないか(5本目)
dkim=failresend._domainkey.mail の値。途中でメールを書き換える仕組みを通っていないか(6本目)
dmarc=failFrom: のドメインが、SPF・DKIM の合格したドメインと揃っているか(7本目)

3つとも pass なのに迷惑メールに入るなら、原因は8本目の評判か、本文の中身のほうです。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 12 / 14
  • 迷惑メールに入ったら、まず判定結果を読む
  • Authentication-Results: mx.google.com;
  • spf=pass smtp.mailfrom=send.mail.animemo.app;
  • dkim=pass [email protected] header.s=resend;
  • dmarc=pass (p=NONE) header.from=mail.animemo.app
  • spf=fail
  • send.mail の TXT・2行になっていないか
  • dkim=fail
  • resend._domainkey.mail の値・途中の書き換え
  • 全部 pass なのに
  • 評判か中身(8本目)
  • 当て推量で設定をいじらない。FAIL の行から直す。

Animemo で確かめること

最後に、Animemo の手順書(docs/operations/smtp-and-invitation-email.md)にある受け入れ確認を、このシリーズの言葉で読み直します。

確かめることこのシリーズとの関係
新しいアドレス:招待 → 登録 → 招待の URL に戻る → 承認Supabase Auth の確認メールが届くこと(9本目)
既存の利用者:招待 → ログイン → 承認招待メールが届き、リンクが壊れていないこと(この作品)
別のアドレスでログイン:EMAIL_MISMATCH招待されたアドレスの持ち主だけが受けられること
期限切れ・失効・使用済み:承認できない古いトークンが無効になっていること(この作品)
同じ人へ再招待:新しいリンクだけが有効で、メールは重複しないトークンの入れ替えと冪等キー(この作品)
Gmail・iCloud・Outlook で受信・迷惑判定・リンク身元確認と評判(5〜8本目)

表の右側を見ると、どの確認も、このシリーズのどこかで「なぜそうするのか」を見てきたものだと分かります。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 13 / 14
  • Animemo で確かめること
  • 新規のアドレス:招待 → 登録 → 招待の URL に戻る → 承認
  • 既存の利用者:招待 → ログイン → 承認
  • 別のアドレスでログイン:EMAIL_MISMATCH で断られる
  • 期限切れ・失効・使用済み:承認できない
  • 同じ人へ再招待:新しいリンクだけが有効・二重に届かない
  • Gmail・iCloud・Outlook で受信・迷惑判定・リンク
  • 新規・既存・別アドレス・期限切れ・再招待、そして3つのメールソフト。

シリーズのまとめ:1本の流れで見る

全11本を、1本の流れでまとめます。

インターネットの通信は番号あてに届き、人間は名前(ドメイン)を使います(1本目)。名前から番号を引く DNS は、持ち主しか書けず、誰でも読める掲示板です(2本目)。

メールは SMTP でサーバーからサーバーへ運ばれ、宛先は DNS の MX で探します。差出人は封筒と手紙の2つがあり、どちらも自由に書けます(3本目)。1982年生まれの SMTP は差出人を確かめないので、誰でも誰にでもなれます(4本目)。

そこで、DNS に宣言を置いて受け取る側が照らし合わせる形で、身元確認が後から足されました。SPF は送信元の一覧(5本目)、DKIM は電子署名(6本目)、DMARC は画面の From と揃っているか(7本目)。3つそろって初めて穴が塞がります。

それでも、身元が本物なだけでは届きません。受け取る側は評判を見ています(8本目)。

評判の良い送信網と、署名・再送・バウンス処理をまとめて借りる先が Resend です(9本目)。自分がやることは、突き詰めると2つだけです。DNS で宣言すること(10本目)と、バウンスや苦情を無視しない運用(11本目)です。

最初に「メールは、あとから身元確認を継ぎ足した仕組み」と書きました。ここまで読んだ今なら、その一文の意味がすべて分かるはずです。

  • メールのしくみ 11 / 11 ― 運用とつまずき
  • 14 / 14
  • シリーズのまとめ:1本の流れで見る
  • 1 住所
  • 2 DNS
  • 3 SMTP
  • 4 欠陥
  • IP・ドメイン
  • 持ち主だけが書ける
  • 差出人は2つ
  • 名乗り放題
  • 5 SPF
  • 6 DKIM
  • 7 DMARC
  • 8 評判
  • 送信元の一覧
  • 電子署名
  • From と揃う
  • 本物でも届かない
  • 9 Resend
  • 10 DNS 設定
  • 11 運用
  • 借りる理由
  • 4行を書く
  • 届け続ける
  • 自分がやるのは、DNS で宣言することと、バウンス・苦情を無視しないこと。

0 件のコメント

読み込み中…

権利侵害・不適切な内容の連絡: [email protected]