CVE-2023-23397 の脆弱性が悪用されていないかを確認する Outlook のスクリプト

3 月 16 日に Outlook のゼロデイ脆弱性となる CVE-2023-23397 が公開され、それを悪用されていないかを Exchange サーバー上で確認するためのスクリプトも同時に公開されました。

しかし、この脆弱性は Outlook を狙ったものであるため、Exchange 環境以外でも影響があります。

3/24 に Guidance for investigating attacks using CVE-2023-23397 – Microsoft Security Blog として CVE-2023-23397 で使用されるプロパティの詳細も公開されたので、こちらの情報をもとに Outlook 上で脆弱性の悪用がないかを確認するスクリプトを作成しました。
このスクリプトは、Outlook のプロファイル中にあるメールボックスや PST などに含まれるすべてのアイテムについて脆弱性に使用される PidLidReminderFileParameter というプロパティの値が存在するかを確認し、存在していた場合はそのアイテムが保存されているフォルダーのパスや件名、受信日時 (受信日時がないアイテムの場合は最終更新日時) および PidLidReminderFileParameter の値を出力します。
フォルダーのパスは PST などが保存されているパスではなく、Outlook のフォルダー ツリー上のパスになります。
各行の最後の文字列が PidLidReminderFileParameter の値となり、この値が \\ で始まる UNC だった場合、悪用されている可能性が高いといえます。
また、LOG_ROOT で \\server\share の様にネットワーク共有を指定すれば多数のユーザーの状況を一元管理できるよう、ファイル名には USERNAME 環境変数に格納されているユーザー名が使用されます。

スクリプトは以下の通りです。
このスクリプトをメモ帳などで拡張子 .vbs として保存し、ダブルクリックで実行すると c:\temp に ReminderFile-ユーザー名.log というファイル名でスキャン結果が格納されます。

'
' ログファイルが書き込まれるフォルダーとファイルのプレフィックスを指定
Const LOG_ROOT = "c:\temp\ReminderFile-"
'
Dim wshShell
Dim strLogFile
Dim objFSO
Dim stmLog
Dim appOlk
Dim oneStore
Dim fldRoot
Dim cFound
cFound = 0
' ファイル名を作成
Set wshShell = CreateObject("WScript.Shell")
strLogFile = LOG_ROOT & wshShell.ExpandEnvironmentStrings("%USERNAME%") & ".log"
' ログファイルを作成
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set stmLog = objFSO.CreateTextFile(strLogFile, True)
' Outlook Application オブジェクトのインスタンスを生成
Set appOlk = CreateObject("Outlook.Application")
' プロファイル中のすべてのストアについてログ出力
For Each oneStore In appOlk.Session.Stores
     Set fldRoot = oneStore.GetRootFolder()
     ListReminderFileRecurs fldRoot
Next
' 見つかったアイテムの数をログ出力
stmLog.WriteLine cFound & " 件のアイテムが見つかりました。"
' ログファイルをクローズ
stmLog.Close()
'
MsgBox "スキャンは終了しました。"
'
' 再帰的にアイテムをチェックする
Sub ListReminderFileRecurs( fldRoot )
     On Error Resume Next
     ' 脆弱性に悪用されるプロパティの定義
     Const PidLidReminderFileParameter = "http://schemas.microsoft.com/mapi/id/{00062008-0000-0000-C000-000000000046}/851F001F"
     Dim objItem
     Dim fldSub
     ' フォルダ中のすべてのアイテムについてチェック
     For Each objItem In fldRoot.Items
         Dim strFile
         Dim strReceived
         strFile = ""
         ' プロパティの値を取得
         strFile = objItem.PropertyAccessor.GetProperty(PidLidReminderFileParameter)
         ' プロパティに値が設定されていたらログ出力
         If strFile <> "" Then
             ' 受信日時がないアイテムについては最終更新日時を取得する
             strReceived = objItem.ReceivedTime
             If strReceived = "" Then
                 strReceived = objItem.LastModificationTime
             End If
             ' ログを出力
             stmLog.WriteLine fldRoot.FolderPath & vbTab & objItem.Subject & vbTab & strReceived
             ' 検出アイテムのカウントを増加
             cFound = cFound + 1
         End If
     Next
     ' サブ フォルダーについて再帰的に処理
     For Each fldSub In fldRoot.Folders
         ListReminderFileRecurs fldSub
     Next
End Sub

このブログのマクロ/スクリプトの配布について

コメントにて以下のご質問をいただきました。


メール本文からスケジュールを登録するマクロ、を活用させてもらってます。大変便利で助かっています。ありがとうございます!

さて、社内の他の人にも使ってもらいたいと思っておりちょっと変更してみんなに配布したいと思っているのですが、そのように配布しても良いものでしょうか??


これまで多数のマクロを公開していましたが、そういえば配布や改変、再利用などについてのガイドは作っていませんでした。

コードの使用に関するガイドは一般的にはライセンスと呼び、GPL や MIT ライセンスなどが有名です。
ただ、このブログのマクロはサンプルのようなものなので、あまり厳密に決めるつもりはありません。

以下の通りでお願いします。

  • このブログのマクロおよびスクリプトについては自由に使用していただいて構いません。
  • コードの部分的な使用や、改変しての使用、配布も問題ありません。
  • 他のブログや SNS などで引用する場合は、このブログへのリンクもつけていただけると嬉しいです。
  • マクロやスクリプトの動作は保証いたしかねます。
  • マクロやスクリプトの使用よって生じる問題について責任は負いかねます。

