自分のアプリからKaiMail SMTPでメールを送る(Python、RubyとPHPサンプル付き)


独自ドメインから少しだけメールを送りたいサイドプロジェクトがあるとする。サインアップ確認、パスワードリセット、たまの通知。それくらい。個人のGmail認証情報をコードに埋め込みたくはないし、月200通のために本格的なトランザクションメールサービスを契約するのも気が重い。そして、受信転送のためにすでにKaiMailを使っている。

朗報だ。同じKaiMailアカウントで送信もできる。本記事では、PythonとRubyでそれを行う方法を、私が削れる限り最小のコードで紹介する。

必要なもの

  1. KaiMailの有料プラン。SMTP送信はPlus・Pro・Businessに含まれている(背景はアナウンス記事を参照)。
  2. KaiMailに登録した独自ドメイン。DKIMとSPFのDNSレコードが設定済みであること。DKIMがまだなら、DKIMセットアップガイドがRoute 53での手順を解説しているが、同じレコードはどのDNSプロバイダでも動く。
  3. SMTP認証情報。ダッシュボードにログインしてプロフィールページを開き、SMTPユーザー名とSMTPパスワードをコピーする。SMTPパスワードはダッシュボードのログインパスワードとは意図的に分けてある。ダッシュボードから締め出されることなく、いつでもローテーションできるようにするためだ。

接続情報

設定項目
SMTPサーバー mail.kaimail.net
ポート 587
暗号化 STARTTLS
ユーザー名 KaiMailのメール
パスワード SMTP用パスワード

ただのSMTP submissionである。独自仕様は何もない。まともなSMTPライブラリならどの言語でも動く。下の例では、PythonとRubyで普通に手が伸びるツールを使っている。

Pythonから送る

Pythonの標準ライブラリで全部足りる。pip installは不要だ。

import os
import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg["Subject"] = "Hello from KaiMail"
msg.set_content("Sent via KaiMail SMTP.")

with smtplib.SMTP("mail.kaimail.net", 587) as smtp:
    smtp.starttls()
    smtp.login(os.environ["KAIMAIL_USER"], os.environ["KAIMAIL_PASS"])
    smtp.send_message(msg)

保存して、KAIMAIL_USERKAIMAIL_PASSをシェルでexportし、実行する。これで全部だ。

最初にハマりがちな点が2つある。

  1. starttls()login()より先に呼ぶ。 順序が逆だとパスワードを暗号化されていない接続で送ることになる。KaiMail側でログインを拒否するが、サーバーに救ってもらうことを当てにしてはいけない。常に先に接続を暗号化する。
  2. FromはKaiMail上で自分が所有しているドメインでなければならない。 サーバーは送信のたびに検証する。[email protected]や、自分のアカウントに追加していないドメインで送ろうとすると拒否される。これはバグではなく仕様だ。あなたのアカウントが他人のなりすましに使われないようにするための仕組みである。

HTMLメールが必要なら、set_content()の後にmsg.add_alternative(html, subtype="html")を足す。ほかのコードはそのままでいい。

Rubyから送る

Rubyにも標準ライブラリのnet/smtpがあるが、デファクトスタンダードはmail gemで、こちらの方がはるかに扱いやすい。依存1つは妥当な対価だと思う。

require "mail"

Mail.defaults do
  delivery_method :smtp,
    address: "mail.kaimail.net",
    port: 587,
    user_name: ENV["KAIMAIL_USER"],
    password: ENV["KAIMAIL_PASS"],
    authentication: :plain,
    enable_starttls_auto: true
end

Mail.deliver do
  from    "[email protected]"
  to      "[email protected]"
  subject "Hello from KaiMail"
  body    "Sent via KaiMail SMTP."
end

gem install mailして、同じ2つの環境変数を設定し、実行する。

Rubyのハマりどころは本質的にPythonと同じで、キーの名前が違うだけだ。

  1. enable_starttls_auto: trueが、Pythonでのsmtp.starttls()の役割を果たしている。 これがないと、gemは平文で認証情報を送ろうとする。
  2. authentication: :plainで正しい。 STARTTLSが暗号化を担当し、PLAIN認証は暗号化されたチャネルの上で動く。「もっと安全そうだから」と:login:cram_md5に切り替える必要はない。安全性は変わらない。
  3. Fromの検証は同じく適用される。 KaiMailで所有していないドメインは失敗する。

