---
title: '【PowerShell】Outlookの.msgをJSONに変換するMSG Studio v3（GUI・CLI対応）'
url: 'https://automationse.net/it-related/msg-studio-v3'
markdown: 'https://automationse.net/it-related/msg-studio-v3.md'
date: '2026-10-11'
description: '【PowerShell】Outlookの.msgをJSONに変換する「MSG Studio v3」── GUIルール編集・一括変換・Jenkins連携 Outlookのメールを問い合わせ票へ転記する際に、チケット番号や問い合わせ者を手作業で探していないでしょうか。この記事では、Outlookの.msgをPowerShellで読み込み、ルールに沿ってJSONに変換するツールを紹介し…'
taxonomy:
  category:
    - IT関連
  tag:
    - PowerShell
    - Outlook
    - プリザンター
    - 自動化
---

## [【PowerShell】Outlookの.msgをJSONに変換するMSG Studio v3（GUI・CLI対応）](https://automationse.net/it-related/msg-studio-v3)

  公開日 2026.10.11 更新日 2026.10.11  [PowerShell](https://automationse.net/tag:PowerShell#body-wrapper) [Outlook](https://automationse.net/tag:Outlook#body-wrapper) [プリザンター](https://automationse.net/tag:%E3%83%97%E3%83%AA%E3%82%B6%E3%83%B3%E3%82%BF%E3%83%BC#body-wrapper) [自動化](https://automationse.net/tag:%E8%87%AA%E5%8B%95%E5%8C%96#body-wrapper)  

# 【PowerShell】Outlookの.msgをJSONに変換する「MSG Studio v3」── GUIルール編集・一括変換・Jenkins連携

Outlookのメールを問い合わせ票へ転記する際に、チケット番号や問い合わせ者を手作業で探していないでしょうか。この記事では、**Outlookの.msgをPowerShellで読み込み、ルールに沿ってJSONに変換するツール**を紹介します。設定画面（GUI）で抽出ルールを変更でき、普段の処理は画面を開かずスクリプトモードで実行できます。

> **公開するコードについて**：以下のコードブロックは MSG Studio v3 の最終配布ZIP内のファイルを全文掲載しています。省略・修正していません。`.ps1` の原本はShift-JIS（CP932）、この記事のMarkdownとJSONはUTF-8です。記事からコピーして保存する場合、**Windows PowerShell 5.1向けの.ps1はCP932で保存**してください。
> 
> **動作検証**：本記事のコードは実機Windows上でのGUI・COM動作確認をしていないサンプルです。Classic Outlookが必要で、New Outlook単体では動きません。

## 何ができる？

| 機能 | 内容 |
|---|---|
| GUIルール編集 | 正規表現・キーワード・直接取得のルールを追加、編集、複数行削除、保存 |
| 解析プレビュー | メール本文を模したサンプルで抽出を確認 |
| 単一ファイル変換 | 1つの.msgからJSONを生成 |
| フォルダ一括変換 | 指定フォルダ内の.msgを一括処理 |
| CLI・無人実行 | GUIなしで実行、ログと終了コードに対応 |
| プリザンター向け出力 | Title、ClassHash、DescriptionHash、Statusなどの構造でJSON化 |

**このツールはJSON生成まで**です。プリザンターAPIへのPOSTや元の.msgの削除は行いません。

## 処理の流れ

```text
Outlook .msg
   │
   ▼
Classic Outlook COM（件名・本文・送信者など）
   │
   ▼
rules.json（GUIで管理できる抽出条件）
   │
   ▼
MsgRules.Core.ps1（Regex / Keywords / Literal）
   │
   ├── GUIから個別変換
   └── Run-Script.ps1から一括変換
             │
             ▼
          JSONファイル
```

## 動作環境と導入

Windows 11、Windows PowerShell 5.1、**Classic Outlook**を用意します。標準のWindows Formsを利用し、PowerShell追加モジュールは不要です。ZIP内のファイルは**すべて同じフォルダに展開**してください。

```text
MSG Studio v3/
├─ Start-GUI.bat
├─ Start-GUI.ps1
├─ Run-Script.bat
├─ Run-Script.ps1
├─ Convert-MsgToJson.ps1
├─ MsgRules.Core.ps1
└─ rules.json
```

### GUIから操作する

`Start-GUI.bat` を実行します。ルール一覧で抽出元・抽出方式・パターン・出力先を編集します。追加・選択行削除・複数行削除に対応します。**「ルールを保存」**を押して初めて `rules.json` に反映されます。解析プレビューで正規表現をテストし、`.msg` を選択して変換できます。

### 画面を開かずに変換する

単一ファイルの変換：

```powershell
powershell.exe -NoProfile -File .\Run-Script.ps1 -Path 'C:\mail est.msg' -PleasanterPayload
```

フォルダ一括変換とログ保存：

```powershell
powershell.exe -NoProfile -File .\Run-Script.ps1 -Path 'C:\mail\inbox' -OutputDirectory 'C:\mail\json' -PleasanterPayload -Overwrite -LogPath 'C:\mail\logs
un.log'
```

サブフォルダも対象にする場合：

```powershell
powershell.exe -NoProfile -File .\Run-Script.ps1 -Path 'C:\mail\inbox' -Recurse -OutputDirectory 'C:\mail\json'
```

既存JSONは原則スキップし、`-Overwrite` で上書きします。終了コードは `0` 正常、`1` ファイル単位の失敗、`2` 初期化・設定失敗です。Jenkinsからは終了コードで判定できます。Outlook COMを使うため、対話型のWindowsログオン環境で実行してください。

## 抽出ルールの考え方

`rules.json` の `source` は `Subject` / `Body` / `Subject+Body` / `SenderName` / `SenderEmailAddress` / `To` / `ReceivedTime` に対応します。`mode` は `Regex`（正規表現）・`Keywords`（`|`で区切った候補）・`Literal`（テキスト全文）の3種類です。

`group` には正規表現の取得グループ番号を指定できます。`target` は `Title`、`ClassA`～`ClassZ`、`DescriptionA`～`DescriptionZ`、`DateA`～`DateZ`、`Ignore` です。未一致時の `fallback` には `$BODY`、`$SUBJECT`、`$SENDER` などを使えます。

**設定の変更は基本的にGUIで行い、日常実行にはRun-Script.ps1を使う**と、ルールと処理を分離できます。

## ソースコード全文（最終版と同一）

以下は配布ZIPから読み取った全実装ファイルです。短縮した擬似コードではありません。コピーしたファイル名とエンコーディングに注意してください。

### rules.json（全文）

```json
{
  "version": 1,
  "siteId": 12345,
  "defaultStatus": 100,
  "includeOriginalBody": true,
  "rules": [
    {
      "enabled": true,
      "name": "チケット番号",
      "source": "Subject+Body",
      "mode": "Regex",
      "pattern": "\\b(?:INC|REQ|TCK)-\\d{4}-\\d+\\b",
      "group": 0,
      "target": "ClassA",
      "fallback": ""
    },
    {
      "enabled": true,
      "name": "問い合わせ者",
      "source": "Body",
      "mode": "Regex",
      "pattern": "(?m)^問い合わせ者[：:]\\s*(.+?)\\s*$",
      "group": 1,
      "target": "ClassB",
      "fallback": ""
    },
    {
      "enabled": true,
      "name": "問い合わせ内容",
      "source": "Body",
      "mode": "Regex",
      "pattern": "(?ms)^問い合わせ内容[：:]\\s*(.*?)(?=^連絡先[：:]|\\z)",
      "group": 1,
      "target": "DescriptionA",
      "fallback": "$BODY"
    },
    {
      "enabled": true,
      "name": "抽出キーワード",
      "source": "Subject+Body",
      "mode": "Keywords",
      "pattern": "障害|ログイン|アカウント|権限|至急|エラー",
      "group": 0,
      "target": "ClassD",
      "fallback": ""
    }
  ]
}
```

### MsgRules.Core.ps1（全文）

```powershell
function Get-RuleSource {
 param($Mail,[string]$Source)
 switch ($Source) {
   'Subject' { return [string]$Mail.Subject }
   'Body' { return [string]$Mail.Body }
   'SenderName' { return [string]$Mail.SenderName }
   'SenderEmailAddress' { return [string]$Mail.SenderEmailAddress }
   'To' { return [string]$Mail.To }
   'ReceivedTime' { return [string]$Mail.ReceivedTime }
   'Subject+Body' { return ([string]$Mail.Subject + "`n" + [string]$Mail.Body) }
   default { throw "Unknown source: $Source" }
 }
}
function Apply-ExtractionRule {
 param($Mail,$Rule)
 $text=Get-RuleSource $Mail ([string]$Rule.source)
 $value=''
 switch ([string]$Rule.mode) {
  'Regex' {
   $m=[regex]::Match($text,[string]$Rule.pattern,'IgnoreCase')
   if ($m.Success) {
    $group=[int]$Rule.group
    if ($group -ge 0 -and $group -lt $m.Groups.Count) { $value=$m.Groups[$group].Value.Trim() }
   }
  }
  'Keywords' {
   $hits=@()
   foreach ($word in ([string]$Rule.pattern -split '\|')) {
    $word=$word.Trim()
    if ($word -and $text.IndexOf($word,[StringComparison]::OrdinalIgnoreCase) -ge 0) { $hits += $word }
   }
   $value=($hits -join '、')
  }
  'Literal' { $value=$text.Trim() }
  default { throw "Unknown mode: $($Rule.mode)" }
 }
 if (-not $value) {
  $fallback=[string]$Rule.fallback
  switch ($fallback) {
    '$BODY' { $value=[string]$Mail.Body }
    '$SUBJECT' { $value=[string]$Mail.Subject }
    '$SENDER' { $value=[string]$Mail.SenderName }
    default { $value=$fallback }
  }
 }
 return $value
}
function Convert-MailData {
 param($Mail,$Config,[switch]$PleasanterPayload)
 $extracted=[ordered]@{}
 $classHash=[ordered]@{}; $descriptionHash=[ordered]@{}; $dateHash=[ordered]@{}
 foreach ($rule in @($Config.rules)) {
   if (-not $rule.enabled) { continue }
   $value=Apply-ExtractionRule $Mail $rule
   $extracted[[string]$rule.name]=$value
   $target=[string]$rule.target
   if ($target -match '^Class[A-Z]$') { $classHash[$target]=$value }
   elseif ($target -match '^Description[A-Z]$') { $descriptionHash[$target]=$value }
   elseif ($target -match '^Date[A-Z]$') { if ($value) { $dateHash[$target]=$value } }
   elseif ($target -eq 'Title') { } # Title is handled below
   elseif ($target -eq 'Ignore') { }
   else { throw "Target must be ClassA..Z, DescriptionA..Z, DateA..Z, Title, or Ignore: $target" }
 }
 if ($PleasanterPayload) {
   $title=[string]$Mail.Subject
   foreach ($rule in @($Config.rules)) {
     if ($rule.enabled -and $rule.target -eq 'Title') { $title=[string]$extracted[[string]$rule.name] }
   }
   $result=[ordered]@{ Title=$title; ClassHash=$classHash; DescriptionHash=$descriptionHash; Status=[int]$Config.defaultStatus }
   if ($dateHash.Count) { $result['DateHash']=$dateHash }
   # These are payload fields, not a complete API request (ApiKey is not included).
   return $result
 }
 return [ordered]@{ Mail=$Mail; Extracted=$extracted }
}
```

### Convert-MsgToJson.ps1（全文）

```powershell
[CmdletBinding()]
param(
  [Parameter(Mandatory)][string]$Path,
  [string]$RulesPath = (Join-Path $PSScriptRoot 'rules.json'),
  [string]$OutputDirectory = '',
  [switch]$PleasanterPayload,
  [switch]$PassThru
)
$ErrorActionPreference='Stop'
. (Join-Path $PSScriptRoot 'MsgRules.Core.ps1')
$config = Get-Content -LiteralPath $RulesPath -Raw -Encoding UTF8 | ConvertFrom-Json
$files = if (Test-Path -LiteralPath $Path -PathType Container) {
  @(Get-ChildItem -LiteralPath $Path -Filter '*.msg' -File)
} else { @(Get-Item -LiteralPath $Path) }
if (-not $files.Count) { throw 'MSG files were not found.' }
$outlook = $null
try {
  $outlook = New-Object -ComObject Outlook.Application
  foreach ($file in $files) {
    $msg = $null
    try {
      if ($file.Extension -ine '.msg') { throw 'Only .msg files are supported.' }
      $msg = $outlook.Session.OpenSharedItem($file.FullName)
      if ([int]$msg.Class -ne 43) { throw 'Not an Outlook MailItem.' }
      $received = ''
      try { $received=([datetime]$msg.ReceivedTime).ToString('yyyy-MM-ddTHH:mm:ss') } catch {}
      $mailData = [ordered]@{
        FileName=$file.Name; Subject=[string]$msg.Subject; Body=[string]$msg.Body
        SenderName=[string]$msg.SenderName; SenderEmailAddress=[string]$msg.SenderEmailAddress
        To=[string]$msg.To; ReceivedTime=$received
      }
      $result = Convert-MailData -Mail $mailData -Config $config -PleasanterPayload:$PleasanterPayload
      $directory = if ($OutputDirectory) { $OutputDirectory } else { $file.DirectoryName }
      [IO.Directory]::CreateDirectory($directory) | Out-Null
      $suffix = if ($PleasanterPayload) { '.pleasanter.json' } else { '.json' }
      $output = Join-Path $directory ($file.BaseName+$suffix)
      [IO.File]::WriteAllText($output, ($result | ConvertTo-Json -Depth 30), (New-Object Text.UTF8Encoding $false))
      Write-Host "Saved: $output"
      if ($PassThru) { $result }
    } catch { Write-Error "Error in $($file.FullName): $($_.Exception.Message)" -ErrorAction Continue }
    finally { if ($msg) { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($msg) } }
  }
} finally { if ($outlook) { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($outlook) } }
```

### Run-Script.ps1（全文）

```powershell
#Requires -Version 5.1
[CmdletBinding()]
param(
 [Parameter(Mandatory)][string]$Path,
 [string]$RulesPath = (Join-Path $PSScriptRoot 'rules.json'),
 [string]$OutputDirectory = '',
 [switch]$PleasanterPayload,
 [switch]$Recurse,
 [switch]$Overwrite,
 [string]$LogPath = '',
 [switch]$Quiet
)
$ErrorActionPreference = 'Stop'
$success = 0; $failed = 0; $skipped = 0
function Log([string]$level, [string]$message) {
 $line = ('{0} [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'),$level,$message)
 if ($LogPath) { Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8 }
 if (-not $Quiet -or $level -eq 'ERROR') { if ($level -eq 'ERROR') { [Console]::Error.WriteLine($line) } else { Write-Host $line } }
}
try {
 . (Join-Path $PSScriptRoot 'MsgRules.Core.ps1')
 if (-not (Test-Path -LiteralPath $RulesPath -PathType Leaf)) { throw "ルールファイルがありません: $RulesPath" }
 $config = Get-Content -LiteralPath $RulesPath -Raw -Encoding UTF8 | ConvertFrom-Json
 if (-not $config.rules) { throw 'rules.json にルールがありません。' }
 if (-not (Test-Path -LiteralPath $Path)) { throw "入力パスがありません: $Path" }
 $source = (Resolve-Path -LiteralPath $Path).Path
 if ((Get-Item -LiteralPath $source).PSIsContainer) {
   $files = @(Get-ChildItem -LiteralPath $source -File -Filter '*.msg' -Recurse:$Recurse)
 } else {
   if ([IO.Path]::GetExtension($source) -ine '.msg') { throw '入力ファイルは .msg を指定してください。' }
   $files = @(Get-Item -LiteralPath $source)
 }
 if ($files.Count -eq 0) { throw '処理対象の.msgがありません。' }
 if ($OutputDirectory) { [void][IO.Directory]::CreateDirectory($OutputDirectory) }
 if ($LogPath) {
   $logDir = Split-Path -Parent $LogPath
   if ($logDir) { [void][IO.Directory]::CreateDirectory($logDir) }
 }
 $outlook = $null
 try {
   $outlook = New-Object -ComObject Outlook.Application
   foreach ($file in $files) {
     $mail = $null
     try {
       $outdir = if ($OutputDirectory) { $OutputDirectory } else { $file.DirectoryName }
       $suffix = if ($PleasanterPayload) { '.pleasanter.json' } else { '.json' }
       $dest = Join-Path $outdir ($file.BaseName + $suffix)
       if ((Test-Path -LiteralPath $dest) -and -not $Overwrite) {
         $skipped++; Log 'SKIP' "出力先が存在: $dest (上書きには -Overwrite)"; continue
       }
       $mail = $outlook.Session.OpenSharedItem($file.FullName)
       if ([int]$mail.Class -ne 43) { throw 'Outlook MailItemではありません。' }
       $received = ''
       try { $received = ([datetime]$mail.ReceivedTime).ToString('yyyy-MM-ddTHH:mm:ss') } catch {}
       $mailData = [ordered]@{
         FileName=$file.Name; Subject=[string]$mail.Subject; Body=[string]$mail.Body
         SenderName=[string]$mail.SenderName; SenderEmailAddress=[string]$mail.SenderEmailAddress
         To=[string]$mail.To; ReceivedTime=$received
       }
       $result = Convert-MailData -Mail $mailData -Config $config -PleasanterPayload:$PleasanterPayload
       $json = ConvertTo-Json -InputObject $result -Depth 30
       [IO.File]::WriteAllText($dest,$json,(New-Object Text.UTF8Encoding $false))
       $success++; Log 'OK' "$($file.Name) => $dest"
     } catch { $failed++; Log 'ERROR' "$($file.FullName): $($_.Exception.Message)" }
     finally { if ($mail) { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($mail) } }
   }
 } finally { if ($outlook) { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($outlook) } }
 Log 'SUMMARY' "成功=$success 失敗=$failed スキップ=$skipped"
 if ($failed -gt 0) { exit 1 }
 exit 0
} catch { Log 'ERROR' $_.Exception.Message; exit 2 }
```

### Start-GUI.ps1（全文）

```powershell
#Requires -Version 5.1
param([string]$RulesPath=(Join-Path $PSScriptRoot 'rules.json'))
$ErrorActionPreference='Stop'
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
. (Join-Path $PSScriptRoot 'MsgRules.Core.ps1')
if ([Threading.Thread]::CurrentThread.GetApartmentState() -ne 'STA') {
 [Windows.Forms.MessageBox]::Show('STA が必要です。Start-GUI.bat から起動してください。') | Out-Null
 return
}
$form=New-Object Windows.Forms.Form
$form.Text='MSG Studio  |  Outlook to JSON'
$form.Size=New-Object Drawing.Size(1210,840)
$form.StartPosition='CenterScreen'
$form.MinimumSize=New-Object Drawing.Size(1080,740)
$font=New-Object Drawing.Font('Meiryo UI',9)
$bg=[Drawing.Color]::FromArgb(17,24,39)
$surface=[Drawing.Color]::FromArgb(31,41,55)
$surface2=[Drawing.Color]::FromArgb(45,55,72)
$fg=[Drawing.Color]::FromArgb(241,245,249)
$muted=[Drawing.Color]::FromArgb(156,163,175)
$accent=[Drawing.Color]::FromArgb(79,135,247)
$form.BackColor=$bg
$form.ForeColor=$fg
$form.AutoScaleMode='Dpi'
$form.Font=$font
$grid=New-Object Windows.Forms.DataGridView
$grid.Location=New-Object Drawing.Point(22,148)
$grid.Size=New-Object Drawing.Size(1148,292)
$grid.Anchor='Top,Left,Right'
$grid.BackgroundColor=$surface
$grid.BorderStyle='None'
$grid.GridColor=$surface2
$grid.EnableHeadersVisualStyles=$false
$grid.ColumnHeadersDefaultCellStyle.BackColor=$surface2
$grid.ColumnHeadersDefaultCellStyle.ForeColor=$fg
$grid.ColumnHeadersDefaultCellStyle.SelectionBackColor=$surface2
$grid.ColumnHeadersHeight=40
$grid.RowTemplate.Height=32
$grid.DefaultCellStyle.BackColor=$surface
$grid.DefaultCellStyle.ForeColor=$fg
$grid.DefaultCellStyle.SelectionBackColor=$accent
$grid.DefaultCellStyle.SelectionForeColor=[Drawing.Color]::White
$grid.AlternatingRowsDefaultCellStyle.BackColor=[Drawing.Color]::FromArgb(36,46,61)
$grid.AlternatingRowsDefaultCellStyle.ForeColor=$fg
$grid.SelectionMode='FullRowSelect'
$grid.MultiSelect=$true
$grid.AllowUserToAddRows=$true
$grid.AllowUserToDeleteRows=$false
$grid.AutoSizeColumnsMode='Fill'
$grid.RowHeadersVisible=$false
$grid.Columns.Add((New-Object Windows.Forms.DataGridViewCheckBoxColumn -Property @{Name='enabled';HeaderText='有効';FillWeight=40})) | Out-Null
$grid.Columns.Add('name','ルール名') | Out-Null
foreach ($name in @('source','mode','target')) {
 $col=New-Object Windows.Forms.DataGridViewComboBoxColumn
 $col.Name=$name
 $col.HeaderText=@{source='解析対象';mode='種類';target='出力先'}[$name]
 $col.FlatStyle='Flat'
 $values=switch($name){
  source {@('Subject','Body','Subject+Body','SenderName','SenderEmailAddress','To','ReceivedTime')}
  mode {@('Regex','Keywords','Literal')}
  target {@('Title','Ignore')}
 }
 if ($name -eq 'target') { $values=@('Title','Ignore'); foreach($prefix in @('Class','Description','Date')){foreach($code in 65..90){$values+=($prefix+[char]$code)}} }
 foreach ($v in $values) { [void]$col.Items.Add([string]$v) }
 [void]$grid.Columns.Add($col)
}
$grid.Columns.Add('pattern','正規表現 / キーワード（|区切り）') | Out-Null
$grid.Columns.Add('group','取得グループ') | Out-Null
$grid.Columns.Add('fallback','不一致時（$BODY 等）') | Out-Null
$grid.Columns['pattern'].FillWeight=190
$grid.Columns['name'].FillWeight=90
$grid.Columns['source'].FillWeight=100
$grid.Columns['mode'].FillWeight=75
$grid.Columns['target'].FillWeight=90
$grid.Columns['group'].FillWeight=52
$grid.Columns['fallback'].FillWeight=90
$form.Controls.Add($grid)
$lbl=New-Object Windows.Forms.Label
$lbl.Text='EXTRACTION RULES  /  上から順に評価 ・ Ctrl/Shiftで複数選択 ・ Regexのgroup=0は全体一致'
$lbl.Location=New-Object Drawing.Point(22,121);$lbl.AutoSize=$true;$lbl.ForeColor=$muted;$form.Controls.Add($lbl)
$heading=New-Object Windows.Forms.Label
$heading.Text='MSG Studio'
$heading.Font=New-Object Drawing.Font('Meiryo UI',19,[Drawing.FontStyle]::Bold)
$heading.ForeColor=$fg
$heading.Location=New-Object Drawing.Point(22,16)
$heading.AutoSize=$true
$form.Controls.Add($heading)
$subtitle=New-Object Windows.Forms.Label
$subtitle.Text='Outlookメールを解析し、Pleasanter用JSONへ変換'
$subtitle.ForeColor=$muted
$subtitle.Location=New-Object Drawing.Point(24,58)
$subtitle.AutoSize=$true
$form.Controls.Add($subtitle)
$siteLabel=New-Object Windows.Forms.Label -Property @{Text='Site ID';Location=(New-Object Drawing.Point(26,458));AutoSize=$true}
$site=New-Object Windows.Forms.NumericUpDown -Property @{Location=(New-Object Drawing.Point(88,452));Width=105;Minimum=1;Maximum=999999999}
$statusLabel=New-Object Windows.Forms.Label -Property @{Text='初期Status';Location=(New-Object Drawing.Point(226,458));AutoSize=$true}
$status=New-Object Windows.Forms.NumericUpDown -Property @{Location=(New-Object Drawing.Point(320,452));Width=75;Minimum=0;Maximum=999999}
foreach ($x in @($siteLabel,$site,$statusLabel,$status)) { $form.Controls.Add($x) }
function New-Button($title,$x,$y,$width,$callback) {
 $b=New-Object Windows.Forms.Button
 $b.Text=$title;$b.Location=New-Object Drawing.Point($x,$y);$b.Size=New-Object Drawing.Size($width,30)
 $b.FlatStyle='Flat'
 $b.FlatAppearance.BorderSize=0
 $b.BackColor=$surface2
 $b.ForeColor=$fg
 $b.Cursor=[Windows.Forms.Cursors]::Hand
 $b.Add_Click($callback);$form.Controls.Add($b);return $b
}
$sample=New-Object Windows.Forms.TextBox
$sample.Multiline=$true;$sample.ScrollBars='Vertical'
$sample.Location=New-Object Drawing.Point(22,544);$sample.Size=New-Object Drawing.Size(548,230)
$sample.Anchor='Top,Bottom,Left';$sample.BackColor=$surface;$sample.ForeColor=$fg;$sample.BorderStyle='FixedSingle';$sample.Text="件名：【障害】ログインできない INC-2026-001`r`n問い合わせ者：山田太郎`r`n問い合わせ内容：`r`nログイン時にエラーが発生。至急対応をお願いします。"
$form.Controls.Add($sample)
$preview=New-Object Windows.Forms.TextBox
$preview.Multiline=$true;$preview.ScrollBars='Both';$preview.WordWrap=$false
$preview.Location=New-Object Drawing.Point(584,544);$preview.Size=New-Object Drawing.Size(586,230)
$preview.Anchor='Top,Bottom,Left,Right';$preview.BackColor=$surface;$preview.ForeColor=$fg;$preview.BorderStyle='FixedSingle';$preview.ReadOnly=$true;$form.Controls.Add($preview)
$form.Controls.Add((New-Object Windows.Forms.Label -Property @{Text='テスト本文（先頭の 件名： は件名として扱います）';Location=(New-Object Drawing.Point(22,514));AutoSize=$true}))
$form.Controls.Add((New-Object Windows.Forms.Label -Property @{Text='解析プレビュー（APIキーは含みません）';Location=(New-Object Drawing.Point(584,514));AutoSize=$true}))
function Read-Rules {
 $data=Get-Content -LiteralPath $RulesPath -Raw -Encoding UTF8 | ConvertFrom-Json
 $grid.Rows.Clear();$site.Value=[decimal]$data.siteId;$status.Value=[decimal]$data.defaultStatus
 foreach ($r in $data.rules) { [void]$grid.Rows.Add([bool]$r.enabled,[string]$r.name,[string]$r.source,[string]$r.mode,[string]$r.target,[string]$r.pattern,[string]$r.group,[string]$r.fallback) }
}
function Build-Rules {
 $grid.EndEdit()
 $rules=@()
 foreach ($row in $grid.Rows) {
  if ($row.IsNewRow) {continue}
  $v=@{};foreach($key in @('enabled','name','source','mode','target','pattern','group','fallback')){$v[$key]=$row.Cells[$key].Value}
  if (-not $v.name) {throw 'ルール名は必須です。'}
  if (-not $v.source -or -not $v.mode -or -not $v.target) {throw "ルール「$($v.name)」の種類・出力先が未指定です。"}
  if ($v.mode -eq 'Regex') { [void][regex]::new([string]$v.pattern) }
  $rules+= [ordered]@{enabled=[bool]$v.enabled;name=[string]$v.name;source=[string]$v.source;mode=[string]$v.mode;pattern=[string]$v.pattern;group=if($v.group){[int]$v.group}else{0};target=[string]$v.target;fallback=[string]$v.fallback}
 }
 return [ordered]@{version=1;siteId=[int]$site.Value;defaultStatus=[int]$status.Value;includeOriginalBody=$true;rules=$rules}
}
$loadBtn=New-Button '再読込' 22 86 104 {try { Read-Rules } catch {[Windows.Forms.MessageBox]::Show($_.Exception.Message)|Out-Null}}
$saveBtn=New-Button 'ルールを保存' 244 86 148 {
 try { $data=Build-Rules; $json=ConvertTo-Json -InputObject $data -Depth 15;[IO.File]::WriteAllText($RulesPath,$json,(New-Object Text.UTF8Encoding $false));[Windows.Forms.MessageBox]::Show('rules.json に保存しました。')|Out-Null }
 catch{[Windows.Forms.MessageBox]::Show($_.Exception.Message)|Out-Null}
}
$previewBtn=New-Button '解析プレビュー' 405 86 148 {
 try {
  $data=Build-Rules
  $body=$sample.Text;$subject=''
  if ($body -match '(?m)^件名[：:]\s*(.*)') {$subject=$Matches[1];$body=($body -replace '(?m)^件名[：:]\s*.*\r?\n?','')}
  $mail=[ordered]@{Subject=$subject;Body=$body;SenderName='';SenderEmailAddress='';To='';ReceivedTime=''}
  $result=Convert-MailData $mail $data -PleasanterPayload
  $preview.Text=ConvertTo-Json -InputObject $result -Depth 20
 } catch{[Windows.Forms.MessageBox]::Show($_.Exception.Message)|Out-Null}
}
$convertBtn=New-Button 'MSGを選択してJSON出力' 566 86 232 {
 try {
  $data=Build-Rules
  $json=ConvertTo-Json -InputObject $data -Depth 15
  [IO.File]::WriteAllText($RulesPath,$json,(New-Object Text.UTF8Encoding $false))
  $dialog=New-Object Windows.Forms.OpenFileDialog
  $dialog.Filter='Outlook MSG (*.msg)|*.msg';$dialog.Multiselect=$true
  if($dialog.ShowDialog() -ne 'OK'){return}
  foreach($file in $dialog.FileNames){& (Join-Path $PSScriptRoot 'Convert-MsgToJson.ps1') -Path $file -RulesPath $RulesPath -PleasanterPayload}
  [Windows.Forms.MessageBox]::Show('MSGの変換が終了しました。結果はMSGと同じフォルダです。')|Out-Null
 } catch{[Windows.Forms.MessageBox]::Show($_.Exception.Message)|Out-Null}
}
$addBtn=New-Button '＋ ルール追加' 982 86 174 {
 [void]$grid.Rows.Add($true,'新しいルール','Body','Regex','Ignore','','0','')
 $grid.ClearSelection()
 $index=$grid.Rows.Count-2
 if ($index -ge 0) { $grid.Rows[$index].Selected=$true; $grid.CurrentCell=$grid.Rows[$index].Cells['name'];$grid.BeginEdit($true)|Out-Null }
}
$deleteBtn=New-Button '選択行を削除' 818 86 152 {
 try {
  $grid.EndEdit()
  $rows=@($grid.SelectedRows | Where-Object { -not $_.IsNewRow } | Sort-Object Index -Descending)
  if ($rows.Count -eq 0 -and $grid.CurrentCell -and -not $grid.CurrentCell.OwningRow.IsNewRow) { $rows=@($grid.CurrentCell.OwningRow) }
  if ($rows.Count -eq 0) { [Windows.Forms.MessageBox]::Show('削除するルール行を選択してください。','MSG Studio')|Out-Null;return }
  $answer=[Windows.Forms.MessageBox]::Show("選択された $($rows.Count) 件のルールを削除しますか？`n保存ボタンを押すと rules.json に反映されます。",'ルール削除の確認',[Windows.Forms.MessageBoxButtons]::YesNo,[Windows.Forms.MessageBoxIcon]::Question)
  if ($answer -ne [Windows.Forms.DialogResult]::Yes) { return }
  foreach ($row in $rows) { $grid.Rows.RemoveAt($row.Index) }
 } catch { [Windows.Forms.MessageBox]::Show($_.Exception.Message)|Out-Null }
}
$saveBtn.BackColor=$accent
$convertBtn.BackColor=[Drawing.Color]::FromArgb(24,105,88)
$deleteBtn.BackColor=[Drawing.Color]::FromArgb(130,48,60)
$site.BackColor=$surface;$site.ForeColor=$fg
$status.BackColor=$surface;$status.ForeColor=$fg
foreach ($ctl in @($siteLabel,$statusLabel)) { $ctl.ForeColor=$muted }
foreach ($ctl in @($form.Controls | Where-Object { $_ -is [Windows.Forms.Label] -and $_ -ne $heading })) { $ctl.ForeColor=$muted }
$grid.Add_DataError({ param($sender,$e) $e.ThrowException=$false })
Read-Rules
[void]$form.ShowDialog()
```

### Start-GUI.bat（全文）

```bat
@echo off
cd /d "%~dp0"
powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File "%~dp0Start-GUI.ps1"
pause
```

### Run-Script.bat（全文）

```bat
@echo off
cd /d "%~dp0"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Run-Script.ps1" -Path "%~1" -PleasanterPayload
exit /b %ERRORLEVEL%
```

## 生成したJSONをプリザンターで使う場合

`-PleasanterPayload` ではプリザンターの項目構造に寄せたJSONを生成しますが、**サイトIDやAPIキーを指定して自動POSTするわけではありません**。送信する場合はAPIの仕様に合わせてペイロードやサイトの項目設定を確認し、別途登録処理を構築してください。

## よくある問題

- **Outlook COMエラー**：Classic Outlookのインストール・プロファイル・対話実行を確認。
- **日本語が文字化け**：`.ps1` はCP932で保存。`rules.json` はUTF-8で保存。
- **抽出されない**：正規表現の `group`、抽出元 `source`、メール本文の改行を確認。
- **JSONが上書きされる**：CLIの `-Overwrite` 指定と同名ファイルの扱いを確認。
- **権限エラー**：出力先フォルダとログ保存先の書き込み権限を確認。

## まとめ

MSG Studio v3 は**GUIで抽出ルールを調整し、CLIで繰り返し処理する**ための土台です。問い合わせ受付やチケット管理に使う場合は、本番メールを投入する前にテストメールで抽出結果、個人情報の扱い、同名ファイル時の挙動を確認してください。

  Previous Post Next Post

---

## Navigation

- Parent: [IT関連](https://automationse.net/it-related.md)
- Previous: [【Pleasanter】Outlookの.msgファイルをPowerShellで自動登録する](https://automationse.net/it-related/outlook-msg-to-pleasanter.md)
