PHPでDMARC集計レポート(XML)を解析する:SimpleXMLでSPF・DKIMの結果を配列化
作成日:2026.09.21
Googleなどのメール受信サービスから届くDMARC集計レポート(XML)をPHPのSimpleXMLで読み込み、report_metadata、policy_published、複数のrecordから送信元IP、件数、disposition、SPF・DKIMの結果を配列へ変換する方法を解説します。欠損項目やXML解析エラー、名前空間、UNIXタイムスタンプの日時変換も扱い、後続の画面表示や保存処理へ渡せる構造を作ります。
目次
以前、GmailのDMARCレポート(XML形式)の読み方ガイドで、DMARCの設定方法や集計レポートに含まれる項目を紹介しました。
今回はその続きとして、保存済みのDMARC集計レポートをPHPで読み込みます。SimpleXMLを使って、レポートのメタデータ、公開されているポリシー、複数のrecordに含まれる送信元IPやSPF・DKIMの結果を、アプリケーションから参照しやすい配列へ変換します。
メールボックスから添付ファイルを取得したり、ZIPを展開したりする処理は扱いません。すでにXMLファイルとして保存されているDMARCレポートを入力にします。CSV出力も行わず、後続処理へ渡せる構造化データを作るところまでを確認します。
前提
今回の前提は次のとおりです。
- PHP 8.3
- SimpleXML、libxml拡張
- ローカルに保存したDMARC集計レポートXML
- コマンド実行はPowerShell
Googleから取得したDMARC集計レポートを参考にしていますが、記事内のサンプルは匿名化しています。ドメイン名にはexample.test、送信元IPには文書用に予約されたIPアドレスを使います。また、複数のrecordを処理する例にするため、2件のレコードを含めています。
DMARC集計レポートのXMLには、レポートを作成した組織や対象期間を示すreport_metadata、公開されているポリシーを示すpolicy_published、認証結果をまとめた複数のrecordが含まれます。
サンプルXMLを用意する
まず、次の内容をsample-dmarc.xmlとして保存します。実際のレポートを使う場合は、ドメイン、メールアドレス、送信元IP、レポートIDなどをそのまま公開しないようにしてください。
<?xml version="1.0" encoding="UTF-8" ?>
<feedback>
<version>1.0</version>
<report_metadata>
<org_name>receiver.example.test</org_name>
<email>dmarc@example.test</email>
<extra_contact_info>https://support.example.test/dmarc</extra_contact_info>
<report_id>report-example-20250101-001</report_id>
<date_range>
<begin>1735689600</begin>
<end>1735775999</end>
</date_range>
</report_metadata>
<policy_published>
<domain>example.test</domain>
<adkim>r</adkim>
<aspf>r</aspf>
<p>none</p>
<sp>none</sp>
<pct>100</pct>
</policy_published>
<record>
<row>
<source_ip>192.0.2.1</source_ip>
<count>3</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.test</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.test</domain>
<result>pass</result>
<selector>selector1</selector>
</dkim>
<spf>
<domain>mail.example.test</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
<record>
<row>
<source_ip>198.51.100.2</source_ip>
<count>1</count>
<policy_evaluated>
<disposition>quarantine</disposition>
<dkim>fail</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.test</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.test</domain>
<result>fail</result>
<selector>selector1</selector>
</dkim>
<dkim>
<domain>partner.example.test</domain>
<result>pass</result>
<selector>selector2</selector>
</dkim>
<spf>
<domain>mail.example.test</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
</feedback>
このサンプルでは、最初のrecordはSPFの評価がfailですが、auth_resultsのSPF結果はpassになっています。policy_evaluatedはDMARCポリシーの評価結果、auth_resultsは認証処理の詳細なので、別の情報として保持します。
2件目のrecordにはDKIM結果を2件入れています。実際のレポートでは、複数の署名結果を保持できるように、DKIMは最初の値だけでなく配列として扱います。
SimpleXMLでXMLを読み込む
SimpleXMLのsimplexml_load_file()は、XMLファイルをSimpleXMLElementオブジェクトとして読み込みます。読み込みに失敗した場合はfalseが返るため、戻り値は=== falseで確認します。
XMLの解析エラーを後から確認できるように、libxml_use_internal_errors(true)を使います。エラーを取得したら、後続の処理へ影響しないようにlibxml_clear_errors()でクリアします。
function loadDmarcXml(string $path): SimpleXMLElement
{
if (!is_file($path)) {
throw new InvalidArgumentException('XMLファイルが見つかりません。');
}
$previous = libxml_use_internal_errors(true);
libxml_clear_errors();
try {
$xml = simplexml_load_file($path);
$errors = libxml_get_errors();
} finally {
libxml_clear_errors();
libxml_use_internal_errors($previous);
}
if ($xml === false) {
$messages = array_map(
static fn (LibXMLError $error): string => trim($error->message),
$errors
);
$detail = $messages === [] ? '詳細不明' : implode('; ', $messages);
throw new RuntimeException(
'DMARCレポートのXMLを解析できませんでした: ' . $detail
);
}
return $xml;
}
解析に失敗した場合は、入力ファイルが空でないか、XMLとして整形式になっているか、文字コード宣言と実際の保存形式が一致しているかを確認します。
この関数はファイルの存在確認とXML解析だけを担当します。メールの添付ファイルを受信したり、外部URLからXMLを取得したりする処理は分けておくと、通信エラーとXML解析エラーを切り分けやすくなります。
要素の値を安全に取り出す
SimpleXMLの要素は、文字列や整数として使う前に明示的にキャストします。項目が存在しない場合や、空要素の場合はnullとして扱う関数を用意します。
function optionalText(?SimpleXMLElement $parent, string $name): ?string
{
if ($parent === null || !isset($parent->{$name})) {
return null;
}
$value = trim((string) $parent->{$name});
return $value === '' ? null : $value;
}
function optionalInteger(?SimpleXMLElement $parent, string $name): ?int
{
$value = optionalText($parent, $name);
if ($value === null || filter_var($value, FILTER_VALIDATE_INT) === false) {
return null;
}
return (int) $value;
}
function unixTimestampToUtc(?string $value): ?DateTimeImmutable
{
if ($value === null || filter_var($value, FILTER_VALIDATE_INT) === false) {
return null;
}
$date = DateTimeImmutable::createFromFormat('!U', $value);
return $date === false
? null
: $date->setTimezone(new DateTimeZone('UTC'));
}
DMARCのdate_rangeはUNIXタイムスタンプで表現されます。ここでは、読み込んだ直後にUTCのDateTimeImmutableへ変換します。表示時のタイムゾーンへ変換したい場合は、利用する画面や保存先の要件に応じて変更してください。
レポートのメタデータを取得する
まず、レポートを作成した組織、レポートID、対象期間を取り出します。
$xml = loadDmarcXml(__DIR__ . '/sample-dmarc.xml');
$metadata = isset($xml->report_metadata)
? $xml->report_metadata
: null;
$dateRange = $metadata !== null && isset($metadata->date_range)
? $metadata->date_range
: null;
$metadataResult = [
'version' => optionalText($xml, 'version'),
'orgName' => optionalText($metadata, 'org_name'),
'email' => optionalText($metadata, 'email'),
'extraContactInfo' => optionalText($metadata, 'extra_contact_info'),
'reportId' => optionalText($metadata, 'report_id'),
'dateRange' => [
'begin' => unixTimestampToUtc(optionalText($dateRange, 'begin')),
'end' => unixTimestampToUtc(optionalText($dateRange, 'end')),
],
];
要素が存在しない場合でも処理を止めず、項目ごとにnullを返すようにしています。ただし、レポートIDなどアプリケーション上必須とする項目がある場合は、nullを許容せず、入力エラーとして扱う設計に変更してください。
日時を確認する場合は、次のようにDateTimeImmutableをフォーマットできます。
$begin = $metadataResult['dateRange']['begin'];
if ($begin instanceof DateTimeImmutable) {
echo $begin->format('Y-m-d H:i:s T'), PHP_EOL;
}
// 2025-01-01 00:00:00 UTC
公開されているDMARCポリシーを取得する
policy_publishedには、対象ドメインや適用するポリシーが入っています。pctは文字列として読み込まれるため、数値として扱う場合は整数へ変換します。
$publishedPolicy = isset($xml->policy_published)
? $xml->policy_published
: null;
$policyResult = [
'domain' => optionalText($publishedPolicy, 'domain'),
'adkim' => optionalText($publishedPolicy, 'adkim'),
'aspf' => optionalText($publishedPolicy, 'aspf'),
'policy' => optionalText($publishedPolicy, 'p'),
'subdomainPolicy' => optionalText($publishedPolicy, 'sp'),
'nonexistentSubdomainPolicy' => optionalText($publishedPolicy, 'np'),
'percentage' => optionalInteger($publishedPolicy, 'pct'),
];
ここで取得するポリシーは、レポートに記録された公開情報です。個々のメールに対して実際に行われた評価結果は、各recordのpolicy_evaluatedから取得します。
複数のrecordを配列へ変換する
DMARC集計レポートの実装で重要なのは、recordが1件だけとは限らないことです。送信元IPや認証結果ごとに複数のrecordが入るため、すべてをforeachで処理します。
function parseRecords(SimpleXMLElement $xml): array
{
$records = [];
foreach ($xml->record as $record) {
$row = isset($record->row) ? $record->row : null;
$policyEvaluated = $row !== null
&& isset($row->policy_evaluated)
? $row->policy_evaluated
: null;
$identifiers = isset($record->identifiers)
? $record->identifiers
: null;
$authResults = isset($record->auth_results)
? $record->auth_results
: null;
$dkimResults = [];
if ($authResults !== null) {
foreach ($authResults->dkim as $dkim) {
$dkimResults[] = [
'domain' => optionalText($dkim, 'domain'),
'result' => optionalText($dkim, 'result'),
'selector' => optionalText($dkim, 'selector'),
];
}
}
$spfResults = [];
if ($authResults !== null) {
foreach ($authResults->spf as $spf) {
$spfResults[] = [
'domain' => optionalText($spf, 'domain'),
'result' => optionalText($spf, 'result'),
];
}
}
$records[] = [
'sourceIp' => optionalText($row, 'source_ip'),
'count' => optionalInteger($row, 'count'),
'policyEvaluated' => [
'disposition' => optionalText($policyEvaluated, 'disposition'),
'dkim' => optionalText($policyEvaluated, 'dkim'),
'spf' => optionalText($policyEvaluated, 'spf'),
],
'identifiers' => [
'headerFrom' => optionalText($identifiers, 'header_from'),
],
'authResults' => [
'dkim' => $dkimResults,
'spf' => $spfResults,
],
];
}
return $records;
}
この処理では、DKIMとSPFの認証結果を常に配列へ入れています。通常は1件しかない場合でも、複数の署名結果や将来の形式差を扱いやすくなります。
policyEvaluatedとauthResultsは似た名前の値を持ちますが、同じものではありません。前者はDMARCポリシーの評価結果、後者はDKIMやSPFそのものの認証結果です。どちらか一方だけを残すと、後から原因を確認しにくくなるため、用途を分けて保存します。
レポート全体を一つの構造へまとめる
ここまでの関数を組み合わせると、レポート全体を次のような連想配列として扱えます。
$result = [
'metadata' => $metadataResult,
'policyPublished' => $policyResult,
'records' => parseRecords($xml),
];
echo 'レポートID: ', $result['metadata']['reportId'], PHP_EOL;
echo '対象ドメイン: ', $result['policyPublished']['domain'], PHP_EOL;
echo 'record件数: ', count($result['records']), PHP_EOL;
foreach ($result['records'] as $record) {
echo $record['sourceIp'], ': ', $record['count'], '件', PHP_EOL;
}
実行結果は次のようになります。
レポートID: report-example-20250101-001
対象ドメイン: example.test
record件数: 2
192.0.2.1: 3件
198.51.100.2: 1件
この時点で、画面表示、データベース保存、集計処理、CSV出力などへ渡せるデータになっています。今回の記事では、利用先ごとの処理は実装しません。配列の代わりにDTOなどのオブジェクトを採用したい場合も、XMLから値を取り出す処理と、利用側の処理を分けておけば置き換えやすくなります。
名前空間付きXMLを扱う場合
今回のサンプルには名前空間を付けていませんが、XMLによってはルート要素や子要素に名前空間が付いている場合があります。その場合、通常の$xml->recordでは要素を取得できないことがあります。
名前空間URIを確認し、registerXPathNamespace()でXPath用の接頭辞を登録してから検索します。
$namespaces = $xml->getDocNamespaces(true);
$defaultNamespace = $namespaces[''] ?? null;
if ($defaultNamespace !== null) {
$xml->registerXPathNamespace('dmarc', $defaultNamespace);
$records = $xml->xpath('/dmarc:feedback/dmarc:record') ?: [];
} else {
$records = iterator_to_array($xml->record);
}
foreach ($records as $record) {
$sourceIp = optionalText(
isset($record->row) ? $record->row : null,
'source_ip'
);
echo $sourceIp, PHP_EOL;
}
名前空間の有無で取得方法が変わるため、実際のレポートを使う場合は、まずルート要素と名前空間を確認してから実装します。名前空間のURIを文字列として決め打ちするのではなく、対象XMLの仕様や実データを確認してください。
解析エラーと欠損項目を分けて扱う
DMARCレポートを処理するときは、少なくとも次の状態を区別します。
- ファイルが存在しない:入力ファイルの配置や取得処理を確認する
- XMLを解析できない:XMLの破損、文字コード、形式変更を確認する
- XMLは正常だが
recordがない:該当データなしとして扱う - 特定の項目だけ存在しない:項目単位で
nullや空配列として扱う - 認証結果が
fail:XML解析エラーではなく、認証結果として扱う
例えば、simplexml_load_file()が成功した後にrecordが0件でも、それはXMLの解析失敗とは限りません。エラーとして処理するか、空のレポートとして記録するかは、利用するアプリケーションの要件で決めます。
また、DMARCレポートの値をそのまま画面へ出力する場合は、HTMLエスケープを行います。ログへ記録するときも、メールアドレスや送信元IPなどの情報を必要以上に残さないようにします。
動作を確認する
PHPのバージョンとSimpleXML拡張が有効になっていることを確認します。PowerShellでは次のように実行できます。
php -v
php -m | Select-String "SimpleXML|libxml"
ここまでのコードをdmarc.phpへ保存し、同じディレクトリにsample-dmarc.xmlを置いて実行します。
php dmarc.php
正常系だけでなく、次の入力でも確認します。
- XMLファイルが存在しない
- 閉じタグが不足した不正なXML
recordが複数ある- DKIM結果が複数ある
countやsource_ipが欠損しているrecordが0件である- 名前空間付きのXMLである
特に、認証結果のfailをXML解析エラーと混同しないことが重要です。XMLを正しく読み込めていても、認証結果としてfailやneutralなどが返る場合があります。
まとめ
DMARC集計レポートのXMLは、PHPのSimpleXMLを使って階層構造のまま読み込めます。
report_metadataからレポートの組織、ID、対象期間を取得するpolicy_publishedから公開されているDMARCポリシーを取得する- 複数の
recordを繰り返し処理し、送信元や認証結果を配列へ変換する - DKIMやSPFの結果は、複数件を保持できる構造にする
- 解析失敗、該当データなし、項目欠損、認証失敗を分けて扱う
今回作成した配列を使えば、画面表示、データベース保存、集計、CSV出力などへつなげられます。どの項目を保存し、どのように表示するかは、DMARCレポートを利用するアプリケーションの目的に合わせて決めるとよいかと思います。
参考資料
奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。
これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。
【制作会社・企業様向けサポート】
Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。
ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。
PHPのcURLで外部API連携を実装する:HTTPメソッド・認証・エラー処理を共通化
2026.09.20
PHPのcURL拡張を使い、GET・POST・PUT・PATCH・DELETEのHTTPリクエストを送信する方法を解説します。APIキーやBearerトークンの扱い、タイムアウト、通信エラーとHTTPエラーの分離、JSONレスポンスの解析までを共通関数にまとめます。
Laravel 13のNamed Rate Limiterでレート制限を実装する:IP・ユーザー・入力値ごとの設定
2026.09.19
LaravelのNamed Rate Limiterを使い、無制限に繰り返されたくない処理へレート制限を追加する方法を解説します。RateLimiter::for()とthrottleミドルウェアの基本から、IPアドレス・ユーザーID・入力値による制限、複数の制限、429レスポンス、ログイン処理への応用、Redis利用時の注意点まで整理します。
Laravel SocialiteでGoogleログインを実装する|OAuth設定から初回登録まで
2026.09.18
Laravel Socialiteを使ったGoogleログインを、OAuthクライアントの設定からコールバック、Laravelユーザーの登録・ログインまで解説します。Googleアカウントは変更されるメールアドレスではなくsubで識別し、初回登録時は検証済みメールアドレスを確認します。既存アカウントへ自動連携しない設計や、セッション管理、Socialite Fakeで確認するテスト項目も紹介します。
Laravel Pintの使い方:PHPコードの書式を整えてCIで検査する
2026.09.15
Laravel Pintを使ってPHPコードの書式を整える方法を紹介します。ローカルでの自動修正、CIで書式違反だけを検査する--test、対象範囲の指定、pint.jsonの設定、Bladeファイルを扱う際の注意点を整理し、Pintを導入する判断基準も説明します。