1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go言語でメール(EML)ファイルのSPFとDKIMを検証するCLIツールを作ってみた

Last updated at Posted at 2026-01-18

はじめに

メールの送信元認証技術であるSPF (Sender Policy Framework) や DKIM (DomainKeys Identified Mail) は、なりすましメール対策として非常に重要です。
しかし、実際に受信したメール(.eml ファイルなど)が正しく認証されているかを手動で確認するのは、ヘッダを読み解く必要があり少々面倒です。
古いレンタルサーバーで受信したあメールは、SPFやDKIMの検証を実施していない場合があります。

そこで今回は、Go言語を使って EMLファイルを読み込み、SPFとDKIMの検証結果を自動で判定・出力するCLIツール を作成しました。
この記事では、そのツールの紹介と実装のポイントを解説します。

作ったもの

指定された .eml ファイルを入力として受け取り、以下の処理を行うコマンドラインツール mailcheck です。

  1. SPF検証: メールの Received ヘッダから送信元IPアドレスを抽出し、送信元ドメインのSPFレコードに基づいて認証結果(Pass/Fail/SoftFailなど)を判定します
  2. DKIM検証: メールに含まれるDKIM署名を検証し、改ざんが行われていないか、署名が有効かを確認します
ソースコード(展開すると見えます)
package main

import (
	"fmt"
	"log"
	"net"
	"net/mail"
	"os"
	"regexp"
	"strings"

	"blitiri.com.ar/go/spf"
	"github.com/emersion/go-msgauth/dkim"
)

// main はアプリケーションのエントリーポイントです。
// コマンドライン引数で指定されたEMLファイルを読み込み、SPFとDKIMの検証結果を出力します。
func main() {
	if len(os.Args) < 2 || os.Args[1] == "-h" || os.Args[1] == "--help" {
		printUsage()
		return
	}

	// 1. EMLファイルを開く
	file, err := os.Open(os.Args[1])
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error opening file: %v\n", err)
		os.Exit(1)
	}
	defer file.Close()

	// 2. ヘッダのみを読み込む (mail.ReadMessageはヘッダを先にパースします)
	msg, err := mail.ReadMessage(file)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error reading email header: %v\n", err)
		os.Exit(1)
	}

	// Return-Path または From からドメインを抽出
	returnPath := msg.Header.Get("Return-Path")
	returnPath = strings.Trim(returnPath, "<> \t\n")
	if returnPath == "" {
		returnPath = msg.Header.Get("From")
	}
	domain := extractDomain(returnPath)
	// 3. SPFチェックに必要な情報の取得
	for _, v := range msg.Header["Received"] {
		senderIP := extractIP(v)
		if senderIP == nil {
			continue
		}
		helo := extractHelo(v)
		if helo == "" {
			helo = domain
		}
		// 4. SPF検証の実行
		// CheckHostはDNSに問い合わせてSPFレコードを検証します
		result, err := spf.CheckHostWithSender(senderIP, helo, returnPath)

		if err != nil {
			fmt.Printf("SPF Error: %v\n", err)
			log.Printf("%s %s %s", senderIP, helo, returnPath)
		}
		fmt.Printf("SPF IP: %s Result: %s\n", senderIP, result) // pass, fail, softfail, neutral 等
	}

	// 5. DKIM検証
	if _, err := file.Seek(0, 0); err != nil {
		fmt.Fprintf(os.Stderr, "Error seeking file: %v\n", err)
		os.Exit(1)
	}

	verifications, err := dkim.Verify(file)
	if err != nil {
		// Verify自体がエラーを返す場合は致命的なフォーマットエラー等の可能性が高いが、
		// 署名が見つからない場合などはここには来ず verifications が空になるかエラーが含まれる
		fmt.Printf("DKIM Verify Error: %v\n", err)
	}

	if len(verifications) == 0 {
		fmt.Println("DKIM Result: none (no signatures found)")
	}

	for _, v := range verifications {
		if v.Err == nil {
			fmt.Printf("DKIM Result: pass (domain: %s)\n", v.Domain)
		} else {
			fmt.Printf("DKIM Result: fail (domain: %s, error: %v)\n", v.Domain, v.Err)
		}
	}
}

// printUsage はツールの使用方法を標準出力に表示します。
func printUsage() {
	fmt.Println("Usage: mailcheck <path/to/email.eml>")
	fmt.Println("Checks SPF and DKIM authentication results for the given EML file.")
}

