✉️

PowerShell:emlのファイル名を日時_メール件名に一括リネームする

に公開
8

需要が限定的ですが、作成要望があったので、システムから出力される大量のemlファイルのファイル名を日時_メール件名に一括でリネームするPowerShellを書いてみた。

スクリプトの内容

  1. 禁止文字の削除用Functionを作成
    ※メールの件名にはファイル名に使用できない文字列があるので、その対策。
  2. フォルダ選択ダイアログを表示して、フォルダのパスの入力
  3. emlファイルを読み込んで、件名と送信日時を取得
  4. 件名から禁止文字を削除
  5. 送信日時+件名でファイル名を置換

更新内容

  • 共有フォルダの選択ができなかったため、フォルダ選択ダイアログ表示はやめてInputBoxに入力する方式に変更
  • emlファイルを[ADODB.Stream]、[CDO.Message]を使って読み込み、件名や送信日時を取得する方式に変更
  • COMオブジェクトのファイルロック(掴みっぱなし)対策
  • 同名ファイルの衝突回避
  • ファイル名長制限対策

完成したスクリプト

eml_rename_path_input.ps1
<#
.SYNOPSIS
    GUIで選択したフォルダ内のEMLファイルを読み込み、送信日時と件名でリネームする。
#>

# -----------------------------------------------------------------------------
# 禁止文字の削除と長さ制限用Function
Function Get-SafeFileName {
    param(
        [Parameter(Mandatory=$true,
        Position=0,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true)]
        [String]$Name
    )

    # 1. 空文字チェック(最初から何もない場合はダミー名を返す)
    if ([string]::IsNullOrWhiteSpace($Name)) { return "NoSubject" }

    # 2. 基本的な禁止文字の削除
    $invalidChars = [IO.Path]::GetInvalidFileNameChars() -join ''
    $re = "[{0}]" -f [RegEx]::Escape($invalidChars)
    $cleanName = $Name -replace $re, ''

    # 3. 件名に混入しがちな改行コード・タブの削除
    $cleanName = $cleanName -replace "[\r\n\t]", ''

    # 4. 前後の不要な空白と、末尾のピリオドを削除(Trim)
    $cleanName = $cleanName.Trim().TrimEnd('.')

    # 5. 空文字チェック(禁止文字を消した結果、文字が消滅した場合)
    if ([string]::IsNullOrWhiteSpace($cleanName)) { return "InvalidSubject" }

    # 6. Windows予約語(デバイス名)の回避
    # ^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$ に完全一致するかチェック
    if ($cleanName -match "^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$") {
        # そのままでは使えないため、アンダースコアを付けて無害化
        $cleanName = "_" + $cleanName
    }

    # 7. ファイル名長制限(MAX_PATH対策として、安全に60文字で切り捨てる)
    if ($cleanName.Length -gt 60) {
        $cleanName = $cleanName.Substring(0, 60)
    }

    return $cleanName
}
# -----------------------------------------------------------------------------

# アセンブリの読み込み
Add-Type -AssemblyName System.Windows.Forms

# 【UI改善】FolderBrowserDialogの作成と表示
$browser = New-Object System.Windows.Forms.FolderBrowserDialog
$browser.Description = "EMLファイルが保存されているフォルダを選択してください。"
$browser.ShowNewFolderButton = $false # 読み込み専用処理のため、新規フォルダ作成ボタンを非表示

# ダイアログを表示し、ユーザーが「OK」を押したか判定
$dialogResult = $browser.ShowDialog()

if ($dialogResult -ne [System.Windows.Forms.DialogResult]::OK -or [string]::IsNullOrWhiteSpace($browser.SelectedPath)) {
    [System.Windows.Forms.MessageBox]::Show("フォルダ選択がキャンセルされました。処理を終了します。", "終了", "OK", "Information")
    exit
}

# 選択された正しい絶対パスを変数へ格納
$path = $browser.SelectedPath

Write-Host "処理を開始します。対象フォルダ: $path" -ForegroundColor Cyan

# 対象ファイルの取得(コンテナ除外)
$targets = Get-ChildItem -Path $path -Filter "*.eml" -Recurse | Where-Object { -not $_.PSIsContainer }

if ($targets.Count -eq 0) {
    [System.Windows.Forms.MessageBox]::Show("対象フォルダ内にEMLファイルが見つかりませんでした。`n($path)", "終了", "OK", "Information")
    exit
}

$successCount = 0
$errorCount = 0

foreach ($file in $targets) {
    try {
        $stream = New-Object -ComObject ADODB.Stream
        $msg = New-Object -ComObject CDO.Message

        $stream.Open()
        $stream.LoadFromFile($file.FullName)
        $msg.DataSource.OpenObject($stream, "_Stream")
        
        $sentDate = $msg.SentOn
        if ($null -eq $sentDate -or $sentDate -eq '1899/12/30 0:00:00') {
            $dateStr = $file.LastWriteTime.ToString("yyyyMMdd_HHmmss")
        } else {
            $dateStr = ([datetime]$sentDate).ToString("yyyyMMdd_HHmmss")
        }

        $subjectStr = Format-ValidFileName -Name $msg.Subject

        $stream.Close()
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($stream) | Out-Null
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($msg) | Out-Null
        [System.GC]::Collect()
        [System.GC]::WaitForPendingFinalizers()

        $baseNewName = "${dateStr}_${subjectStr}"
        $extension = $file.Extension
        $newName = $baseNewName + $extension
        $newFullName = Join-Path -Path $file.DirectoryName -ChildPath $newName

        $counter = 1
        while (Test-Path -Path $newFullName) {
            if ($file.FullName -eq $newFullName) { break } 
            $newName = "${baseNewName}_${counter}${extension}"
            $newFullName = Join-Path -Path $file.DirectoryName -ChildPath $newName
            $counter++
        }

        if ($file.FullName -ne $newFullName) {
            Rename-Item -Path $file.FullName -NewName $newName -ErrorAction Stop
            $successCount++
        }
    } catch {
        Write-Warning "ファイル処理エラー: $($file.Name) - $($_.Exception.Message)"
        $errorCount++
    }
}