スクリプトや RPA でメールを送信すると送信トレイにたまったり、二重に送信されたりする

スクリプトや Outlook 以外の VBA、RPA などで Outlook オブジェクト モデルを使用してメールを送信する際に、以下のような現象が発生する場合があります。

  • メールが送信トレイに残ったまま送信されない
  • 一度送信したはずのメールが再送される

このような現象は、Outlook を起動していない状態でスクリプトなどによりメールを送信し、その後すぐに Outlook オブジェクト モデルのインスタンスを開放することで発生します。

Outlook はメールの送信処理をバックグラウンド処理で実行するため、Send メソッドでメールを送信すると、そのメールが送信トレイに格納されて送信処理が開始された時点で Send メソッドが終了してスクリプトなどに制御が戻ります。
つまり、Send メソッドが終了した時点では、実はまだメールの送信が完了していないことになります。

Outlook がすでに起動している状態であれば、そのまま送信処理が継続して完了するので問題はありません。
しかし、スクリプトなどにより Outlook が起動された場合は、そのスクリプトで Outlook のオブジェクトをすべて解放すると Outlook に対する外部の参照がなくなるので、Outlook の終了処理が行われます。
その結果、メールの送信処理が中断されて送信トレイにたまったり、送信ができても送信トレイのアイテムが送信済みアイテムフォルダーに移動されないことで、次回起動時に再び送信されたりするのです。

この問題を回避するには、スクリプトが実行される環境で常に Outlook を起動しておくか、メールの送信が完了するまでスクリプトから Outlook のオブジェクトを参照し続ける必要があります。
以下は、メールの送信が完了し、送信トレイからメールがなくなるまで待機するスクリプトのサンプルです。

' olkApp に Outlook.Application オブジェクトが格納されていると想定
' 送信トレイを取得
Set fldOut = olkApp.Session.GetDefaultFolder(4)
' 送信トレイにアイテムがなくなるまでループ
While fldOut.Items.Count > 0
     ' 10 秒待つ
     WScript.Sleep 10000
Wend

このサンプルだと、何らかの理由 (例えばサーバーのダウンなど) でメールの送信ができない場合にスクリプトが終わらなくなってしまうので、タイムアウトなどを設定する必要があるかもしれません。

メールをマクロで作成して送信するための基礎知識

スクリプトや Excel などの VBA マクロを使って Outlook から自動的にメールを送信したいということはありませんか?
今回はそのような Outlook からマクロを使ってメールを送信する方法と、いくつかのオプションについて説明します。

送信のための準備

スクリプトや Outlook 以外の Office VBA からメールを送信する場合、最初に Outlook の Application オブジェクトを生成する必要があります。
具体的には以下のようなコードです。

    Set olkApp = CreateObject("Outlook.Application")

これにより olkApp という変数に Outlook の Application オブジェクトが格納されます。
なお、Outlook の VBA を使用する場合は上記は必要ありません。
元から存在する Application という変数にすでに格納済みですので、こちらを使用します。

メール オブジェクトの作成

メールを送信するためには、MailItem オブジェクトが必要です。
このオブジェクトに件名などの必要な情報を設定して Send メソッドで送信することにより、Outlook でメールを送信することができます。
MailItem オブジェクトを新規作成するには Application オブジェクトの CreateItem メソッドにメールアイテムを意味する 0 を指定します。
コードは以下のようになります。

    Set objMail = olkApp.CreateItem(0)

Outlook VBA の場合は以下のようになります。

    Set objMail = Application.CreateItem(0)

最低限のプロパティを指定して送信

メールを送信するために必要最低限のプロパティは以下の 3 つです。

  • 宛先 (To)
  • 件名 (Subject)
  • 本文 (Body または HtmlBody)

実際には件名と本文はなくても送信できてしまうのですが、通常は必ず設定するものとなるでしょう。
宛先には名前だけを入れてアドレス帳で名前解決して送信することも可能ですが、合致するエントリーが複数あったような場合に送信に失敗する可能性があるので、メールアドレスを設定したほうが良いでしょう。
宛先に表示名が必要であれば以下のように指定します。

    objMail.To = """表示名"" <アドレス>"

例えば Test User という表示名で test@example.com に送信する場合は以下の通りです。

    objMail.To = """Test User"" <test@example.com>"

複数の宛先を指定するときは ; で区切ります。
Cc や Bcc を指定する場合は、それぞれ objMail.Cc、objMail.Bcc にアドレスを設定します。

本文に対応するプロパティとしては Body と HtmlBody があり、Body はテキスト形式、HtmlBody は HTML 形式の本文になります。
リッチテキスト形式の本文も RTFBody プロパティでアクセス可能ですが、扱いが難しいので今回は割愛します。

