LoginSignup
8
4

「意外な落とし穴!? Webサービスに必須のメール送信機能の実装ナレッジ・運用ノウハウ・失敗談などあなたのTipsを投稿してください!by blastengine Advent Calendar 2023」9日目の記事です。

メール送信機能を持つサービスの開発に関わった際に得た知見を抽象化した形で記します。
後で自分自身が読み返して振り返られるように参考URLを豊富に掲載するようにしています。

システム概要

パブリッククラウドサービスのIaaS上にメール送信機能を有するWebシステムです。
メール機能としては、本人認証URLを送付するだけの簡易機能である。
このようなケースでは、多くのプロジェクト・サービスでは、SendGridのようなメール配信サービスを使用することが多いと思う。
なぜなら、メールサーバを構築して、自前で実装・運用すると色々やるべきことが多く沼が深いので手間暇がかかる。
その中でこのサービスでは、SMTPサーバなしでメール送信可能でNode.js上で利用できるnode-sendmailを使用して実装した。
当時はSendGridのようなサービスを知らなく、自前で実装・運用という方針になったんだっけな:thinking:
:point_up:私は、プロジェクト立ち上げ後ちょっとしたところで参画したので実装のスコープはある程度決まっていた記憶・・・。

このライブラリは、アプリケーション上に簡易的なSMTP機能を実装できるイメージだと思う。
メール宛先のメールサーバからSMTPエラーコードを取得してログにも出力する事ができるので送信専用機能を実装するには十分であった。

メール送信機能.png

この記事を投稿した直後に事態は急変・・・:sweat_drops:

※2024/01/28 追記

Google社から2023年12月にメール送信者のガイドラインを更新して、送信者の要件に「メールの送信にTLS接続を使用する」が追加となった。

これに対応するため、システム内にSMTPサーバを立ててアプリケーションはこのサーバを経由して外部SMTPサーバへメール送信する構成に変更した。

メール送信機能(改).png

一般的なメールサービスプロバイダが行っている迷惑メール確認について

一般的には、以下3つの点でなりすまし(迷惑)メール判定を行っているよう。

  1. 送信元SMTPサーバのIPアドレスの逆引き(PTRレコード)が設定されていて、逆引きしたホスト名の正引き(Aレコード)と整合性が合っていること
  2. 送信元SMTPサーバのホスト名の正引き(Aレコード)が設定されていて、正引きしたIPアドレスと送信元SMTPサーバのIPアドレスが一致していること
  3. エンベロープFromおよびヘッダFromのドメインのAレコードとMXレコードが設定されていること

DNSレコードの設定例
<Aレコード>
smtp.sample.com  300   IN  A   ◯◯◯.▢▢▢.△△△.XXX

<PTRレコード>
XXX.△△△.▢▢▢.◯◯◯.in-addr.arpa. 3600  IN  PTR  smtp.sample.com

<MXレコード>
sample.com  1000  IN   MX  10 smtp.sample.com

迷惑メール判定されないためにやったこと

自前でメールサーバを構築して、運用するに当たり迷惑メール判定されないためにやるべきことは多い(多かったというべきか・・・)。

前述のDNSへのAレコード、PTRレコード、MXレコードの設定に加えて行ったことを記述する。
PTRレコードの設定については、補足を記載した。

IPレピュテーションをチェック!

IPレピュテーション等は、IPアドレスの信用性のスコア。
このスコアが低いとメール送達率が悪くなるので、このスコアは非常に重要!
IaaSの場合、払い出されるグローバルIPアドレスは使い回されていることが多いため、前回使用者の使用実績によってはIPレピュテーションが低いことが多い。
今回のサーバで払い出されたIPレピュテーションは、某サイトで「判定不能」でした。
これは、稼働実績が少なくて判定できるレベルではなかった。

レピュテーションは、以下のサイトでチェックできる。

「実績がない≠問題無い」ではない。
信用がないメールサーバのため、メールサービスプロバイダにより迷惑メール判定される可能性がある。
メールサービスを着実に運用して実績を積み重ねる必要がある。

