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)
            }
        }
    }
}

zo3検証ツール 簡易MIMEメール作成スクリプト New-MimeMessage.ps1

<#
.SYNOPSIS
    zo3検証ツール 簡易MIMEメール作成スクリプト
    New-MimeMessage.ps1

.DESCRIPTION
    指定したヘッダ情報・本文・添付ファイルから RFC 2822 準拠の EML ファイルを生成する。
    生成した EML は curl の --upload-file で SMTP 送信に使用できる。
    検証環境・クローズド環境での使用を前提とする。

.PARAMETER From
    送信者アドレス。例: user1@example.test

.PARAMETER To
    宛先アドレス(複数指定可)。例: user2@example.test, user3@example.test

.PARAMETER Subject
    件名。日本語可(RFC2047 Base64 エンコード)。

.PARAMETER Body
    本文。日本語可(UTF-8 / 8bit)。

.PARAMETER Attachments
    添付ファイルパスの配列(省略可)。日本語ファイル名可(RFC2231 エンコード)。

.PARAMETER OutFile
    出力する EML ファイルパス。デフォルト: mail.eml

.EXAMPLE
    .\New-MimeMessage.ps1 -From user1@example.test -To user2@example.test `
        -Subject "テスト" -Body "本文" -OutFile test.eml

.EXAMPLE
    .\New-MimeMessage.ps1 -From user1@example.test -To user2@example.test `
        -Subject "添付テスト" -Body "本文" `
        -Attachments @("C:\tmp\資料.pdf", "C:\tmp\image.png") -OutFile attach.eml

.NOTES
    対応エンコード : Subject=RFC2047 B, ファイル名=RFC2231, 本文=UTF-8/8bit
    非対応        : HTML メール, multipart/alternative, DKIM 署名, CC/BCC ヘッダ自動付与
    送信例        :
        curl.exe --url smtp://mailserver:25 `
            --mail-from user1@example.test `
            --mail-rcpt user2@example.test `
            --upload-file test.eml

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

param(
    [string]$From,
    [string[]]$To,
    [string]$Subject,
    [string]$Body,
    [string[]]$Attachments = @(),
    [string]$OutFile = "mail.eml"
)

# ===== 基本 =====
$CRLF = "`r`n"

# ===== RFC2047 Subject =====
function Encode-Subject( $string ) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes( $string )
    $b64data = [Convert]::ToBase64String( $bytes )
    return "=?UTF-8?B?${b64data}?="
}

# ===== Base64(76文字折返し)=====
function To-Base64Lines( $bytes ) {
    $b64data = [Convert]::ToBase64String( $bytes )
    return ( ${b64data} -split "(.{1,76})" | Where-Object { $_ }) -join $CRLF
}

# ===== RFC2231 filename* =====
function Encode-RFC2231( $string ) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes( $string )
    $enc = ""
    foreach ( $byte in $bytes ) {
        if (
            ( $byte -ge 0x30 -and $byte -le 0x39 ) -or
            ( $byte -ge 0x41 -and $byte -le 0x5A ) -or
            ( $byte -ge 0x61 -and $byte -le 0x7A ) -or
            $byte -in 0x2D, 0x2E, 0x5F, 0x7E
        ) {
            $enc += [char]$byte
        }
        else {
            $enc += "%" + $byte.ToString( "X2" )
        }
    }
    return "UTF-8''$enc"
}

