2026-04-09

zo3検証ツール 簡易MIMEメール展開スクリプト Expand-MimeMessage.ps1

<#
.SYNOPSIS
    zo3検証ツール 簡易MIMEメール展開スクリプト
    Expand-MimeMessage.ps1

.DESCRIPTION
    EML ファイルを解析し、本文と添付ファイルを指定ディレクトリに展開する。
    New-MimeMessage.ps1 で生成した EML の検証用途を主目的とする。
    検証環境・クローズド環境での使用を前提とする。

.PARAMETER EmlFile
    展開対象の EML ファイルパス。

.PARAMETER OutDir
    展開先ディレクトリ。存在しない場合は自動作成。デフォルト: .\out

.EXAMPLE
    .\Expand-MimeMessage.ps1 -EmlFile test.eml

.EXAMPLE
    .\Expand-MimeMessage.ps1 -EmlFile attach.eml -OutDir C:\tmp\expand

.OUTPUTS
    <OutDir>\body.txt        : 本文テキスト
    <OutDir>\<filename>      : 添付ファイル(ファイル名はメールヘッダから取得)

.NOTES
    対応エンコード : Base64 添付, RFC2231 ファイル名デコード
    非対応        : HTML パート, Base64 エンコード本文, multipart/alternative
    前提          : New-MimeMessage.ps1 が生成した CRLF 区切り EML

    MIT License
    Copyright (c) 2026  yoshio@zo3
#>


param(
    [string]$EmlFile,
    [string]$OutDir = ".\out"
)

$CRLF = "`r`n"

# 出力先
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null

# 全文取得
#$content = Get-Content -Raw -Path $EmlFile
$content = [System.IO.File]::ReadAllText( $EmlFile, [System.Text.Encoding]::UTF8)
$content = $content -replace "`r`n", "`n" -replace "`r", "`n" -replace "`n", $CRLF

# ヘッダとボディ分離
$parts = $content -split "$CRLF$CRLF", 2
$headers = $parts[0]
$body = $parts[1]

# boundary取得
$boundary = $null
if ( $headers -match 'boundary="([^"]+)"') {
    $boundary = $matches[1]
}

# RFC2231 decode
function Decode-RFC2231($s) {
    if ($s -match "UTF-8''(.+)") {
        $enc = $matches[1]
        $bytes = @()
        for ($i = 0; $i -lt $enc.Length; ) {
            if ($enc[$i] -eq '%') {
                $bytes += [Convert]::ToByte($enc.Substring($i + 1, 2), 16)
                $i += 3
            }
            else {
                $bytes += [byte][char]$enc[$i]
                $i++
            }
        }
        return [System.Text.Encoding]::UTF8.GetString($bytes)
    }
    return $s
}

# Base64 decode
function Decode-Base64($text) {
    $clean = ($text -replace '\s', '')
    return [Convert]::FromBase64String($clean)
}

if ( $null -eq $boundary ) {
    # 単一パート 本文のみ
    $textPath = Join-Path $OutDir "body.txt"
    Set-Content -Path $textPath -Value $body -Encoding UTF8
}
else {
    # マルチパート分解
    #$sections = $body -split "--$boundary"
    $escaped = [Regex]::Escape("--$boundary")
    $sections = $body -split $escaped

    foreach ($sec in $sections) {

        if ($sec -match "--\s*$") { continue }
        if ($sec.Trim() -eq "") { continue }

        $sp = $sec -split "$CRLF$CRLF", 2
        if ( $sp.Count -lt 2 ) { continue }

        $h = $sp[0]
        $b = $sp[1].Trim()

        # 添付判定
        if ( -not ( $h -match "Content-Disposition: attachment" ) ) {
            # 本文
            $textPath = Join-Path $OutDir "body.txt"
            Set-Content -Path $textPath -Value $b -Encoding UTF8
        }
        else {
            # filename取得(優先:filename*)
            $filename = "unknown.bin"

            if ($h -match "filename\*=(.+)") {
                # 修正
                $raw = $matches[1].Trim() -replace ';.*$', ''
                $filename = Decode-RFC2231 $raw
                #$filename = Decode-RFC2231 $matches[1].Trim()
                $filename = [System.IO.Path]::GetFileName( $filename )  # パス成分を除去
            }
            elseif ($h -match 'filename="([^"]+)"') {
                $filename = $matches[1]
            }

            # Base64デコード
            if ($h -match "base64") {
                $bytes = Decode-Base64 $b
                $outPath = Join-Path $OutDir $filename
                [System.IO.File]::WriteAllBytes($outPath, $bytes)
            }
        }
    }
}