:beginner:以下に挙げた基礎的なことを1つ1つ積み上げてレピュテーションを上げるために着実なメールサーバ運用をしている

  1. ベストプラクティスに従ったメールサービス運用
  2. スパムメール送信の踏み台にならないようになセキュリティ対策
  3. SMTPエラーとなり、届かなった場合の対策
  4. メールサービスドメインがブラックリスト入りしていないかの確認

ベストプラクティスに従う!

これはものすごく大事で正規なメール送信サービスを運用していることを担保する最低限の設定(施策)である。
ベストプラクティスは、GメールやiCloudメールなどの主要なメールサービス運用会社が提示している。
これを守らないと迷惑メール判定されても文句は言えない!

:grin:提示されているベストプラクティスをなるたけ実施することで、迷惑メール判定されることをかなり防げた!

Google社から提示されているベストプラクティス

Apple社から提示されているベストプラクティス

Big企業2社から出ているベストプラクティスを見ると共通している。
その中でも大事なベストプラクティスはなりすまし対策のメール認証であると思う。

SPF、DKIM、DMARC、逆引きレコードを設定する!
この4つを設定するだけで、迷惑メール判定は回避できる!!

SPF(Sender Policy Framework)

正規のIPアドレスからメールが送信されているか検証(認証)する送信ドメイン認証技術。
メール送信者がDNSサーバに送信元ドメインをSPFレコード(TXT)を公開する。
メール受信サーバはメールの送信元IPアドレスと送信元ドメインからDNSで公開されているSPFレコードを引いてきて記載されているIPアドレスと照合して送信元IPアドレスを認証する。

<SPFレコード設定例>

SPFレコード設定例
sample.co.jp. IN TXTv=spf1  ip4:xxx.xxx.xxx.xxx  -all”

※xxx.xxx.xxx.xxxはIPアドレス

SPFメール認証は、エンベロープFromのドメインで認証するため、ヘッダーFrom(メールに表示されている送信元ドメイン)が偽装されている場合は、なりすましメールを見抜くことができない。

DKIM(DomainKeys Identified Mail)

送信者がメールに電子署名し、メール受信側は「公開鍵」と「秘密鍵」を使って電子署名を検証することで送信元のなりすましやメール内容の改ざんがされていないか確認する。
送信者はメール作成時に秘密鍵で電子署名を行いヘッダーフィールドにDKIM-Signatureに付与する。
秘密鍵に対する公開鍵をDNSサーバに登録して公開する。
受信側はDNSサーバから公開鍵情報を取得してメールに付与された電子署名で正当性をチェックする。
これにより送信者の正当性とメール内容の改竄チェックを同時にできる。

<DKIMレコード設定例>

DKIMレコード設定例
SELECTOR._domainkey.sample.co.jp 300 IN TXT “v=DKIM1; k=rsa; t=s; p=<公開鍵データ(Base64でエンコード)>

DMARC(Domain-based Message Authentication, Reporting, and Conformance)

送信ドメイン認証技術の一種で、なりすましメールの防止、メール配信状況の把握、認証に失敗したメールの取扱い指示等を行える。
送信者はメール認証結果に関する情報が記載されたDMARCレポートを受け取ることができる。
※DMARCレポートは、受信側サーバが受け取ったメールの認証結果等をドメイン所有者に対してフィードバックするための機能。

DMARCに対応している受信サーバがメールを受信すると、ヘッダFROMのドメインにDMARCが設定されているかチェックする。
DMARCが設定されている場合は、DMARC認証を検証してその認証結果とDMARCポリシーに従ってメールを処理する。

初めはp=noneで設定してレポート情報をもとにメール認証が成功しているのを確認してp=quarantine またはp=rejectに変更するのがセオリーのようです。

<DMARCレコード設定例>

DMARCレコード設定例
_dmarc.sample.co.jp IN TXT “v=DMARC1; p=quarantine; rua=mailto:dmarc-report@sample.co.jp; ruf=mailto:dmarc-report@sample.co.jp

※ruaとrufがDMARCレポートの受信先メールアドレス
※pがポリシー設定。

ポリシー 内容
none 認証失敗時はそのまま配信する
quarantine 認証失敗時は迷惑メールフォルダに隔離する
reject 認証失敗時は削除する

DNS逆引きレコード