# ===== Date / Message-ID =====
$now = Get-Date
#$dateStr = $now.ToString("ddd, dd MMM yyyy HH:mm:ss K", [System.Globalization.CultureInfo]::InvariantCulture)
$tz = $now.ToString("zzz").Replace(":", "")
$dateStr = $now.ToString("ddd, dd MMM yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture) + " $tz"

$domain = ($From -split "@")[-1]
$msgid = "<" + [guid]::NewGuid().ToString() + "@$domain>"

# ===== boundary =====
$boundary = "----=_Boundary_" + [guid]::NewGuid().ToString("N")

# ===== ヘッダ =====
$headers = @()
$headers += "Date: $dateStr"
$headers += "Message-ID: $msgid"
$headers += "From: $From"
$headers += "To: " + ($To -join ", ")
$headers += "Subject: " + ( Encode-Subject $Subject )
$headers += "MIME-Version: 1.0"

if ($Attachments.Count -gt 0) {
    $headers += "Content-Type: multipart/mixed; boundary=""$boundary"""
}
else {
    $headers += "Content-Type: text/plain; charset=UTF-8"
    $headers += "Content-Transfer-Encoding: 8bit"
}

# ===== 本文 =====
$bodyLines = @()
# 修正(本文取り込み前に正規化)
$Body = $Body -replace "`r`n", "`n" -replace "`n", $CRLF

if ( $Attachments.Count -eq 0 ) {

    $bodyLines += $Body
}
else {
    # 本文パート
    $bodyLines += "--$boundary"
    $bodyLines += "Content-Type: text/plain; charset=UTF-8"
    $bodyLines += "Content-Transfer-Encoding: 8bit"
    $bodyLines += ""
    $bodyLines += $Body

    # 添付
    foreach ( $file in $Attachments ) {

        $bytes = [System.IO.File]::ReadAllBytes( $file )
        $name = [System.IO.Path]::GetFileName( $file )

        # ASCIIフォールバック
        $nameAscii = $name -replace '[^\x20-\x7E]', '_'

        # RFC2231
        $name2231 = Encode-RFC2231 $name

        $bodyLines += ""
        $bodyLines += "--$boundary"
        $bodyLines += "Content-Type: application/octet-stream; name=""$nameAscii"""
        $bodyLines += "Content-Transfer-Encoding: base64"
        $bodyLines += "Content-Disposition: attachment; filename=""$nameAscii""; filename*=$name2231"
        $bodyLines += ""
        $bodyLines += (To-Base64Lines $bytes)
    }
    # 終端
    $bodyLines += ""
    $bodyLines += "--$boundary--"

}


# ===== 結合 =====
$all = ( $headers -join $CRLF ) + $CRLF + $CRLF + ( $bodyLines -join $CRLF )

# ===== 出力(CRLF維持)=====
#[System.IO.File]::WriteAllText( $OutFile , $all , [System.Text.Encoding]::ASCII)
[System.IO.File]::WriteAllBytes($OutFile, [System.Text.Encoding]::UTF8.GetBytes($all))

2026-04-06

curl でのプロトコルテストコマンド


前提確認

curl --version  # protocols: に smtp, pop3, imap, ftp が含まれているか確認

SMTP / SMTPS

# SMTP (25番 or 587番) — メール送信テスト
curl smtp://mail.example.com:25 --mail-from sender@example.com --mail-rcpt recipient@example.com --upload-file mail.txt -v

# SMTP STARTTLS (587番)
curl smtp://mail.example.com:587 --mail-from sender@example.com --mail-rcpt recipient@example.com --upload-file mail.txt --ssl-reqd -v

# SMTPS (465番 — 最初からTLS)
curl smtps://mail.example.com:465 --mail-from sender@example.com --mail-rcpt recipient@example.com --upload-file mail.txt -v

# 認証あり
curl smtps://mail.example.com:465 -u "user@example.com:password" --mail-from user@example.com --mail-rcpt to@example.com --upload-file mail.txt -v

mail.txt の中身(最低限):

From: sender@example.com
To: recipient@example.com
Subject: test

Hello

POP3 / POP3S

# POP3 (110番) — メール一覧
curl pop3://mail.example.com -u "user:pass" -v

# 特定メール取得 (メッセージ番号1)
curl pop3://mail.example.com/1 -u "user:pass" -v

# POP3 STARTTLS
curl pop3://mail.example.com --ssl-reqd -u "user:pass" -v

# POP3S (995番)
curl pop3s://mail.example.com -u "user:pass" -v

IMAP / IMAPS

# IMAP (143番) — INBOXの一覧
curl imap://mail.example.com/INBOX -u "user:pass" -v

# 特定メール取得 (UID=1)
curl "imap://mail.example.com/INBOX;UID=1" -u "user:pass" -v

# IMAP STARTTLS
curl imap://mail.example.com/INBOX --ssl-reqd -u "user:pass" -v

# IMAPS (993番)
curl imaps://mail.example.com/INBOX -u "user:pass" -v

# フォルダ一覧
curl imaps://mail.example.com/ -u "user:pass" --request "LIST \"\" \"*\"" -v

FTP / FTPS

# FTP (21番) — ファイル一覧
curl ftp://ftp.example.com/ -u "user:pass" -v

# ファイルダウンロード
curl ftp://ftp.example.com/file.txt -u "user:pass" -o file.txt -v

# ファイルアップロード
curl ftp://ftp.example.com/upload.txt -u "user:pass" --upload-file upload.txt -v

# FTPS (Explicit TLS — STARTTLS方式, 21番)
curl ftp://ftp.example.com/ -u "user:pass" --ssl-reqd -v

# FTPS (Implicit TLS — 990番)
curl ftps://ftp.example.com/ -u "user:pass" -v

共通オプション

オプション 用途
-v 詳細ログ(必須級)
--ssl-reqd STARTTLSを強制
-k 自己署名証明書を無視(テスト環境向け)
--trace-ascii - バイナリ含む全通信ダンプ
--resolve host:port:IP DNS代わりに直接IP指定
-u "user:pass" 認証


2026-03-29

PowerShell 7は魔法なのに、なぜ8年経っても偽物の5.1が座っているのか…

7年、8年ほど前に始まった PowerShell Ver.6 の頃の希望は、とっくの昔に消えてしまったな…いつまでもWindows標準はゴミの Windows PowerShell 5.1 のまま。
そして世の中の人は、ゴミの方しか認知していない。
いや、PowerShell自体ほとんど認知されてない。されてても「Windowsの管理ツール」くらいにしか思われてない…

PowerShell 7は結構すごいのに…
.NETを直接使えるから、何も追加で入れなくても何でも作れる、魔法みたいなスクリプト言語なんだ。
クラス書けるし、型もガッツリ使える。
かなり優秀なスクリプト環境だと思ってる。
ただし、インストールが必要という一点で全てが台無しになる…クソだな、これ。Windowsに最初から入ってるのが5.1のままってのが、全部の元凶…
「みんなに勧めたいけど、インストールさせてからじゃないと話が始まらない」って状況が、歯がゆい。