1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「送ったはずのメールが届かない」を3日かけて潰した話 〜Gmailの新ガイドライン対応とSendGrid移行の全記録〜

1
Posted at

はじめに

「登録ありがとうございます」のメールが、なぜか半分くらいのユーザーに届いていませんでした。

しかもエラーは出ていません。アプリのログ上は「送信成功」。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は、もはや「あって当たり前」のパスポートです。無いと受信トレイの入り口で止められます。

ここまでで方針が固まりました。

  1. 自前運用をやめ、到達率に強いSendGridに送信を寄せる
  2. ドメイン認証(SPF / DKIM / DMARC)を正しく整備する
  3. バウンスやスパム報告を監視して、悪いリストを送り続けない

対策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週間レポートを眺め、認証が安定してから quarantinereject と段階を上げるのが安全です。私はここで焦って 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.comSPF が設定されているか確認する
  • DKIM 署名が付いているか(SendGridならドメイン認証で自動)
  • _dmarcDMARC レコードがあるか。まずは p=none で監視から
  • from は必ず認証済みドメインのアドレス。フリーメールを使わない
  • バウンス・スパム報告を検知して送信を止める仕組みを持つ
  • 宣伝系メールにはワンクリック解除を入れる(Gmail新ルール)

「送信成功」は「到達成功」ではありません。SMTPが 250 OK を返しても、その先の受信トレイに入るかどうかは、ドメインの信頼を地道に積み上げているかで決まります。

同じように「届かない」で消耗している方の、3日分の時間が浮けば嬉しいです。


本記事は実運用での対応をもとに、構成を整理して記録したものです。DNSレコードの値やアカウント設定はサービスごとに異なるため、ご自身の環境に合わせて読み替えてください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?