メール送信者の送信元を確認するのに使用する。
送信元IPアドレスから逆引したAレコード(IPアドレス)と送信元IPアドレスと一致しているか確認する。
加えて、送信元IPアドレスから逆引きしたホスト名と送信元SMTPサーバーのホスト名が一致しているか確認する。

<逆引きレコード設定例>

逆引きレコード設定例
xxx.xxx.xxx.xxx.in-addr.arpa. IN PTR mail.sample.co.jp

※xxx.xxx.xxx.xxxはIPアドレス

DNSの設定情報をチェック!

世の中には、DNS設定をチェックできるサイトが多くあるので使用して確認するのがベスト!

SPFレコードのチェック

MXレコードのチェック

SPF & DMARCチェック

DMARCチェック

逆引きチェック

DNS全般のチェック

Webサイトドメインの調査

ブラックリストに載っていないか定期的に確認する!

メールセキュリティ設定をしていても、迷惑メール判定されたりRejectされる可能性がある。
各プロバイダでブラックリストと照らし合わせて判断しているので、ブラックリストに載っていないか定期的に確認する。
以下のサイトでブラックリストに載っていないかチェックできる。

メール送信にTLS接続を使用する

※2024/01/28 追記

Google社から2023年12月にメール送信者のガイドラインを更新して、送信者の要件に「メールの送信にTLS接続を使用する」が追加となった。
これに対応しないと迷惑メール判定され段階的にメール受信拒否になるようなので、Gメール宛にメール送信するためには対応必須となる。

当システムで採用したnode-sendmailは、2024年1月28日時点でどうもTLS接続のオプションがなさそう・・・:sob:
Referance : guileen / node-sendmailリポジトリ ※2024年1月28日アクセス時点

2024年1月28日時点でリポジトリに公開されていたオプション仕様
const sendmail = require('sendmail')({
  logger: {
    debug: console.log,
    info: console.info,
    warn: console.warn,
    error: console.error
  },
  silent: false,
  dkim: { // Default: False
    privateKey: fs.readFileSync('./dkim-private.pem', 'utf8'),
    keySelector: 'mydomainkey'
  },
  devPort: 1025, // Default: False
  devHost: 'localhost', // Default: localhost
  smtpPort: 2525, // Default: 25
  smtpHost: 'localhost' // Default: -1 - extra smtp host after resolveMX
})

よくよく見るとSMTPホストを指定できるので、システム内にSMTPサーバを立ててそれ経由でメール送信すればTLS接続はクリアできそう!

オプションのdevPortdevHostsmtpPortsmtpHostでは動きが違う。
ここは、かなり詰まったところなので次の節でまとめます。

ということで、システム内にSMTPサーバを立ててアプリケーションはこのサーバを経由して外部SMTPサーバへメール送信する構成に変更した。

メール送信機能(改).png

メールサーバとしては、デファクトスタンダードなPostfixを使用した。
Postfixであれば、簡単にTLS接続を使用したメール送信が可能となる。
/etc/postfix/master.cfに以下の設定を追記すれば、TLS経由で宛先のSMTPサーバにメール送信することができる。

/etc/psotfix/master.cf
smtp_tls_CAfile = /etc/pki/tls/certs/ca-bundle.crt
smtp_tls_security_level = may
smtp_tls_loglevel = 1

TLS接続時の認証は、サーバにインストールされているルート証明書を利用する(smtp_tls_CAfile = /etc/pki/tls/cert.pem)。
相手先がTLS利用できない可能性もあるので、smtp_tls_security_levelは「may」とした。
ログについては、最低限認証できたかを記録できれば十分なのでsmtp_tls_loglevel = 1としてTLSハンドシェイクと証明書の情報をログに記録するようにした。

使用するルート証明書ファイルを以下にするケースもあるが、私の調べた限りでca-bundle.crtと証明書リスト内容は一緒で鍵ファイルを同梱しているかの違いのようである。
smtp_tls_CAfile = /etc/pki/tls/cert.pem

TLS利用に関する細かい設定は以下参照。

Node.jsだけでTLS利用してメール送信するには、Nodemailerというライブラリを使用するのも手らしい。
むしろこちらの方がメール送信ライブラリとしてはメジャーかもしれない:sweat_smile:

node-sendmailでSMTPサーバ経由でメール送信するオプション設定について

この節は余談的な話です。2024年1月29日時点のnode-sendmailライブラリの実装内容を調べた内容まとめになっています。

node-sendmailには、SMTPサーバ経由でメール送信できそうなオプションの組み合わせが2つある。

  1. smtpPortsmtpHostの指定
  2. devPortdevHostの指定

:warning:この2つの動きは大きく違います!

2024年1月29日時点でguileen/node-sendmailリポジトリにアクセスした際のsendmail.jsの実装内容の抜粋が以下です。

sendmail.js 77~124行目抜粋
 function connectMx (domain, callback) {
    if (devPort === -1) { // not in development mode -> search the MX
      resolveMx(domain, function (err, data) {
        if (err) {
          return callback(err);
        }

        data.sort(function (a, b) { return a.priority > b.priority });
        logger.debug('mx resolved: ', data);

        if (!data || data.length === 0) {
          return callback(new Error('can not resolve Mx of <' + domain + '>'));
        }
        if(smtpHost !== -1) data.push({exchange:smtpHost});
        
        function tryConnect (i) {
          if (i >= data.length) return callback(new Error('can not connect to any SMTP server'));

          const sock = createConnection(smtpPort, data[i].exchange);
        
          sock.on('error', function (err) {
            logger.error('Error on connectMx for: ', data[i], err);
            tryConnect(++i)
          });

          sock.on('connect', function () {
            logger.debug('MX connection created: ', data[i].exchange);
            sock.removeAllListeners('error');
            callback(null, sock);
          })
        }

        tryConnect(0)
      })
    } else { // development mode -> connect to the specified devPort on devHost
      const sock = createConnection(devPort, devHost);

      sock.on('error', function (err) {
        callback(new Error('Error on connectMx (development) for "'+ devHost +':' + devPort + '": ' + err))
      });

      sock.on('connect', function () {
        logger.debug('MX (development) connection created: '+ devHost +':' + devPort);
        sock.removeAllListeners('error');
        callback(null, sock);
      })
    }

smtpPortsmtpHostを使用した場合は、メール宛先ドメインからDNSでメールサーバを引いてきた結果の後にオプションで指定したSMTPサーバをメール送信サーバリスト追加して順次接続する。そして、接続できたメールサーバ宛にメールをリレーする動きとなっている。
そのため、オプション指定したSMTPよりも先に宛先ドメインのメールサーバへ通信をかけることになる。よっぽどのことがないと宛先ドメインのメールサーバに繋がらないことはないので、そのまま宛先ドメインのメールサーバへメール送信される。

smtpPortsmtpHostに自前のSMTPサーバを指定しても、そのサーバ経由でメール送信される可能性は低い。
ほぼほぼnode-sendmailの簡易SMTP機能を使用して、直接宛先ドメインのメールサーバへメール配送される。

つまり、自前のSMTPサーバを経由して外部メールサーバへメール配信するには、devPortdevHostオプション(development mode)を使用する必要がある!

私感

smtpPortsmtpHostオプションで設定した値を配列の先頭に入れてアクセスしてエラーであれば、ドメインから引いたメールサーバに配送がきれいな形では思う:thinking:

結び

メール送信機能を構築するのは、初だったので参考資料を読みながらSPF、DKIM、DMARC、逆引きレコード、MXレコードの設定を行いました。
加えて2024年2月から適用されるGメールのポリシに従ったTLS接続手順の設定を行いました。
試行錯誤した際に参照したサイトをリンクで掲載することで自らも後から読み直して試行錯誤の過程を振り返られるようにしました。
この記事が迷惑メール判定に悩んでいる方の参考になれば幸いです。

参考資料

  • IPレピュテーション

  • iCloudにメールにメールが届かない

  • SPF

  • DKIM

  • DMARC

  • DNS逆引きレコード

  • 送信ドメイン認証

  • Gmailの新スパム規制対応全部書く

  • メールサービスプロバイダがなりすましメール判定する条件

  • @ken_yoshi氏、「Gmailの新スパム規制対応全部書く」

8
4
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
8
4