// extractDomain はメールアドレス文字列からドメイン部分を抽出して返します。
// 例: "user@example.com" -> "example.com"
func extractDomain(addr string) string {
	if strings.Contains(addr, "@") {
		parts := strings.Split(addr, "@")
		domain := strings.Trim(parts[len(parts)-1], "> ")
		return domain
	}
	return addr
}

// extractIP はReceivedヘッダの文字列から送信元のIPアドレスを抽出します。
// [] や () で囲まれたIPアドレス、あるいは単独のIPアドレスを探します。
func extractIP(received string) net.IP {
	// IPv4 pattern
	// Simple pattern to find IP in [] or () or just plain, prefer brackets
	// Typical formats: "from ... [1.2.3.4]", "from ... (1.2.3.4)"
	
	// Try to find IP enclosed in brackets first (most reliable for Received headers)
	reBrackets := regexp.MustCompile(`\[(::ffff:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]|\[([a-fA-F0-9:]+)\]`)
	matches := reBrackets.FindStringSubmatch(received)
	if len(matches) > 0 {
		if matches[2] != "" {
			return net.ParseIP(matches[2])
		}
		if matches[3] != "" {
			return net.ParseIP(matches[3])
		}
	}

	// Try parenthesis
	reParens := regexp.MustCompile(`\((::ffff:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\)|\(([a-fA-F0-9:]+)\)`)
	matches = reParens.FindStringSubmatch(received)
	if len(matches) > 0 {
		if matches[2] != "" {
			return net.ParseIP(matches[2])
		}
		if matches[3] != "" {
			return net.ParseIP(matches[3])
		}
	}

	// Fallback to simple IPv4 search
	reV4 := regexp.MustCompile(`\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b`)
	matchesV4 := reV4.FindStringSubmatch(received)
	if len(matchesV4) > 1 {
		return net.ParseIP(matchesV4[1])
	}

	return nil
}

// extractHelo はReceivedヘッダの文字列からHELO/EHLOホスト名を抽出します。
// "from" の直後にある文字列を取得します。
func extractHelo(recived string) string {
	re := regexp.MustCompile(`(?i)from\s+([^\s(]+)`)
	matches := re.FindStringSubmatch(recived)
	if len(matches) > 1 {
		return matches[1]
	}
	return ""
}

使い方

1. ビルド

まずはソースコードをビルドします。

go build

2. 実行

解析したいEMLファイルのパスを引数に渡して実行します。

./mailcheck sample.eml

実行結果の例

SPF IP: 209.85.220.41 Result: pass
DKIM Result: pass (domain: google.com)

このように、SPFとDKIMの検証結果がシンプルに出力されます。

実装のポイント

実装には主に以下のライブラリと標準パッケージを使用しています。

SPFチェックの仕組み

SPF検証では、「どのIPアドレスから送られてきたか」という情報が不可欠です。このツールでは、メールヘッダの Received フィールドを解析して、送信元のIPアドレスを抽出しています。

// ReceivedヘッダからIPアドレスを正規表現で抽出する部分(抜粋)
func extractIP(received string) net.IP {
    // [1.2.3.4] のような形式を優先して探す
    reBrackets := regexp.MustCompile(`\[(::ffff:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]`)
    // ...
}

抽出したIPアドレスと、Return-Path (または From) から得られるドメインを使って、SPFライブラリの CheckHostWithSender 関数を呼び出します。

DKIMチェックの仕組み

DKIM検証は go-msgauth/dkim ライブラリを使うことで非常にシンプルに実装できます。ファイルの読み込み位置を先頭に戻してから dkim.Verify に渡すだけです。

// ファイルポインタを先頭に戻す
file.Seek(0, 0)

// 検証実行
verifications, err := dkim.Verify(file)
for _, v := range verifications {
    if v.Err == nil {
        fmt.Printf("DKIM Result: pass (domain: %s)\n", v.Domain)
    } else {
        fmt.Printf("DKIM Result: fail (domain: %s, error: %v)\n", v.Domain, v.Err)
    }
}

まとめ

Go言語のエコシステム(強力な標準ライブラリとサードパーティ製の認証ライブラリ)を活用することで、比較的少ないコード量で実用的なメール認証チェッカーを作ることができました。
メールサーバーのログ調査や、怪しいメールの解析などに役立てば幸いです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?