必要なプロパティを設定したら Send メソッドで送信します。
送信前に表示させて自分で送信したいなら Display メソッドで表示を行います。
以下は、メール送信スクリプトのサンプルです。

    Set olkApp = CreateObject("Outlook.Application")
    Set objMail = olkApp.CreateItem(0)
    objMail.To = """Test User"" <test@example.com>"
    objMail.Subject = "テストメール"
    objMail.Body = "これはテストです。"
    objMail.Send

以降はメール送信時の様々なオプションの指定方法について説明しますが、これらは objMail.Send の前に指定します。

メールにファイルを添付

メールにファイルを添付するには MailItem オブジェクトの Attachments プロパティの Add メソッドを使用します。
コードは以下のようになります。

    objMail.Attachments.Add "ファイルのフルパス"

例えば、c:\temp\test.docx というファイルを添付する場合は以下の通りです。

    objMail.Attachments.Add "c:\temp\test.docx"

メールに配信日時を指定

メールをすぐに送信せず、決まった日時に送信したい場合、MailItem オブジェクトの DeferredDeliveryTime にその日時を設定します。
例えば、2021/5/22 12:00 に送信したい場合は以下の通りです。

    objMail.DeferredDeliveryTime = #2021/05/22 12:00#

もし、現在時刻から 10 分後というような指定がしたければ、以下のように Now 関数で現在時刻を取得し、DateAdd 関数で 10 分後の時間を計算します。

    objMail.DeferredDeliveryTime = DateAdd("n", 10, Now())

なお、配信日時指定をする場合、その日時に Outlook が起動してメールが送信可能な状態となっている必要があります。

メールに重要度を指定

Outlook ではメールの重要度として高、標準、低の 3 種類が設定可能であり、MailItem オブジェクトの Importance プロパティで指定します。
指定する値は高が 2、標準が 1、低が 0 です。
例えば、重要度高で送信する場合は以下の通りです。

    objMail.Importance = 2

メールの差出人アドレスを指定

通常使用している差出人アドレスと異なるアドレスでメールを送信するには MailItem オブジェクトの SendOnBehalfOfName プロパティに表示名とアドレスを指定します。
例えば Test User という表示名の test@example.com というアドレスを差出人に指定する場合は以下の通りです。

    objMail.SendOnBehalfOfName = """Test User"" <test@example.com>"

なお、これによりメールヘッダーの From は変更されるのですが、実際の送信者を表す Sender に元の差出人アドレスが指定されるので、受信側で「~ が代理で送信: ~」というように表示される場合があります。

メールの送信アカウントを指定

プロファイルにアカウントが複数ある場合、そのまま送信すると既定のアカウントが使用されます。
既定以外のアカウントで送信するには、Application オブジェクトの Session プロパティの Accounts から送信するアカウントの Account オブジェクトを取得し、MailItem オブジェクトの SendUsingAccount プロパティにそのアカウントを設定します。
コードは以下のようになります。

    Set objAccount = olkApp.Session.Accounts("送信アカウントのメールアドレス")
    objMail.SendUsingAccount = objAccount

メールの返信アドレスを指定

