MSのメールクライアントと言えばOutlookですが、メールを他のソフトウェアから扱おうとするとちょっと厄介ですね。
今回はPowerShellでメールをエクスポートする方法を試したいと思います。
もくじ:
はじめに
OutlookでExchangeのメールをエクスポートしたいときって、何がベストプラクティスなんでしょうね。普通にエクスポート機能を使うとPSTファイル(*.pst)に出力できますが、MS独自仕様なので一筋縄では扱えません。テキストエディタで開いたり、バイナリモードで開いても内容が読めない、ということです。
単にメールなどのアイテムだけではなく、フォルダ構造や属性などのメタデータも記録していますし、長い歴史のなかで互換性を保つために独自の拡張が必要だった経緯を考えると、無理からぬことではあります。
ネットで調べると、一応MSはPSTの仕様を公開しているようですし、C++のSDKやらPythonでも有志がSDKのラッパを作っているようです。しかしいずれも一様に面倒臭そう。
今回は、できるだけシンプルかつMSオフィシャルなやり方を目指して、COMオブジェクトを使うことにしました。調べてみるとVBAの実行例が多いようですね。
前提
今回のスクリプトは、OutlookのCOMオブジェクトに依存しています。
つまりはOutlookがインストールされており、メールアカウントがセットアップされていてメールが閲覧できる(またはPSTファイルからインポートされている)PCで実行する必要があります。
また、COMオブジェクト経由でOutlookの機能にアクセスするには、Officeのインストールオプションとして「.NETプログラミングサポート」を有効化する必要があります。
デフォルトで有効化されているかどうか確認していませんが、いつものコンパネ「プログラムと機能」からOfficeのインストーラを立ち上げれば構成できます。
スクリプト
今回のスクリプトです。検証した環境はWindows 10 Pro + Outlook 2016です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | # output destination $dstDir = "C:\pathto\dir" # scan message items recursively function scanFolders ($olObj) { # get destination path Write-Host "Loading folder:" $olObj.Name -ForegroundColor Green if (("{0}" -f $olObj.FullFolderPath) -ne "") { $folderPath = Join-Path $dstDir $olObj.FullFolderPath.Replace("\\","") } else { $folderPath = $dstDir } # save per message item foreach ($item in $olObj.Items) { # skip except mailItem # https://msdn.microsoft.com/ja-jp/vba/outlook-vba/articles/olobjectclass-enumeration-outlook if (("{0}" -f $item.Class) -ne "43") { continue } # generate item directory $dirname = "{0}_{1}" -f $item.ReceivedTime,$item.SenderEmailAddress $dirname = $dirname.Replace(":","").Replace("/","") if ($dirname.Length -lt 5) { $dirname = $item.EntryID } $itemPath = Join-Path $folderPath $dirname if (!(Test-Path $itemPath)) { $result = New-Item -ItemType Directory -Path $itemPath -Force } # format and save data to json file $data = @{ "entryId" = $item.EntryID; "conversationId" = $item.ConversationID; "conversationTopic" = $item.ConversationTopic; "subject" = $item.Subject; "senderAddress" = $item.SenderEmailAddress; "senderName" = $item.SenderName; "senderType" = $item.SenderEmailType; "to" = $item.To; "cc" = $item.CC; "bcc" = $item.BCC; "body" = $item.Body; "htmlBody" = $item.HTMLBody; "received" = "{0}" -f $item.ReceivedTime; "modified" = "{0}" -f $item.LastModificationTime; "created" = "{0}" -f $item.CreationTime; "sent" = "{0}" -f $item.SentOn }; $data | ConvertTo-Json | Out-File -FilePath (Join-Path $itemPath "message.json") -Encoding utf8 # save attachments $item.Attachments | ForEach-Object { if (("{0}" -f $_.FileName) -eq "") { $fname = "attch.dat" } else { $fname = $_.FileName } $_.SaveAsFile((Join-Path $itemPath $fname)) } } # scan each sub folder foreach ($folder in $olObj.Folders) { # call recursively if mailItem object # https://msdn.microsoft.com/ja-jp/vba/outlook-vba/articles/olitemtype-enumeration-outlook if (("{0}" -f $folder.DefaultItemType) -eq "0") { scanFolders $folder } else { Write-Host ("Skip folder: {0} (DefaultItemType:{1})" -f $folder.Name,$folder.DefaultItemType) -ForegroundColor Yellow } } } # get outlook COM object $olApp = New-Object -ComObject Outlook.Application $olObj = $olApp.GetNameSpace("MAPI") # invoke scanning function scanFolders $olObj |
ローカルの保存先ディレクトリパスを$dstDir
に指定して実行します。
アカウントやメールフォルダごとにディレクトリを作成し、その中にメッセージをファイルとして書き出していきます。
1 | PS> ./olclient.ps1 |
環境によっては、アクセスを許可するかをたずねるメッセージボックスがポップアップする場合もあるようです。
以下、スクリプトの詳細を説明しておきます。
PowerShellでメールを保存する
今回のスクリプトの目的は、Outlookが保持しているアカウントやフォルダ毎にローカルのファイルシステムにディレクトリ階層を作り、メールの本文を宛先や送信時刻などのメタデータとともに保存することです。
COMオブジェクトを作成しているのは、上のスクリプト終わりのほう(下記の部分)。オブジェクト名や名前空間を取得するところは共通的な処理なので、ググると他にも色々なサンプルがヒットすると思います。
1 2 | $olApp = New-Object -ComObject Outlook.Application $olObj = $olApp.GetNameSpace("MAPI") |
今回は、基本的にFolders
プロパティを再帰的に辿って、Items
プロパティごとに処理していきます。
受信トレイのみ、とか特定のアカウント名だけ、などフィルタすることもできます。
1 2 3 4 5 6 7 | # 名前が{account_name}フォルダのみを取得する $olObj = $olApp.GetNameSpace("MAPI") $olObj = $olObj.Folders | where {$_.Name -eq "{account_name}"} # 特定の種類のフォルダのみを取得する(6:受信トレイ) $olObj = $olApp.GetNameSpace("MAPI") $olObj = $olObj.GetDefaultFolder(6) |
階層を辿ってメールに行き着けば、メール1通毎にディレクトリを作成し、データをまとめたJSONファイルを作成します。
メールフォルダの取得
Outlookを開いたときにツリー表示されるフォルダの部分は、Folders
プロパティで取得できます。今回は、メール以外の、例えばタスクや連絡先といったフォルダは不要なので、MailItem
オブジェクトを表す「0」のみを処理します。
1 2 3 4 5 | if (("{0}" -f $folder.DefaultItemType) -eq "0") { scanFolders $folder } else { Write-Host ("Skip folder: {0} (DefaultItemType:{1})" -f $folder.Name,$folder.DefaultItemType) -ForegroundColor Yellow } |
メッセージと添付ファイルの取得
Outlook、というかExchangeはスケジュールや連絡先など様々な種類のオブジェクトを使っていますが、今回欲しいのはメールのメッセージのみ。
すでにフォルダの種別で絞り込んでいますが、ここではさらにClass
プロパティの値でメッセージを抽出します。
1 2 3 | if (("{0}" -f $item.Class) -ne "43") { continue } |
メッセージのオブジェクトを取り出せば、あとは各プロパティで本文や宛先などのデータを取得できます。今回は一旦、連想配列に複製し、JSONファイルとして書き出します。
また、メッセージに添付ファイルが付いている場合はAttachments
プロパティのSaveAsFile()
メソッドを使って保存できます。
1 2 3 4 5 6 7 8 | $item.Attachments | ForEach-Object { if (("{0}" -f $_.FileName) -eq "") { $fname = "attch.dat" } else { $fname = $_.FileName } $_.SaveAsFile((Join-Path $itemPath $fname)) } |
コメント
質問です。
MAPIを使って、outlookアカウントに送信される添付ファイルをローカルに保存するpowershellのスクリプトを作成しました。
https://kapibara-sos.net/archives/394
こちらを参考しています。
powershellを作成後、.ps1ファイルを直接実行すると添付ファイルの保存ができますが
.ps1ファイルをwindowsのタスクスケジューラで実行すると、MAPIのインスタンスが作成されません。
試したこと:
・タスクスケジューラの実行権限を最上に権限にする
・.ps1ファイルをタスクスケジューラで実行する際には、powershellの実行権限をBypassにしている。
よろしくお願いいたします。
返信
タスクスケジューラでpowershellを使うとパスの指定が面倒なので、あまり私は利用しません。(バッチファイルを作成している)
どうしても、ということであればこちらが参考になります。
https://www.atmarkit.co.jp/ait/articles/1412/03/news125.html
あとは実行時のユーザをSystem (NT AUTHORITY\System)にして試してみるか、くらいしか思いつきません。
[…] https://kapibara-sos.net/archives/394 […]