メールサーバーの運用中に大量のバウンスメールが発生し、その原因調査と対策を行った事例を紹介する。本記事では、Python による解析スクリプトの実装から、qmail 特有のログ解析手法まで、実践的な技術内容を詳しく解説する。
背景
ある日、メールサーバーの postmaster 宛に大量のバウンスメール(配信失敗通知)が届いていることに気づいた。約2日間で4,500件以上のバウンスメールが蓄積されており、原因の特定と対策が急務となった。
調査環境
- MTA: qmail
- アンチウイルス: DrWeb
- OS: CentOS / Rocky Linux
- 解析ツール: Python 3
バウンスメールの構造を理解する
qmail のバウンスメール形式
qmail が生成するバウンスメールは以下の構造を持つ。
Return-Path: <#@[]> ← ダブルバウンスの特徴
Delivered-To: [email protected]
From: [email protected]
Subject: failure notice
Hi. This is the qmail-send program at example.com.
I tried to deliver a bounce message to this address, but the bounce bounced!
<[email protected]>:
192.0.2.1 does not like recipient.
Remote host said: 553 sorry, that domain isn't in my list of allowed rcpthosts (#5.7.1)
Giving up on 192.0.2.1.
--- Below this line is the original bounce.
(元のバウンスメールが続く)
ポイント:
– Return-Path: <#@[]> はダブルバウンス(バウンスのバウンス)を示す
– Return-Path: <> は通常のバウンス
– エラーコードは (#5.x.x) 形式で記載される
SMTP エラーコードの体系
5.x.x = 永続的エラー(再送しても失敗)
4.x.x = 一時的エラー(再送で成功の可能性あり)
5.1.x = 宛先アドレス関連
5.2.x = メールボックス関連
5.4.x = ネットワーク/ルーティング関連
5.7.x = セキュリティ/ポリシー関連
Pythonによるバウンスメール解析スクリプト
バウンスメール解析クラスの実装
Python の email モジュールを使用してバウンスメールをパースする。
#!/usr/bin/env python3
"""バウンスメール解析スクリプト"""
import re
import email
from email import policy
from pathlib import Path
from dataclasses import dataclass
from typing import Optional
@dataclass
class BounceInfo:
"""バウンスメール情報"""
filename: str
original_recipient: Optional[str] = None
error_code: Optional[str] = None
error_message: Optional[str] = None
bounce_type: str = "unknown"
is_double_bounce: bool = False
class BounceAnalyzer:
# SMTPエラーコードとバウンスタイプのマッピング
BOUNCE_PATTERNS = [
(r"no mailbox here by that name.*\(#5\.1\.1\)", "mailbox_not_found", "5.1.1"),
(r"couldn't find a mail exchanger.*\(#5\.4\.4\)", "no_mx_record", "5.4.4"),
(r"domain isn't in my list of allowed rcpthosts.*\(#5\.7\.1\)", "relay_denied", "5.7.1"),
(r"envelope sender is in my badmailfrom list.*\(#5\.7\.1\)", "sender_blocked", "5.7.1"),
]
def analyze_file(self, filepath: Path) -> BounceInfo:
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
msg = email.message_from_string(content, policy=policy.default)
bounce = BounceInfo(filename=filepath.name)
# 本文取得
body = self._get_body(msg)
# ダブルバウンス判定
if "bounce bounced" in body.lower():
bounce.is_double_bounce = True
# エラー情報抽出
self._extract_error_info(bounce, body)
return bounce
def _extract_error_info(self, bounce: BounceInfo, body: str):
# Remote host said パターンでエラーメッセージ抽出
remote_match = re.search(
r'Remote host said:\s*(\d+)\s+(.+?)(?:\n|$)',
body, re.IGNORECASE
)
if remote_match:
bounce.error_message = remote_match.group(2).strip()
# エラーコード抽出
code_match = re.search(r'\(#(\d\.\d\.\d+)\)', bounce.error_message)
if code_match:
bounce.error_code = code_match.group(1)
# バウンスタイプ分類
for pattern, btype, code in self.BOUNCE_PATTERNS:
if re.search(pattern, body.lower()):
bounce.bounce_type = btype
if not bounce.error_code:
bounce.error_code = code
break
qmail ログ解析スクリプト
qmail のログは syslog 形式で出力され、独自のタイムスタンプ(UNIX時間)を持つ。
Jan 20 10:49:40 mailserver qmail: 1768873780.433367 info msg 67524490: bytes 2726 from <> qp 20850 uid 496
ログエントリの種類:
| エントリ | 意味 |
|---|---|
new msg XXXXX |
新規メッセージ受信 |
info msg XXXXX: bytes N from <sender> qp N uid N |
メッセージ情報 |
starting delivery N: msg XXXXX to local/remote addr |
配信開始 |
delivery N: success/failure/deferral: message |
配信結果 |
bounce msg XXXXX qp N |
バウンス生成 |
end msg XXXXX |
メッセージ処理終了 |
triple bounce: discarding bounce/XXXXX |
トリプルバウンス破棄 |
#!/usr/bin/env python3
"""qmail ログ解析スクリプト"""
import re
from dataclasses import dataclass, field
from typing import Dict, List
@dataclass
class MessageSession:
"""メッセージセッション"""
msg_id: int
first_seen: float
sender: str = ""
deliveries: List = field(default_factory=list)
bounced: bool = False
is_triple_bounce: bool = False
class QmailLogParser:
# syslog + qmail タイムスタンプのパターン
SYSLOG_PATTERN = re.compile(
r'^(\w+\s+\d+\s+[\d:]+)\s+(\S+)\s+(\S+):\s+(\d+\.\d+)\s+(.+)'
)
# イベントパターン
EVENT_PATTERNS = {
'new_msg': re.compile(r'^new msg (\d+)'),
'info_msg': re.compile(r'^info msg (\d+): bytes (\d+) from <([^>]*)> qp (\d+) uid (\d+)'),
'starting_delivery': re.compile(r'^starting delivery (\d+): msg (\d+) to (local|remote) (.+)'),
'delivery_result': re.compile(r'^delivery (\d+): (success|failure|deferral): (.*)'),
'bounce': re.compile(r'^bounce msg (\d+) qp (\d+)'),
'triple_bounce': re.compile(r'^triple bounce: discarding bounce/(\d+)'),
}
def __init__(self, log_file: str):
self.sessions: Dict[int, MessageSession] = {}
def parse_line(self, line: str):
match = self.SYSLOG_PATTERN.match(line)
if not match:
return None
timestamp, hostname, program, qmail_ts, message = match.groups()
# qmail 以外のログは除外
if 'qmail' not in program:
return None
# イベントタイプを判定してセッションを更新
for event_type, pattern in self.EVENT_PATTERNS.items():
event_match = pattern.match(message)
if event_match:
self._process_event(event_type, event_match.groups(), float(qmail_ts))
break
バウンスメールとログの紐付け
バウンスメールのファイル名にはタイムスタンプが含まれている。
1768790936.3292.mailserver,S=4071
↑ UNIXタイムスタンプ
これを qmail ログのタイムスタンプと照合することで紐付けが可能になる。
調査結果
4,521件のバウンスメールを分析した結果:
| 原因 | 件数 | 割合 | エラーコード |
|---|---|---|---|
| Cronエラー | 2,157 | 47.7% | 5.7.1 (relay_denied) |
| アンチウイルス通知 | 1,490 | 33.0% | 5.7.1 (relay_denied) |
| 宛先不存在 | 473 | 10.5% | 5.1.1 |
| リレー拒否 | 272 | 6.0% | 5.7.1 |
| MXレコードなし | 22 | 0.5% | 5.4.4 |
| その他 | 107 | 2.3% | – |
qmail ログからの統計:
| 指標 | 値 |
|---|---|
| 総配信試行 | 34,423 |
| 配信成功 | 8,487 (24.7%) |
| 配信失敗 | 25,936 (75.3%) |
| トリプルバウンス | 6,111 |
原因と対策
原因1: Cron ジョブの出力がメール送信される(47.7%)
問題の詳細:
Linux の cron デーモンは、ジョブの標準出力・標準エラー出力を MAILTO で指定されたアドレス(デフォルトは実行ユーザー)にメール送信する。
# cron の動作フロー
1. ジョブ実行
2. 出力があれば sendmail 経由でメール送信
3. MAILTO 未設定の場合は root@hostname 宛
4. hostname が外部ドメインだとリレー拒否でバウンス
発見された問題パターン:
# パターン1: リダイレクトなし(出力がメールされる)
*/1 * * * * /path/to/script.sh
# パターン2: リダイレクト記法ミス
*/5 * * * * /path/to/script.sh > /dev/null 2&>1 # NG: 2&>1 は無効
*/5 * * * * /path/to/script.sh >./dev/null 2>&1 # NG: ./dev/null は相対パス
*/1 * * * * /path/to/script.sh > /dev/null 2>&>1 # NG: 2>&>1 は無効
# パターン3: 正しい記法
*/1 * * * * /path/to/script.sh > /dev/null 2>&1 # OK
*/1 * * * * /path/to/script.sh >/dev/null 2>&1 # OK(スペースなしも可)
リダイレクトの解説:
> /dev/null # 標準出力を /dev/null へ
2>&1 # 標準エラー出力(2)を標準出力(1)と同じ場所へ
# 結果: 両方とも /dev/null へ
# 順序が重要
command > /dev/null 2>&1 # OK: まず stdout を /dev/null に、次に stderr を stdout(=/dev/null) に
command 2>&1 > /dev/null # NG: まず stderr を stdout(=端末) に、次に stdout を /dev/null に
対策:
# 方法1: 出力を破棄
*/1 * * * * /path/to/script.sh > /dev/null 2>&1
# 方法2: ログファイルに出力
*/1 * * * * /path/to/script.sh >> /var/log/myjob.log 2>&1
# 方法3: MAILTO を設定
MAILTO="" # 空にするとメール送信しない
*/1 * * * * /path/to/script.sh
# 方法4: cronic を使用(エラー時のみ通知)
*/1 * * * * /usr/bin/cronic /path/to/script.sh
原因2: アンチウイルスソフトの通知メール(33.0%)
問題の詳細:
DrWeb がメールをスキャンできない場合(ライセンス制限等)、管理者に通知メールを送信する。
Subject: A message has not been checked due to license limitations
Dear Postmaster,
the message with following attributes has not been checked
due to the licenses limitation.
--- Dr.Web report ---
message's envelope (addresses) aren't present in license (protected e-mail's), skipped!
対策:
DrWeb の設定ファイルで通知をブロック:
# /etc/drweb/addresses.conf に追加
to "postmaster@"
または通知自体を無効化:
# /etc/drweb/drweb_qmail.conf
AdminNotify = no
設定変更後は再起動が必要:
service drweb restart
原因3: トリプルバウンスと postmaster 問題
トリプルバウンスとは:
1. 元のメール送信 A → B
↓ 失敗
2. バウンス生成 B → A (from: <>)
↓ 失敗
3. ダブルバウンス → postmaster (from: <#@[]>)
↓ 失敗
4. トリプルバウンス → 破棄
"triple bounce: discarding bounce/XXXXX"
対策:
# qmail の rcpthosts に追加
echo "local-server.example.com" >> /var/qmail/control/rcpthosts
# postmaster エイリアスを設定
echo "[email protected]" > /var/qmail/alias/.qmail-postmaster
qmail 設定の確認ポイント
rcpthosts の確認
cat /var/qmail/control/rcpthosts
リレーを許可するドメインが列挙されている。内部サーバー間でメール転送する場合、相互に許可が必要だ。
badmailfrom の確認
cat /var/qmail/control/badmailfrom
拒否する送信元アドレスのリスト。アンチウイルスの daemon アドレスが誤って登録されていないか確認。
キューの確認
# キュー内のメッセージ数
/var/qmail/bin/qmail-qstat
# キューの詳細
/var/qmail/bin/qmail-qread
監視とアラート設定
バウンス監視スクリプト
#!/bin/bash
# /usr/local/bin/check_bounces.sh
THRESHOLD=100
COUNT=$(grep -c "triple bounce" /var/log/maillog)
if [ "$COUNT" -gt "$THRESHOLD" ]; then
echo "Warning: $COUNT triple bounces detected" | mail -s "Bounce Alert" [email protected]
fi
ログローテーション設定
# /etc/logrotate.d/maillog
/var/log/maillog {
daily
rotate 30
compress
delaycompress
missingok
notifempty
}
ベストプラクティスまとめ
Cron ジョブ設定
| 項目 | 推奨設定 |
|---|---|
| 出力リダイレクト | 必ず > /dev/null 2>&1 または適切なログファイルへ |
| MAILTO | 必要に応じて設定、不要なら空に |
| 記法 | 2>&1 を正確に(2&>1 は間違い) |
qmail 設定
| 項目 | 確認ポイント |
|---|---|
| rcpthosts | 内部サーバー間のリレー許可 |
| postmaster | 到達可能なアドレスを設定 |
| badmailfrom | 誤ったブロックがないか |
監視
| 項目 | 推奨 |
|---|---|
| バウンス数 | 日次で監視、閾値超えでアラート |
| トリプルバウンス | 発生したら即調査 |
| キューサイズ | 異常な増加を監視 |
まとめ
今回の調査で、大量のバウンスメールの約80%が設定ミスに起因していることが判明した。
- Cron の出力リダイレクト問題: 47.7%
- アンチウイルスの通知設定問題: 33.0%
これらは適切な設定により完全に防止可能だ。
メールサーバーの運用では、配信エラーだけでなく、周辺システムからの通知メールの扱いも重要である。定期的なログ解析とバウンスメールの監視により、問題を早期に発見・対処することが大切だ。
付録: 主要な SMTP エラーコード一覧
| コード | カテゴリ | 意味 | 対策 |
|---|---|---|---|
| 5.1.0 | アドレス | 送信者アドレスが無効 | 送信元設定を確認 |
| 5.1.1 | アドレス | 宛先メールボックスが存在しない | 宛先アドレスを確認 |
| 5.1.2 | アドレス | 宛先ドメインが無効 | DNS 設定を確認 |
| 5.2.1 | メールボックス | メールボックス無効(利用停止) | アカウント状態を確認 |
| 5.2.2 | メールボックス | メールボックス容量超過 | 容量を確認・拡張 |
| 5.4.4 | ネットワーク | MX レコードが見つからない | DNS 設定を確認 |
| 5.7.1 | セキュリティ | 配信拒否(ポリシー違反) | リレー許可/認証設定を確認 |
| 5.7.26 | セキュリティ | メール認証失敗 | SPF/DKIM/DMARC 設定を確認 |


コメント