Googleのメルマガ配信に対する要求は、前回の「メルマガをワンクリックで登録解除」だけでなく、配信するすべてのメールは、「ドメインに SPF および DKIM メール認証を設定します。」とあります。SPFはDNSでの設定のみですが、DKIMメール認証のためには、それぞれの配信メールの内容を元にDKIM署名のヘッダーの追加が必要です。私のクライアントの環境では、postfix + opendkimとシステムレベルで対応としました。ところが、そのパフォーマンスが非常に悪く配信時間が以前の何倍もかかるようになりました。パフォーマンスの悪さの原因がわからず悩んでいたところ、Laravelで(正確にはSymfonyで)、DKIM署名が可能なことを発見しました。試した結果、postfix + opendkimの半分以上の配信時間が可能となりました。

DKIM認証の仕組み

まず、DKIM認証の仕組みを簡単に説明します。DKIM認証のためには、配信側においてsshと同様に秘密キーと公開キーを作成します。公開キーはドメインのDNSにエントリーを作成して保存します。メール配信側(通常はMTA)は、送信メールのデータと秘密キーでDKIM署名を作成してメールのヘッダーに、DKIM-Signatureの値として追加します。メール送信先のMTAでは、このメールを受け取ると配信者のDNSから先の公開鍵を取得して、メールに含まれるDKIM署名の正当性を検証します。

以下、DKIM認証されたメールのソースです。Dkim-Signatureヘッダーとしてあり、受け取り側のgoogle.comでDKIMの認証がOK(dkim=pass)なことが伺えます。

DKIMのキーを作成

DKIM認証の対象となるドメイン(上の例では、lotsofbytes.com)のために、秘密キーと公開キーを以下のコマンド実行で作成します。

$ openssl genrsa -out dkim_private.key 2048
Generating RSA private key, 2048 bit long modulus
............................+++
......................................+++
e is 65537 (0x10001)

次に、それをもとに公開キーを作成します。

$ openssl rsa -in dkim_private.key -pubout -outform der 2>/dev/null | openssl base64 -A
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Em6C71Rcvf9qsRJciVRjuqcJQRzMeNRMK7Kizz2jfPxnx7XBu7hg2lJ3AvJj86pXJE2DASR0LmXMUfwOyXQ5dCJrBdfYK31ZCuBfVN1Vz/9utupyq1zPfAoteB8biR+mvc5MinrY2kj6LDBbuYU3Ff930AOVig/5ExAKfrSvt9gqrSSbZn36L/MnigcJWmB61mbCDXEPxTsZ8C+X7OsplpSdmHM3VpL9Dzo6FPyFwNnKMcGMBpgnTGo8PfN7H+Fh5n7QTv42f6BLEiWd47JoBU5yRnRqjVCFenTI1Gu/8RlwaWUJBZ7+eDeJmaPONb6WFjRlVD8ScU0qzEpKi71mQIDAQAB

これをDNSに以下のように、TXTレコードとして追加します。公開キーは長いので1つの文字列で収めることができません。キーの適当な場所で引用符を使って2つの文字列にする必要があります。以下は、AWSでのRoute53でのDNSエントリーの例です。

アプリでDKIM署名

DKIMの署名の機能はLaravelには存在しないので、Laravelが使用しているSymfony MailerのDKIM署名を使います。

「メルマガをワンクリックで解除」で使用したMailableのコードを編集しました。以下では差分を掲載しています。


namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Symfony\Component\Mime\Email;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Headers;
use Illuminate\Queue\SerializesModels;
use Illuminate\Mail\Mailables\Envelope;
use Symfony\Component\Mime\Crypto\DkimSigner;

class Newsletter extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * @var DkimSigner
     */
    public static $signer;

    /**
     * Create a new message instance.
     */
    public function __construct(public string $email)
    {
        $this->email = $email;

        if (! self::$signer) {
            // 署名者のインスタンスを作成
            self::$signer = new DkimSigner(
                'file://'.storage_path('dkim_private.key'), // 秘密キーのパス
                config('mail.dkim.domain'), // DKIM対象のドメイン名
                'default' // DNSのセレクター
            );
        }
    }

    /**
     * Build the message.
     */
    public function build(): static
    {
        $html = view('newsletter')->render();

        // 以下で、メールにDKIM署名
        $this->withSymfonyMessage(function (Email $symfonyMessage) use($html) {
            $symfonyMessage->html($html); // symfonyのコンポーネントにデータを入れる
            $signedEmail = self::$signer->sign($symfonyMessage); // 署名
            $symfonyMessage->setHeaders($signedEmail->getHeaders()); // ヘッダーに追加
        });

        return $this;
    }

...
}

コードの変更箇所は、2つあります。

まずコンストラクターにおいて署名者のインスタンスを作成して、これをクラス変数として$signerに収めます。ここで必要な情報は、

  • 秘密キーのパス:これは先に作成したdkim_private.keystorageのディレクトリに置いておくことが必要。Laravelのプロジェクトの.gitignoreには、/storage/*.keyの行が含まれるので、バージョン管理からは除外となります。
  • DKIMの対象となるドメイン名:ここでは以下のように、congig/mail.phpに項目を追加しておき、.envにて、MAIL_DKIM_DOMAIN=で値を渡します。ちなみに、ここでドメインは、メールの差出人Fromで使われるドメイン(例えば、hello@lotsofbytes.comnならlotsofbytes.com)となります。
    return [
    ...
        'dkim' => [
            'domain' => env('MAIL_DKIM_DOMAIN'),
        ]
    
    ];
    </li>
    
  • 公開キーのレコード名のセレクター名:ここでは先にDNSに登録した名前の、default._domainkey.lotsofbytes.comdefaultを指定。

次に、build()のメソッドを追加します。これは、Laravel 9x以前にEnvelopeやContentなどのコンポートがないときにメールを構築するために使われていたメソッドです。しかし、ここにおいてSymfony MailのモジュールにアクセスしてDKIM署名を実行する機能のために使います。ここで大事なのは、SymfonyのDKIM署名を使用するために、Symfonyのメールのインスタンス(Symfony\Component\Mime\Email)においてメールの内容となるデータを先に注入することです。もちろん、後にLaravel側でも同様な作業が行われますが、それはメール配信時の作業で、この時点ではDKIM署名のために必要なメールの内容がありません。それゆえにです。

最後に

postfix+opendkimのDKIM署名のパフォーマンスが悪かったために、このような一時対応となりましたが、これが複数のMailableで必要となると管理は先のシステムレベルで行えることに越したことはありません(改善方法を知っているお客様がいれば是非ご連絡を!)。しかし、レンタルサーバーでpostfixなどの管理権限がないときは、今回の方法が役に立つと思います。

By khino