Railsアプリの中で使う場合は、Mail.defaultsブロックをそのままconfig/environments/production.rbconfig.action_mailer.smtp_settingsに置けばよい。ActionMailerは内部で同じgemを使っているので、キーは完全に同じだ。

PHPから送る

PHP組み込みのmail()関数はSMTPには使い物にならないことで有名だ。ローカルにsendmailの設定が要り、認証も扱えず、エラー報告もほぼない。使わない方がいい。代わりにPHPMailerを使う。10年以上前からデファクトスタンダードだ。

<?php
require "vendor/autoload.php";

use PHPMailer\PHPMailer\PHPMailer;

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host       = "mail.kaimail.net";
$mail->Port       = 587;
$mail->SMTPAuth   = true;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Username   = getenv("KAIMAIL_USER");
$mail->Password   = getenv("KAIMAIL_PASS");

$mail->setFrom("[email protected]");
$mail->addAddress("[email protected]");
$mail->Subject = "Hello from KaiMail";
$mail->Body    = "Sent via KaiMail SMTP.";

$mail->send();

composer require phpmailer/phpmailerして、同じ2つの環境変数を設定し、php send.phpで実行する。

PHPでのハマりどころ。

  1. SMTPSecure = ENCRYPTION_STARTTLSがPythonのstarttls()に相当する。 もう1つの定数ENCRYPTION_SMTPSは465での暗黙的TLSを使う。KaiMailは465で待ち受けていないので、こちらは使わない。
  2. new PHPMailer(true)の引数trueは例外を有効にする。 これがないと、エラーはオブジェクトに静かに溜まり、send()は単にfalseを返すだけになる。最初に「何も起きなかったのはなぜか」をデバッグするのに何時間も溶かすことになる。必ずtrueを渡すこと。
  3. Fromの検証は同じく適用される。 なりすましの送信ドメインはサーバーに拒否される。

Laravelの中で使うなら、PHPMailerは不要だ。フレームワークに同梱されているSymfony Mailerで済む。.envMAIL_MAILER=smtpMAIL_HOST=mail.kaimail.netMAIL_PORT=587MAIL_ENCRYPTION=tls、それにMAIL_USERNAMEMAIL_PASSWORDを設定すれば、あとはLaravelがやってくれる。

よくあるハマりどころ

実際のサポート問い合わせから、もう少し挙げておく。

  1. SMTPパスワードはダッシュボードのパスワードではない。 別々のシークレットで、ローテーションサイクルも別だ。コードが認証に失敗するようになったら、プロフィールページからSMTPパスワードを再生成する。アカウントパスワードを変えてはいけない。
  2. FromのドメインはKaiMailで所有しているものでなければならない。 サーバーはなりすまし送信を拒否する。新しいドメインの追加はダッシュボードで1分で終わるが、送信前に必ず行う必要がある。
  3. 587でSTARTTLSを使う。465の暗黙的TLSではない。 KaiMailはポート465で待ち受けていない。一部のRuby・PHP・Javaの設定はデフォルトで465を使うので、ポートは明示的に指定すること。
  4. クォータはローリング30日間で、暦月ではない。 月初めにウェルカムメールを大量に送っても、翌月の頭にリセットされるわけではない。ダッシュボードの使用量バーを見ておけば、不意打ちを食らわずに済む。

次にやること

このスクリプトは「動く最小のもの」だ。本番コードでは、3つ追加する価値がある。

  1. キュー経由で送る。 SMTP送信はブロッキングで遅い。SidekiqやCelery、あるいはバックグラウンドジョブに渡して、リクエストハンドラは早く返す。
  2. 一時的な失敗はリトライする。 SMTPの4xxレスポンスは一時的なもの(greylist、一時的な負荷)で、5xxは恒久的(不正なアドレス、ポリシー拒否)だ。前者はリトライし、後者はリトライしない。
  3. SMTPサーバーが返すmessage IDをログに残す。 「あなたのシステムはメールを送ったのか」と顧客に聞かれたとき、ダッシュボードの送信ログをgrepするキーがmessage IDだ。

メールを受信してアプリに流し込みたい場合は、本記事の対になるWebhook統合ガイドがある。両方を合わせれば送受信両方向をカバーできる。

機能の全容はSMTP送信機能ページを参照。アカウントでまだSMTPを有効化していなければ、アナウンス記事にダッシュボード上の手順が載っている。Gmailの「Send mail as」回避策をまだ使っているなら、そのセットアップガイドに、コードを書くなら結局プログラムからのSMTPの方が綺麗だという話が書いてある。