送信したメールの返信を別のアドレスにする (Reply-To ヘッダーに別のアドレスを指定する) には、MailItem オブジェクトの ReplyRecipients プロパティの Add メソッドでアドレスを指定します。
例えば Test User という表示名の test@example.com というアドレスを返信先に指定する場合は以下の通りです。

    objMail.ReplyRecipients.Add """Test User"" <test@example.com>"

このようにすると、このメールに対する返信は指定したアドレスのみに届き、自分自身には届かないことになりますので注意が必要です。

配信確認通知や開封確認通知の要求

メールがサーバーに到達した際の通知 (配信確認通知) や受信者がメールを開封した際の通知 (開封確認通知) を要求する場合は、それぞれ MailItem オブジェクトの OriginatorDeliveryReportRequested や ReadReceiptRequested に True を設定します。
コードは以下のようになります。

    objMail.OriginatorDeliveryReportRequested = True
    objMail.ReadReceiptRequested = True

なお、配信確認通知は受信者側のサーバーが配信確認をサポートしている必要があります。
また、開封確認通知は受信者側のクライアントが開封確認をサポートしており、かつ受信者が開封確認通知を送信することを選択しない限り通知されません。
開封確認通知はスパムなどで悪用される可能性もあるので、組織によっては開封確認通知が送信されないようにしている場合もあります。

テンプレートを基にメールを送信する

メール本文が長文だったり、画像が埋め込まれていたりすると、メールの文面を一からマクロで作るのはちょっと大変です。
このような場合は、あらかじめ本文などを設定したメールをテンプレート ファイル (OFT) として保存し、CreateItem の代わりに CreateItemFromTemplate メソッドでそのファイルを基にメールを作成して送信することができます。
件名やの一部を変更して送信する必要がある場合は、あらかじめ変更したい部分に特定のキーワードを埋め込んで置き、送信時に Replace 関数を使って文字列を置き換えます。
宛先や件名、本文などが設定済みのテンプレート ファイルを “c:\temp\sample.oft” として保存し、本文中の %DATE% という文字列を送信する日付に置き換えてマクロで送信する場合は以下のようなコードになります。

    Set objMail = olkApp.CreateItem("c:\temp\sample.oft")
    objMail.Body = Replace(objMail.Body, "%DATE%", FormatDateTime(Now(), vbShortDate))
    objMail.Send

受信日時が一定の日数より前のメールを年と月のサブフォルダーに移動するスクリプト

受信トレイにたまったメールを整理する方法としては、以下のようなものがあります。

  • 古いアイテムの整理で個人用フォルダー ファイルに移動する
  • サブフォルダーを作ってルールまたは手動で移動する

しかし、古いアイテムの整理では個人用フォルダー ファイルの受信トレイに大量のメールがたまってしまったり、サブフォルダーへの移動ではどのフォルダーに振り分けるべきか悩ましいようなメールが出てきたりします。

いっそ受信日時の年と月のフォルダーに振り分けてしまえばいいのではないかと思い、そのようなスクリプトを作ってみました。

このスクリプトを実行すると EXPIRE_DAYS で指定した日数を経過している受信日時の年、月というフォルダー階層のサブフォルダーに移動されます。
例えば、2021 年 4 月に受信したメールであれば、受信トレイの下の 2021 というフォルダーのさらに下の 04 というフォルダーに移動されます。

Windows のタスク スケジューラーで毎月 1 日にこのスクリプトを実行するというようなタスクを作っておくとよいかもしれません。
スクリプトは以下の通りです。

'
On Error Resume Next
Const EXPIRE_DAYS = 60
Const olFolderInbox = 6
Dim olkApp
Dim fldInbox
Dim fldYear
Dim fldMonth
Dim i
Dim strYearMonth
' Outlook.Application を取得
Set olkApp = CreateObject("Outlook.Application")
' 受信トレイフォルダーを取得
Set fldInbox = olkApp.Session.GetDefaultFolder(olFolderInbox)
' 移動する場合はアイテムの最後から繰り返す
For i = fldInbox.Items.Count To 1 Step -1
     With fldInbox.Items(i)
         ' 受信日時が EXPIRE_DAYS で指定された日数よりも前なら
         If DateDiff("d", .ReceivedTime, Now) > EXPIRE_DAYS Then
             ' 受信日の年のフォルダーを取得
             strYear = Year(.ReceivedTime)
             Set fldYear = fldInbox.Folders(strYear)
             If Err.Number <> 0 Then
                 ' フォルダーがなければ作成
                 Set fldYear = fldInbox.Folders.Add(strYear)
                 Err.Clear
             End If
             ' 受信日の月のフォルダーを取得
             strMonth = Right("0" & Month(.ReceivedTime), 2)
             Set fldMonth = fldYear.Folders(strMonth)
             If Err.Number <> 0 Then
                 ' フォルダーがなければ作成
                 Set fldMonth = fldYear.Folders.Add(strMonth)
                 Err.Clear
             End If
             ' フォルダーに移動
             .Move fldMonth
         End If
     End With
Next

2021 年の海の日、山の日およびスポーツの日を移動するスクリプトと 2021 年以降の祝日を追加するスクリプト

11 月 27 日に 2020 年の海の日、山の日およびスポーツの日が東京オリンピックに合わせて移動されるという法律が可決、成立しました。

今後、この法律に基づいて更新された Outlook の祝日ファイルも更新プログラムとして提供されることになると思いますが、すでに延長サポート フェーズに入っている Outlook 2016 や Outlook 2013、先日サポートを終了した Outlook 2010 には提供されない可能性があります。
また、Outlook 2019 や Microsoft 365 Apps for Enterprise の Outlook でも、更新プログラムを適用すれば変更されるというわけではなく、いったん祝日を削除して改めて追加するという作業が必要になります。

そのようなわけで、祝日を移動するスクリプトを作ってみました。
以下のテキストをメモ帳などのテキストエディタに保存し、拡張子を vbs として保存してダブルクリックで実行すると、祝日が移動します。

Option Explicit
Const olFolderCalendar = 9
Const olAppointmentItem = 1
Const olFree = 0
Dim olkApp
Dim fldCal
Dim apptHol
'
Set olkApp = CreateObject("Outlook.Application")
Set fldCal = olkApp.Session.GetDefaultFolder(olFolderCalendar)
'
MoveHoliday fldCal, "海の日", "7/19", "7/22"
MoveHoliday fldCal, "山の日", "8/11", "8/8"
MoveHoliday fldCal, "スポーツの日", "10/11", "7/23"
MoveHoliday fldCal, "体育の日", "10/11", "7/23"
'
Set apptHol = olkApp.CreateItem(olAppointmentItem)
With apptHol
     .Subject = "振替休日 (山の日)"
     .Start = "2021/8/9 0:00"
     .AllDayEvent = True
     .Categories = "祝日"
     .ReminderSet = False
     .BusyStatus = olFree
     .Location = "日本"
     .Save
End With
'
Sub MoveHoliday(fldCal, strName, strStart, strNewStart)
     Set apptHol = fldCal.Items.Find("[件名]='" & strName & _
         "' And [開始日]='2021/" & strStart & " 00:00'")
     If Not apptHol Is Nothing Then
         apptHol.Start = CDate("2021/" & strNewStart)
         apptHol.Save
     End If
End Sub

なお、2021 年以降の祝日もスクリプトで追加したいというような要望もあるかと思い、2021 年から 2026 年までの祝日を追加するスクリプトも作成しました。

Option Explicit
Const olFolderCalendars = 9
Const olAppointmentItem = 1
Const olFree = 0
Dim objOutlook
Dim objSession
Dim objCalendar
Dim colEvents
Dim objHoliday
Dim iYear
' Outlook アプリケーション オブジェクトの取得
Set objOutlook = CreateObject("Outlook.Application")
' Namespace オブジェクトの取得
Set objSession = objOutlook.GetNamespace("MAPI")
' 予定表フォルダの取得
Set objCalendar = objSession.GetDefaultFolder(olFolderCalendars)
Set colEvents = objCalendar.Items
' 予定表から 2021 年以降の祝日のみを取得
Set objHoliday = colEvents.Find("[分類項目] = '祝日' AND [開始日] >= '2020/12/31' AND [場所] = '日本'")
' 2021 年以降の祝日を削除
While Not objHoliday Is Nothing
     objHoliday.Delete
     Set objHoliday = colEvents.FindNext
Wend
'
' 2021 年から 2026 年までの祝日を追加
For iYear = 2021 to 2026
     AddNormalHoliday "勤労感謝の日", iYear, 11, 23
     AddNormalHoliday "文化の日", iYear, 11, 3
     AddHappyMonday "敬老の日", iYear, 9, 3
     AddNormalHoliday "こどもの日", iYear, 5, 5
     AddNormalHoliday "みどりの日", iYear, 5, 4
     AddNormalHoliday "憲法記念日", iYear, 5, 3
     AddNormalHoliday "昭和の日", iYear, 4, 29
     AddNormalHoliday "建国記念の日", iYear, 2, 11
     AddHappyMonday "成人の日", iYear, 1, 2
     AddNormalHoliday "元日", iYear, 1, 1
     AddNormalHoliday "天皇誕生日", iYear, 2, 23
     If iYear > 2021 Then
         AddHappyMonday "スポーツの日", iYear, 10, 2
         AddNormalHoliday "山の日", iYear, 8, 11
         AddHappyMonday "海の日", iYear, 7, 3
     End If
Next
' 日付が一定でない祝日の追加
AddNormalHoliday "春分の日", 2019, 3, 21
AddNormalHoliday "春分の日", 2020, 3, 20
AddNormalHoliday "春分の日", 2021, 3, 20
AddNormalHoliday "春分の日", 2022, 3, 21
AddNormalHoliday "春分の日", 2023, 3, 21
AddNormalHoliday "春分の日", 2024, 3, 20
AddNormalHoliday "春分の日", 2025, 3, 20
AddNormalHoliday "春分の日", 2026, 3, 20
AddNormalHoliday "秋分の日", 2019, 9, 23
AddNormalHoliday "秋分の日", 2020, 9, 22
AddNormalHoliday "秋分の日", 2021, 9, 23
AddNormalHoliday "秋分の日", 2022, 9, 23
AddNormalHoliday "秋分の日", 2023, 9, 23
AddNormalHoliday "秋分の日", 2024, 9, 22
AddNormalHoliday "秋分の日", 2025, 9, 23
AddNormalHoliday "秋分の日", 2026, 9, 23
' 敬老の日と秋分の日に挟まれるため
AddNormalHoliday "国民の休日", 2026, 9, 22
' 2020 年の祝日移動のための特別処理
AddNormalHoliday "海の日", 2021, 7, 22
AddNormalHoliday "山の日", 2021, 8, 8
AddNormalHoliday "スポーツの日", 2021, 7, 23
'
' 振り替え休日を考慮しない祝日の追加
Sub AddHoliday( sName, dtDay )
     Set objHoliday = objOutlook.CreateItem(olAppointmentItem)
     objHoliday.Subject = sName
     objHoliday.Start = dtDay
     objHoliday.AllDayEvent = True
     objHoliday.Categories = "祝日"
     objHoliday.ReminderSet = False
     objHoliday.BusyStatus = olFree
     objHoliday.Location = "日本"
     objHoliday.Save
     Set objHoliday = Nothing
End Sub
'
' ハッピーマンデーの祝日の追加
Sub AddHappyMonday( sName, iYear, iMonth, iMonday )
     Dim iWk
     Dim iDay
     Dim dtDay
     iWk = Weekday(iYear & "/" & iMonth & "/1" )
     If iWk <= 2 Then
         iWk = iWk + 4
     Else
         iWk = iWk - 3
     End If
     iDay = 7 * iMonday - iWk
     AddHoliday sName, iYear & "/" & iMonth & "/" & iDay & " 00:00 AM"
End Sub
'
' 通常 (振り替え休日あり) の祝日の追加
Sub AddNormalHoliday( sName, iYear, iMonth, iDay )
     Dim iWk
     Dim dtSub
     Dim objHoliday
     AddHoliday sName, iYear & "/" & iMonth & "/" & iDay & " 00:00 AM"
     iWk = Weekday( iYear & "/" & iMonth & "/" & iDay )
     If iWk = 1 Then
         dtSub = CDate(iYear & "/" & iMonth & "/" & iDay)
         Do    ' 振替休日が国民の祝日だったら、翌日に繰り越し
             dtSub = DateAdd("d", 1, dtSub)
             Set objHoliday = colEvents.Find("[分類項目] = '祝日' AND [開始日] >= '" & _
                 dtSub & " 00:00 AM' AND [終了日] <= '" & DateAdd("d", dtSub, 1) & _
                 "' AND [場所] = '日本'")
         Loop While Not objHoliday Is Nothing
         AddHoliday "振替休日 (" & sName & ")", dtSub & " 00:00 AM"
     End If
End Sub

IMAP アカウントの設定を Autodiscover により行うためのスクリプト

コメントにて以下のご要望をいただきました。


日頃より参考にさせていただいております。貴重な情報ありがとうございます。
Outlook2016を利用しております。
アカウント追加を手動で行わせると、利用者による入力間違い多発しておりますため、スクリプトで行いたいと思っておりますが、スクリプトでのアカウント設定(IMAP)は可能でしょうか?


残念ながら、スクリプトで Outlook のアカウント設定を行うことはできません。
しかし、Outlook の Autodiscover によりユーザーごとに共通であるサーバー名や使用ポートなどの設定を自動的に行うことは可能です。
以下のようなスクリプトを実行後、プロファイル作成やアカウント追加の際にメールアドレスを入力して [次へ] をクリックすると、自動的にサーバー設定が行われます。
スクリプト内のドメイン名やサーバー名などは必要に応じて変更してください。

' ここをトリプルクリックでスクリプト全体を選択できます。
' メール アカウントのドメイン名を指定
Const DOMAIN_NAME = "example.com"
' IMAP の設定
' IMAP サーバーの FQDN を指定
Const IMAP_SERVER = "imap.example.com"
' IMAP サーバーの TCP ポート番号を指定
' 通常、暗号化ありは 993、なしは 143
Const IMAP_PORT = 993
' IMAP サーバーの認証で SPA を使用するかを指定
' 使用する場合は on、しない場合は off
Const IMAP_SPA = "off"
' IMAP 通信で暗号化を使用するかを指定
' 使用する場合は on、しない場合は off
Const IMAP_SSL = "on"
' SMTP の設定
' SMTP サーバーの FQDN を指定
Const SMTP_SERVER = "smtp.example.com"
' SMTP サーバーの TCP ポート番号を指定
' 通常は 25、ISP などでは 587
Const SMTP_PORT = 587
' SNTP サーバーの認証で SPA を使用するかを指定
' 使用する場合は on、しない場合は off
Const SMTP_SPA = "off"
' SMTP 通信で使用する暗号化方式を指定
' 使用しない場合は none、TLS 方式は tls、SSL 方式は ssl
Const SMTP_ENC = "tls"
' SNTP サーバーで認証を行うか指定
' 認証する場合は on、しない場合は off
Const SMTP_AUTH = "on"
'
Const AUTO_DISCOVER_XML_FILE = "c:\autodiscover\autodiscover.xml"
Const AUTO_DISCOVER_KEY = "HKCU\SOFTWARE\Microsoft\Office\16.0\Outlook\AutoDiscover\"
'
Const adTypeText = 2
Const adSaveCreateOverwrite = 2
Dim stmXml
Set stmXml = CreateObject("ADODB.Stream")
With stmXml
     .Open
     .Type = adTypeText
     .Charset = "UTF-8"
'
     .WriteText "<?xml version=""1.0"" encoding=""utf-8""?>", 1
     .WriteText "<Autodiscover xmlns=""http:"
     .WriteText "//schemas.microsoft.com/exchange/autodiscover/"
     .WriteText "responseschema/2006""><Response xmlns=""http:"
     .WriteText "//schemas.microsoft.com/exchange/autodiscover/"
     .WriteText "outlook/responseschema/2006a"">", 1
     .WriteText "  <Account>", 1
     .WriteText "    <AccountType>email</AccountType>", 1
     .WriteText "    <Action>settings</Action>", 1
     .WriteText "    <Protocol>", 1
     .WriteText "      <Type>IMAP</Type>", 1
     .WriteText "      <Server>" & IMAP_SERVER &"</Server>", 1
     .WriteText "      <Port>" & IMAP_PORT &"</Port>", 1
     .WriteText "      <SPA>" & IMAP_SPA &"</SPA>", 1
     .WriteText "      <SSL>" & IMAP_SSL &"</SSL>", 1
     .WriteText "      <AuthRequired>on</AuthRequired>", 1
     .WriteText "    </Protocol>", 1
     .WriteText "    <Protocol>", 1
     .WriteText "      <Type>SMTP</Type>", 1
     .WriteText "      <Server>" & SMTP_SERVER &"</Server>", 1
     .WriteText "      <Port>" & SMTP_PORT &"</Port>", 1
     .WriteText "      <SPA>" & SMTP_SPA &"</SPA>", 1
     .WriteText "      <Encryption>" & SMTP_ENC &"</Encryption>", 1
     .WriteText "      <AuthRequired>" & SMTP_AUTH &"</AuthRequired>", 1
     .WriteText "      <UsePOPAuth>off</UsePOPAuth>", 1
     .WriteText "    </Protocol>", 1
     .WriteText "  </Account>", 1
     .WriteText "</Response></Autodiscover>", 1
'
     .SaveToFile AUTO_DISCOVER_XML_FILE, adSaveCreateOverwrite
     .Close
End With
'
Set WshShell = CreateObject("WScript.Shell")
WshShell.RegWrite AUTO_DISCOVER_KEY & DOMAIN_NAME, AUTO_DISCOVER_XML_FILE, "REG_SZ"

キャッシュ モードのオフラインの期間を取得するスクリプト

コメントにて以下のご要望をいただきました。


はじめまして。outlook vba 初心者です。
下記の取得方法をご教授いただければありがたいです。
(outlook2013)
「ファイル」>「アカウント設定」>「アカウント設定(A)」>
「Mictosoft exchange」を選択して「変更」をクリックで表示される
「アカウントの変更」画面の「オフライン設定」の「Exchange キャッシュモードを使う」にチェックが入力されているかどうかと「オフラインにしておくメール:」の期間を取得したいのです。
GetNamespace(“MAPI”)のExchangeConnectionModeでは状態により数値が変化しています。
恐れ入りますが、ヒントだけでもいただければありがたいです。
宜しくお願いいたします


キャッシュ モードの [オフラインにしておくメール] の設定は MAPI プロファイルに格納されています。
具体的な場所についての説明はややこしいので、スクリプトを作成しました。

' ここをトリプルクリックでスクリプト全体を選択できます。
Const LOG_FILE = "C:\temp\cache_sync_window.txt" ' ログを出力するファイル名
Const HKEY_CURRENT_USER = &H80000001
Const MAPI_PROFILE_KEY = "Software\Microsoft\Office\15.0\Outlook\Profiles"
Const OUTLOOK_KEY = "Software\Microsoft\Office\15.0\Outlook"
' Outlook 2016 以降は以下を使用
' Const MAPI_PROFILE_KEY = "Software\Microsoft\Office\16.0\Outlook\Profiles"
' Const OUTLOOK_KEY = "Software\Microsoft\Office\16.0\Outlook"
Const MAPI_SERVICES_KEY = "9207f3e0a3b11019908b08002b2a56c2"
Const PR_STORE_PROVIDERS = "01023d00"
Const PR_EMSMDB_SECTION_UID = "01023d15"
Const SYNC_WINDOW_SETTING_MONTHS = "00036649"
Const SYNC_WINDOW_SETTING_DAYS = "0003665a"
Const PR_PROFILE_DISPLAY_NAME = "001f3001"
Dim stdRegProv
Dim strDefaultProfile
Dim strProfileKey
Dim arrStoreUIDs
Dim strServicesKey
Dim objFS
Dim stmText
Dim iCount
Dim i,j
Dim strServiceKey
Dim strSectionKey
Dim strSectionKeys
Dim strDisplayName
Dim iSyncMonths
Dim iSyncDays
Dim arrData
Dim arrSync
'
Set stdRegProv = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
stdRegProv.GetStringValue HKEY_CURRENT_USER, OUTLOOK_KEY, "DefaultProfile", strDefaultProfile
strProfileKey = MAPI_PROFILE_KEY & "\" & strDefaultProfile & "\"
strServicesKey = strProfileKey & MAPI_SERVICES_KEY
stdRegProv.GetBinaryValue HKEY_CURRENT_USER, strServicesKey, PR_STORE_PROVIDERS, arrStoreUIDs
'
Set objFS = CreateObject("Scripting.FileSystemObject")
Set stmText = objFS.CreateTextFile(LOG_FILE,True)
'
stmText.WriteLine "プロファイル: " & strDefaultProfile
strSectionKeys = ""
iCount = (UBound(arrStoreUIDs)+1)/16
For i=0 To iCount-1
     strServiceKey = ""
     For j=0 To 15
         strServiceKey = strServiceKey & Right("0" & Hex(arrStoreUIDs(i*16+j)), 2)
     Next
     stdRegProv.GetBinaryValue HKEY_CURRENT_USER, strProfileKey & strServiceKey, PR_EMSMDB_SECTION_UID, arrData
     If Not IsNull(arrData) Then
         strSectionKey = ""
         For j=0 To 15
             strSectionKey = strSectionKey & Right("0" & Hex(arrData(j)), 2)
         Next
         '
         If Instr(strSectionKeys, strSectionKey) = 0 Then
             ' 表示名取得
             stdRegProv.GetBinaryValue HKEY_CURRENT_USER, strProfileKey & strSectionKey, PR_PROFILE_DISPLAY_NAME, arrData
             If Not IsNull(arrData) Then
                 strDisplayName = BinToUnicode(arrData)
             End If
             ' 月単位の同期期間を取得
             stdRegProv.GetBinaryValue HKEY_CURRENT_USER, strProfileKey & strSectionKey, SYNC_WINDOW_SETTING_MONTHS, arrSync
             If Not IsNull(arrSync) Then
                 iSyncMonths = arrSync(0)
             Else
                 iSyncMonths = 0
             End If
             ' 月単位の同期期間が設定されていたらログ出力
             If iSyncMonths > 0 Then
                 stmText.WriteLine vbTab & strDisplayName & " の キャッシュ期間: " & iSyncMonths & "カ月"
             End If
             ' 日単位の同期期間を取得
             stdRegProv.GetBinaryValue HKEY_CURRENT_USER, strProfileKey & strSectionKey, SYNC_WINDOW_SETTING_DAYS, arrSync
             If Not IsNull(arrSync) Then
                 iSyncDays = arrSync(0)
             Else
                 iSyncDays = 0
             End If
             ' 日単位の同期期間が設定されていたらログ出力
             If iSyncDays > 0 Then
                 stmText.WriteLine vbTab & strDisplayName & " の キャッシュ期間: " & iSyncDays & "日"
             End If
             '
             strSectionKeys = strSectionKeys & strSectionKey
         End If
     End If
Next
stmText.Close
'
Set stdRegProv = Nothing
Set stmText = Nothing
Set objFS = Nothing
'
Function BinToUnicode( arrData )
     Dim strUnicode
     Dim i
     strUnicode = ""
     For i = 0 To UBound(arrData) Step 2
         strUnicode = strUnicode & ChrW( arrData(i) + arrData(i+1) * &h100 )
     Next
     BinToUnicode = Replace( strUnicode, Chr(0), "" )
End Function

Outlook オブジェクト モデルを呼び出すスクリプトをタスク スケジューラーで実行する際の注意点

MAPI についてのコメントにて以下のご質問をいただきました。


質問です。
MAPIを使って、outlookアカウントに送信される添付ファイルをローカルに保存するpowershellのスクリプトを作成しました。
https://kapibara-sos.net/archives/394
こちらを参考しています。

powershellを作成後、.ps1ファイルを直接実行すると添付ファイルの保存ができますが
.ps1ファイルをwindowsのタスクスケジューラで実行すると、MAPIのインスタンスが作成されません。

試したこと:
・タスクスケジューラの実行権限を最上に権限にする
・.ps1ファイルをタスクスケジューラで実行する際には、powershellの実行権限をBypassにしている。

よろしくお願いいたします。   


参考にされたサイトのスクリプトを見たところ、MAPI を呼び出しているのではなく、Outlook のオブジェクト モデルを呼び出しているようです。

Outlook を含む Office 製品のオブジェクト モデルについては以下の Microsoft 技術情報にある通り、サーバー サイド オートメーションがサポートされていません。

257757 Office のサーバー サイド オートメーションについて

サーバー サイド オートメーションとは、Web サーバー上でのプログラムの実行のようにユーザーとコンソールでの対話がない状態で実行される状態を指します。

Outlook のオブジェクト モデルを使用する場合、その処理は Outlook のプロセスで実行されますが、Outlook はユーザーが対話形式で使用することが前提となっているため、スクリプトなどから呼び出される場合でも Outlook はユーザーとの対話ができる状態となっている必要があるということなのです。

また、Outlook が起動するには Outlook のプロファイルが必要となりますが、Outlook のプロファイルはユーザーのレジストリから読み込まれるため、スクリプトを実行するユーザーのレジストリに Outlook のプロファイルが設定されていなければなりません。

つまり、「サーバー サイド オートメーション」と記載されていますが、実際にはクライアント上で動作する場合でも、Outlook オブジェクト モデルを呼び出すようなスクリプトをサービスの権限で実行したり、非対話形式で実行することはサポートされていないのです。

したがって、タスク スケジューラーで Outlook オブジェクト モデルを使用するスクリプトなどを実行する場合は、まず [セキュリティ オプション] で以下のように設定します。

  • [タスクの実行時に使うユーザー アカウント] として、Outlook のプロファイルが設定済みのユーザーを指定する
  • [ユーザーがログオンしているときのみ実行する] が選択されている

そして、[タスクの実行時に使うユーザー アカウント] で指定したユーザーで Windows にログオンした状態のままにしておくことで、タスク スケジューラーでもスクリプトが実行できるようになります。

Outlook を最小化して起動する方法

コメントにて以下のご要望をいただきました。


いつもお世話になっております。
VBSでの外部コマンド実行する際のRunメソッドに関して
分からない動作があり、質問をさせていただきます。
元々、Outlookの実行を最小化して表示したいと考え、調べたところ
Runメソッドの実行オプションから実行した際のウィンドウのオプションが
指定できるとのことで指定を行ったのですが最小化、最大化、非表示など
何を指定しても、展開されるウィンドウに変化が無くネット上サンプルを動かしてみたところ
全く同じ処理で実行した際にメモ帳(“notepad”)の時には指定通りウィンドウが
最大化して開かれるにもかかわらず、電卓(“calc”)など他アプリケーションでは
エラーも出ていないが指定の実行ウィンドウオプションが全く反映されない現象が発生しました。
同現象について調べてみましたが有用なものが無く、手詰まり困っております。
分かる方がもしおられましたら、ご教授願えると幸いです。
実行環境は
Windows10 Pro 64bitにです。

以下スクリプトになります。
Dim oWshShell
Set oWshShell = CreateObject(“WScript.Shell”)
‘// メモ帳実行、最大化指定(3)→最大化され開かれた
oWshShell.Run “notepad”, 3, True
WScript.Echo “Bye!”

Set oWshShell = CreateObject(“WScript.Shell”)
‘// 電卓実行、最大化指定(3)→最大化されない、念のため非表示(0)でも試したが反映されない
oWshShell.Run “calc”, 3, True
WScript.Echo “Bye!”


私の手元で試す限り、Outlook については以下のような動作となりました。

第 2 パラメータで指定する値 Outlook のウィンドウの動作
2 通常のウィンドウで開く
6 通常のウィンドウで開く
7 最小化で開く

したがって、Run メソッドの第 2 パラメータに 7 を指定すれば Outlook を最小化して開くことができるはずです。
Calc のようなストア アプリなどでは、通常の Windows アプリケーションとウィンドウの操作方法が異なるため、Run メソッドの第2パラメータが有効にならないのではないかと考えられます。

なお、Outlook のオブジェクト モデルにもウィンドウの状態を設定するプロパティ (Explorer オブジェクトの WindowState) があるため、これを設定して起動すれば起動後にウィンドウを最小化することもできます。(ただし、起動直後にいったんウィンドウが表示されます。)
Outlook を起動してウィンドウを最小化するスクリプトは以下のようになります。    

' ここをトリプルクリックでスクリプト全体を選択できます。
Const olFolderInbox = 6
Const olMinimized = 1
Dim olkApp
Dim fldInbox
Dim expInbox
Set olkApp = CreateObject("Outlook.Application")
If olkApp.Explorers.Count = 0 Then
     Set fldInbox = olkApp.Session.GetDefaultFolder(olFolderInbox)
     fldInbox.Display
     Set expInbox = fldInbox.GetExplorer()
     expInbox.WindowState = olMinimized
End If