はじめに
「登録ありがとうございます」のメールが、なぜか半分くらいのユーザーに届いていませんでした。
しかもエラーは出ていません。アプリのログ上は「送信成功」。SMTPも 250 OK を返している。なのに届かない。問い合わせ窓口には「メール来ないんですけど」が積み上がっていく——。
これは、自社のWebサービスでメール到達率がじわじわ悪化し、最終的にSendGridへの移行とドメイン認証の整備で到達率を約78%→99%まで戻したときの記録です。
- 登録確認メールや決済完了メールを自前のSMTPやレンタルサーバーから送っている方
- 「うちのメール、Gmailだと迷惑メールに入る気がする」とうっすら感じている方
- SPF / DKIM / DMARC を「名前は聞くけど設定したことはない」方
こういう方にとって、明日から使えるチェックリストになるはずです。先に結論だけ置いておきます。
届かない原因のほとんどは「送信ドメイン認証の不備」と「2024年からのGmail・Yahooの新ルール未対応」。アプリ側のコードはほぼ無実。
事件は本番リリース直後に起きた
開発中、メールはちゃんと届いていました。自分のGmailにも、テスト用のYahooアカウントにも、確認メールはきれいに届く。「メールなんて送るだけだろ」とタカをくくっていました。
ところがリリースから3日後、CSチームからこんな報告が来ます。
「新規登録したのに確認メールが来ない、というお問い合わせが今日だけで12件です」
最初は「迷惑メールフォルダ見てください」で済むと思っていました。でも、ユーザーに確認してもらうと迷惑メールフォルダにすら入っていない。完全に消えているケースが多発していました。
送信側のログを見ても、エラーは一切なし。ここで初めて、自分が「メール送信」をまったく理解していなかったことに気づきます。
まず原因を切り分ける
メールが届かない問題は、犯人候補が多すぎて手当たり次第にやると沼ります。なので、メールがユーザーに届くまでの流れを図にして、どこで消えうるのかを整理しました。
注目すべきは一番下の 「黙って破棄(サイレント拒否)」 です。Gmailのような大手は、信頼できない送信元のメールを「迷惑メールにすら入れず、エラーも返さず、静かに捨てる」ことがあります。送信側からは成功に見えるのに届かない、の正体はこれでした。
そして「信頼できる送信元かどうか」を受信側が判断する材料が、送信ドメイン認証(SPF / DKIM / DMARC) です。自分のドメインを dig で確認したところ——
$ dig TXT example.com +short
# → 何も返ってこない(SPFすら無い)
$ dig TXT _dmarc.example.com +short
# → 同上(DMARCも無い)
見事にスカでした。つまり受信側から見ると、うちのメールは「身元不明の人がうちのドメインを名乗って送ってきている」状態。Gmailに警戒されて当然です。
追い打ち:Gmail・Yahooの新しい送信者ガイドライン
調査を進めるうちに、より根本的な事実にぶつかります。2024年2月以降、GmailとYahooは大量送信者に対するルールを大幅に厳格化していました。 知らずに従来通りの運用を続けていたら、到達率が落ちるのは必然でした。
主な要件はこの3つです。
| 要件 | 内容 |
|---|---|
| メール認証 | SPF と DKIM の両方を設定。さらに DMARC を必須化 |
| ワンクリック登録解除 | 宣伝系メールに1クリックで解除できる仕組み(List-Unsubscribe)を付ける |
| 迷惑メール率 | スパム報告率を 0.3%未満(理想は0.1%未満)に抑える |
「1日5000通も送ってないからうちは関係ない」と思いがちですが、これは閾値以下でも基本指針として効いてきます。特にSPF/DKIM/DMARCは、もはや「あって当たり前」のパスポートです。無いと受信トレイの入り口で止められます。
ここまでで方針が固まりました。
- 自前運用をやめ、到達率に強いSendGridに送信を寄せる
- ドメイン認証(SPF / DKIM / DMARC)を正しく整備する
- バウンスやスパム報告を監視して、悪いリストを送り続けない
対策1:SendGridへ移行し、ドメイン認証を通す
SendGrid(Twilio SendGrid)を選んだ理由は、ドメイン認証のためのDNS設定をほぼ自動で用意してくれる点と、後述する到達状況の可視化(Event Webhook)が標準で使える点でした。
ドメイン認証で発行されるDNSレコード
SendGridの管理画面で Sender Authentication → Authenticate Your Domain を進めると、登録すべきCNAMEレコードが提示されます。これをDNSに追加するだけで、DKIMの鍵管理を肩代わりしてくれます。
# SendGridが発行するCNAMEレコード(値はアカウントごとに異なります)
種別 ホスト名 向き先
CNAME em1234.example.com u1234567.wl123.sendgrid.net
CNAME s1._domainkey.example.com s1.domainkey.u1234567.wl123.sendgrid.net
CNAME s2._domainkey.example.com s2.domainkey.u1234567.wl123.sendgrid.net
CNAMEを向ける形にしておくと、DKIMの鍵がローテーションされてもこちら側のDNSをいじらずに済むのが地味に効きます。鍵をTXTで直書きすると、更新のたびに事故ります(経験談)。
送信コード(Node.js)
アプリ側はこれだけ。APIキーは必ず環境変数で持ちます。コードに直書きしてGitHubに上げて流出、は鉄板の事故なので絶対にやめましょう。
const sgMail = require('@sendgrid/mail');
// APIキーはコードに書かず、環境変数から読む
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
async function sendConfirmationMail(to, name) {
const msg = {
to,
// Fromは「認証済みドメイン」のアドレスにする。ここがズレると一気に弾かれる
from: { email: 'noreply@example.com', name: 'Example サービス' },
subject: 'ご登録ありがとうございます',
text: `${name} 様\nご登録ありがとうございます。`,
html: `<p>${name} 様</p><p>ご登録ありがとうございます。</p>`,
// 宣伝系メールはワンクリック解除を入れる(Gmail新ルール対応)
asm: { groupId: 1234 },
};
try {
await sgMail.send(msg);
console.log(`sent: ${to}`);
} catch (err) {
// ここで握りつぶさず、必ずstatusCodeとbodyを記録する
console.error(err.response?.body || err.message);
}
}
ここで個人的に一番効いた学びが、from を必ず認証済みドメインのアドレスに揃えることでした。フリーメール(gmail.com など)を from に使うと、DMARCの観点で「なりすまし」扱いされて一発で迷惑メール行きになります。
対策2:SPF / DKIM / DMARC が何をしているのか
ここは多くの人が「呪文」として丸暗記しがちなので、役割を一枚の図にしました。要は**「送信元のなりすましを防ぐ三段構え」**です。
それぞれをDNSに設定したレコードがこちらです。
# SPF:このドメインからの送信を許可するサーバーを宣言
example.com. TXT "v=spf1 include:sendgrid.net ~all"
# DMARC:認証に失敗したメールをどう扱うか + レポート送付先を指定
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com; pct=100"
DMARCの p=none は「失敗しても拒否はしないが、結果はレポートして」という監視モードです。いきなり p=reject(拒否)にすると、設定漏れで自分の正規メールまで全部消えるので、まずは none で1〜2週間レポートを眺め、認証が安定してから quarantine → reject と段階を上げるのが安全です。私はここで焦って reject にして、社内通知メールを全滅させかけました。
rua=mailto: に届くXMLレポートを読むと、「どのサーバーがうちのドメインを名乗って送っているか」が丸見えになります。身に覚えのない海外IPからの送信が見えたら、それはなりすましのサインです。
対策3:Event Webhookで「届いたか」を可視化する
送って終わり、にしないことが到達率を守る最後の鍵でした。SendGridの Event Webhook を有効にすると、配信・バウンス・スパム報告・開封・クリックといったイベントがJSONでPOSTされてきます。これを受けて、バウンスしたアドレスには二度と送らないようにします。
// SendGridからのイベントを受け取るエンドポイント
app.post('/webhook/sendgrid', express.json(), (req, res) => {
for (const event of req.body) {
switch (event.event) {
case 'bounce':
case 'dropped':
// 存在しない宛先。リストから即除外する
suppressEmail(event.email);
break;
case 'spamreport':
// スパム報告された。配信を止める(放置すると全体の評価が落ちる)
suppressEmail(event.email);
break;
case 'delivered':
console.log(`delivered: ${event.email}`);
break;
}
}
res.sendStatus(200);
});
存在しないアドレスやスパム報告を放置して送り続けると、ドメイン全体の評価(レピュテーション)が下がり、正常なユーザーへの到達率まで巻き添えで落ちます。「悪い宛先を切り捨てることが、良い宛先を守る」という感覚は、運用して初めて腹落ちしました。
結果
施策前後の数字がこちらです(社内ツールで実測)。
| 指標 | 対応前 | 対応後 |
|---|---|---|
| 到達率(届いた / 送った) | 約78% | 約99% |
| Gmailで迷惑メール判定 | 頻発 | ほぼ解消 |
| 「メール来ない」問い合わせ | 週20件超 | ほぼゼロ |
特に効いたのは、コードの修正よりもDNS側の整備でした。アプリのコードはほとんど変えていません。最初に「コードが悪いんじゃないか」と何時間もデバッグした自分に教えてやりたいです。犯人はアプリではなく、ドメインの身元証明だったと。
まとめ:明日からのチェックリスト
長くなったので、要点だけ持ち帰れるようにまとめます。
-
dig TXT yourdomain.comで SPF が設定されているか確認する - DKIM 署名が付いているか(SendGridならドメイン認証で自動)
-
_dmarcに DMARC レコードがあるか。まずはp=noneで監視から -
fromは必ず認証済みドメインのアドレス。フリーメールを使わない - バウンス・スパム報告を検知して送信を止める仕組みを持つ
- 宣伝系メールにはワンクリック解除を入れる(Gmail新ルール)
「送信成功」は「到達成功」ではありません。SMTPが 250 OK を返しても、その先の受信トレイに入るかどうかは、ドメインの信頼を地道に積み上げているかで決まります。
同じように「届かない」で消耗している方の、3日分の時間が浮けば嬉しいです。
本記事は実運用での対応をもとに、構成を整理して記録したものです。DNSレコードの値やアカウント設定はサービスごとに異なるため、ご自身の環境に合わせて読み替えてください。