[System.Windows.Forms.MessageBox]::Show("処理が完了しました。`n成功: ${successCount}`nエラー: ${errorCount}件", "結果レポート", "OK", "Information")

実行用bat

start.bat
powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy RemoteSigned -File eml_rename_path_input.ps1

関連スクリプト

https://zenn.dev/8chikuwa3/articles/5718421d92f745

Discussion

ウマヤスウマヤス

はちくわ様

突然のご連絡失礼いたします。
ウマヤスと申します。
今回は御礼とご依頼があり、ご連絡いたしました。

業務でメールを整理する必要が生まれ、何か良い方法がないか?と検索したところ、このスクリプトを発見しました。まさに私が欲していたものです。スクリプトの公開ありがとうございます。

現在は私的利用の範囲内で使用しています。このスクリプトを編集・加工し、私が所属する組織内で使用する許諾を以下の内容でいただけないでしょうか。

スクリプトの使用に際して、発生したあらゆる事象の責任は使用者が負います。また、引用元であるこのページのURL及び作者名義(はちくわ様)を明記します。商業利用もいたしません。

お忙しいところ恐縮ですが、許諾の可否についてご回答お願いいたします。

1
はちくわはちくわ

ご連絡ありがとうございます。
こんなスクリプトでもお役に立てているのようで、嬉しく思います。

特に利用制限は考えていないため、使用者の責任の範囲内であれば、ウマヤス様の組織内で利用していただいて構いません。

1
ウマヤスウマヤス

ご回答ありがとうございます。

非常に助かります。
私はプログラムの知識はほぼゼロなので、ゼロベースからスクリプトを書くのは不可能でした。
このスクリプトのおかげで業務の効率が向上しました。

今後ともよろしくお願いいたします。

1
nonakinonaki

はちくわ様
ご教授お願いしたく、こちらへ書き込みいたしております。
私もメール1通ずつネーム変更していたので、一括で出来ないものか?とネットで調べてるところで、こちらのスクリプトを拝見し、何とか環境下で使用できるところまで漕ぎつきました。
ただ私の環境下では日時の取得がうまくいかず、今は日付-件名へのリネームを試みています。

"yyyymmdd-hhmm"部分を「yyyyMMdd」に書き換えて動作確認しておりますが
処理はうまくいっていますが、時間の部分を消してるため、エラーメッセージが出ます。
時間の部分を省く方法、どの部分からどの部分を省略するとよいのか教えていただけませんでしょうか?
お忙しいとは存じますが、何卒ご対応いただけますよう宜しくお願い致します。

はちくわはちくわ

ご連絡ありがとうございます。
不具合があったので、スクリプトを少しだけ修正しております。

日時の取得は、emlファイルのヘッダー部分を見て行ってます。

$GetEml = (Get-Content $_.FullName)[5..100]

ここでemlファイルの5行目から100行目まで見て、Date行を確認してます。
100行目までの間にDateがないと日時の取得ができないので、環境に応じて修正してください。
※emlファイルをメモ帳等で開くと確認ができます。

日時のフォーマットはここで指定しているので、ここを変更するのみになります。

- $date = ([datetime]$matches.ReTime).ToString("yyyyMMdd_HHmmss")
+ $date = ([datetime]$matches.ReTime).ToString("yyyyMMdd")

ただし、同じ日に同じ件名のメールがあるとファイル名の重複エラーになってしまうため、日時まで含めた方がエラーが起きにくいかなと思います。

はちくわはちくわ

数年ぶりに送信日時の取得方法を見直ししましたので、もしまだ必要であればお試しください。
(フォルダ指定は、ダイアログの表示はせず、パスを入力する方式に変更もしてます。)

1
nonakinonaki

早々の返信ありがとうございます。
修正、わたくしの方の環境に適したスクリプトも載せていただきありがとうございます。

日時については、はちくわ様のおっしゃる通り、先方の送信日付・時間が分かった方が、
後日メール閲覧時検索しやすいと思っております。

私の環境下で私の分かる範囲、ネットで調べた限り試行錯誤した結果、
スクリプトをあまり扱わず[HHmmss]を外した方が日付違いが起きませんでしたので、
それで行こうと思ったのですが私以外の方が使用した場合、エラーメッセージで戸惑われ
一通一通開いてファイル名変更することになると思い、無礼覚悟でコメントさせていただきました。

このスクリプトの活用で、毎日数時間やっても終わらず溜まっていくリネーム作業が
数分で終わる見込みなので、リネームを担当する者の負担軽減にもなるので、大変ありがたく思います。

1