バウンスとは、メールサーバーがメッセージを受け付けなかった理由を、3桁のコードで伝えてくるものです。このコードを正しく分類できるかどうかで、本当に存在しないアドレスを抑制リストに追加するのか、再送すれば届いたはずのアドレスを捨ててしまうのかが分かれます。このガイドでは、バウンス分類ツールを作成し、同じ分類を送信の後ではなく送信の前に行う方法を紹介します。
結論から言うと#
SMTPコードの最初の1桁で対応が決まります。5xxは恒久的な失敗なので抑制リストに追加し、4xxは一時的な失敗なので再送します。重要な補足は拡張ステータスコードです。5.7.xはポリシーまたはレピュテーションによるブロックであり、サーバーがメールボックスではなく送信者を拒否していることを示すため、存在しないアドレスとして扱ってはいけません。検証ツールはメールボックスのプローブに同じロジックを適用するため、実際のバウンスが起きる前にアドレスを分類できます。
バウンスの2つの側面#
どのバウンスにも、切り分けて考えるべき2つの性質があります。
- 恒久性:最終的な失敗(
5xx、ハードバウンス)か、一時的な失敗(4xx、ソフトバウンス)かを示します。これが、抑制リストに追加するか再送するかの判断になります。 - 対象:サーバーが評価しているのはメールボックス(「そのようなユーザーは存在しない」)か、送信者(「送信元IPがブラックリストに載っている」)かを示します。送信者に対する恒久的な拒否はハードバウンスに見えますが、同じように扱ってはいけません。問題はレピュテーションにあるため、受信者を抑制リストに追加しても何も解決しません。
コードの全体的な仕様はSMTPレスポンスコードのガイドで解説しています。分類とは、それを対応方針に変換する作業です。
バウンス分類ツール#
SMTPコードと、任意の拡張コードを読み取って対応方針を返す分類ツールを紹介します。単純な分類ツールの多くが見逃すケース、つまり5xxの形をしたポリシーブロックにも対応しています。
/**
* Classify a bounce into an action.
* @param {number} code basic SMTP code, e.g. 550
* @param {string} [enhanced] enhanced status code, e.g. "5.1.1"
*/
export function classifyBounce(code, enhanced) {
const cls = Math.floor(code / 100); // 2, 4, or 5
// Policy / reputation block: the server is refusing YOU, not the mailbox.
// Suppressing the recipient would be treating the wrong problem.
if (enhanced?.startsWith("5.7") || enhanced?.startsWith("4.7")) {
return { type: "policy", action: "review", note: "sender reputation, not the mailbox" };
}
if (cls === 5) {
// Permanent: no such user, disabled, or the domain won't relay.
return { type: "hard", action: "suppress", reason: "rejected_email" };
}
if (cls === 4) {
// Temporary: full mailbox, greylisting, or a rate limit.
const fullMailbox = code === 452 || enhanced === "4.2.2";
return {
type: "soft",
action: "retry",
reason: fullMailbox ? "full_mailbox" : "timeout",
};
}
return { type: "unknown", action: "retry" };
}
classifyBounce(550, "5.1.1"); // { type: "hard", action: "suppress" }
classifyBounce(452); // { type: "soft", action: "retry", reason: "full_mailbox" }
classifyBounce(554, "5.7.1"); // { type: "policy", action: "review" } ← don't suppress
ここに組み込まれたルールは、メールボックスを理由とする明確な拒否だけを抑制リストに追加することです。5.7.xは送信側のレピュテーションを改善すべきことを意味します。受信者を抑制リストに追加しても、シグナルを隠すだけで何も解決しません。
バウンスの後ではなく、前に分類する#
バウンスの分類には、事後対応にならざるを得ないという難点があります。アドレスが存在しないとわかるのは、実際に送信し、その過程でレピュテーションを損なった後だからです。検証では、実際の送信ではなくSMTPのプローブに同じ分類を行うため、バウンスを起こさずに判定結果を得られます。判定結果と理由コードは、同じ対応方針にそのまま対応づけられます。
const ACTION_BY_REASON = {
accepted_email: "keep", // deliverable
rejected_email: "suppress", // hard bounce confirmed by probe
invalid_email: "suppress", // bad syntax
invalid_domain: "suppress", // no mail route
low_quality: "suppress", // disposable
low_deliverability: "segment", // catch-all or full mailbox — send carefully
timeout: "retry", // greylisted / no answer yet
unavailable_smtp: "retry", // couldn't reach the server
unknown: "retry",
};
// From a verification result row:
const action = ACTION_BY_REASON[row.reason] ?? "retry";
判断の流れは同じで、実行するタイミングが前倒しになるだけです。ハードバウンスになったはずのアドレスはundeliverableとして返され、送信前に抑制リストに追加されます。unknownのアドレスは、推測で判断せず再試行の対象になります。
運用への組み込み#
どちらの方向で運用する場合も、出力の行き先は抑制リストの1か所です。
- 事後対応:ESPのバウンスWebhookを解析して
classifyBounceを実行し、ハードバウンスは抑制リストに追加し、ソフトバウンスは再送を予定します。 - 事前対応:一括検証ジョブを実行し、
include=resultsで結果を取得して、各reasonをACTION_BY_REASONで対応方針に変換し、実行します。これは、ESP別のクリーニングガイドでプラットフォームごとに解説しているエクスポート、検証、抑制のループです。
理想は両方を組み合わせることです。事前に検証してバウンスの大半を防ぎ、それでも残るバウンスは事後に分類して、その後で無効になったアドレスを見つけます。
よくある質問
01コード上で、ハードバウンスとソフトバウンスの違いは何ですか?
SMTPのクラスです。5xxはハードバウンス(恒久的なので抑制)、4xxはソフトバウンス(一時的なので再送)です。唯一の落とし穴は5.7.xの拡張コードで、これは存在しないメールボックスではなく、送信者に対する恒久的なポリシーブロックです。別に分類し、受信者を抑制リストに追加するのではなく、レピュテーションを改善してください。
02ソフトバウンスは何回再送してから抑制すべきですか?
決まった回数はありませんが、よくある方法は、ソフトバウンスしたアドレスへの送信を連続する数回の配信で再試行し、回復しなければ抑制に切り替えるというものです。これは、メールプロバイダーやESPが内部で行っている処理におおむね近いものです。メールボックスの容量超過は自然に解消することが多いため、ソフトバウンスは、ハードバウンスとは違い、焦らず様子を見る価値があります。
03送信する前にアドレスを分類できますか?
はい。それが検証です。SMTPのメールボックスプローブはバウンスと同じシグナルを生み出し、理由コードとともにdeliverable | risky | undeliverable | unknownの判定結果に対応づけられます。そのため、存在しないアドレスはバウンスが起きる前に抑制できます。上のACTION_BY_REASONは、これらの判定結果を、抑制、再試行、保持という同じ対応方針に変換します。
分類の基準となる判定結果をお試しになりたい場合は、無料プランに100クレジットが含まれています。事前対応のループが読み取る理由コードと/jobsの結果エンドポイントは、APIリファレンスに記載されています。



