Send-MailMessageが非推奨になった今、PowerShellでセキュアにメールを送信する方法

# 1. 背景

PowerShellスクリプト内からメール送信する際```Send-MailMessage```や```Net.Mail.SmtpClient```を使うのはすでに非推奨(obsolete)になっています。

[Send-MailMessage (Microsoft.PowerShell.Utility)](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/send-mailmessage)

[SmtpClient クラス (.NET)](https://docs.microsoft.com/ja-jp/dotnet/api/system.net.mail.smtpclient)

これらに代わり、この記事を書いている2022/04時点ではAzureに登録したアプリケーションへMicrosoft Graph APIで接続してメールを送信するのが、推奨されるセキュアな方法になっているという理解です。

この場合でもクライアントシークレット認証を使う方法ですと、PowerShellスクリプト側に何らかの方法でクライアントシークレットを保存する必要があり、あまりセキュアとは言えません。

そのためここでは社内に閉じたメール自動送信を前提として、クライアントで作成した自己署名証明書を利用する方法を採用します。(社外向けに大量にメール送信したい場合は適切なSaaSを使うべきでしょう)

# 2. 事前準備

## 2.1. 必要なPowerShellモジュールのインストール

PowerShellスクリプトを実行するマシンで管理者権限でPowerShellを開き、以下のモジュールをインストールする。

```PowerShell
Install-Module Microsoft.Graph.Users.Actions
Install-Module MSAL.PS
```

1行目はMicrosoft Graphでメール送信アクションを実行するため、2行目はMicrosoft Graphに対してMicrosoft Authentication Library (MSAL)で認証を行うために必要です。

## 2.2. Azure Active Directoryにアプリを登録

メールを送信するためのアプリをAzure Active Directoryに登録します。

1. Azure Active Directoryで「アプリの登録」＞「新規登録」を選択
2. 「アプリケーションの登録」画面で「名前」は適当に（説明のため仮に「TestMgUserMail」とする）
3. 「この組織ディレクトリのみ含まれるアカウント」を選択
4. 「リダイレクトURI」は省略
5. 「登録」をクリック

後で使うので「概要」メニューから「アプリケーション(クライアント)ID」と「ディレクトリ(テナント)ID」の値を控えておきます。

## 2.3. 登録したアプリにAPIアクセス許可を設定

登録したアプリでMicrosoft Graphのメール送信APIを利用できるようにします。

1. Azure Active Directoryで「アプリの登録」を選択、「所有しているアプリケーション」タブに表示される「TestMgUserMail」アプリを選択
2. 「APIのアクセス許可」＞「アクセス許可の追加」を選択
3. 「APIアクセス許可の要求」画面で「Microsoft Graph」を選択
4. 「アプリケーションの許可」を選択
5. 「アクセス許可を選択する」の検索ボックスに「Mail.Send」と入力し、表示された「Mail.Send」をチェック、「アクセス許可の追加」をクリック
6. 「構成されたアクセス許可」画面で「～に管理者の同意を与えます」をクリック
7. 既定で許可されている「User.Read」は**削除しない**

ユーザーがインタラクティブにスクリプトを実行するのではなく、バッチ処理として実行する前提のため、上記4.で「委任されたアクセス許可」ではなく「アプリケーションの許可」を選択しています。任意のユーザーに「なりすまして」メール送信可能になる点はご注意ください。

## 2.4. クライアント側で自己署名証明書を作成、エクスポート

1. PowerShellスクリプトを実行するクライアント（多くの場合はWindows Server）で自己署名証明書を作成します。下記は有効期限5年の例です。後でエクスポートするため```-KeyExportPolicy```は```Exportable```とします。
2. Windowsスタートメニューから「ユーザー証明書の管理」を起動
3. 「証明書 - 現在のユーザー」＞「個人」＞「証明書」と展開
4. 先ほど作成した証明書を右クリック、「すべてのタスク」＞「エクスポート」を選択
5. 「証明書のエクスポート ウィザード」になるので「次へ」＞「いいえ、秘密キーをエクスポートしません」＞「DER encoded binary X.509 (.CER)」を選択、適当な「ファイル名」で適当な場所にエクスポートする。

```PowerShell:自己署名証明書の作成
New-SelfSignedCertificate -Subject "CN=(証明書の名称を適当に)" `		
    -CertStoreLocation "Cert:\CurrentUser\My" `
    -KeyExportPolicy Exportable -KeySpec Signature `
    -NotAfter (Get-Date).AddYears(5)
```

## 2.5. Azureアプリへエクスポートした自己署名証明書をアップロード

1. Azure Active Directoryの「アプリの登録」メニューから先ほど作成した「TestMgUserMail」を開く
2. 「証明書とシークレット」＞「証明書」タブを選択
3. 「証明書のアップロード」をクリック、先ほどエクスポートしたCERファイルを選択し「追加」をクリック

複数のマシンで実行する場合は、同じ手順で各マシンで自己署名証明書を作成してアップロードすることになります。

以上で事前準備は終わりです。

# 3. PowerShellスクリプトからメールを送信する

以下、PowerShellスクリプトの記述方法を段階に分けて説明します。

## 3.1. 必要モジュールのインポート

```PowerShell:必要モジュールのインポート
Import-Module Microsoft.Graph.Users.Actions
Import-Module MSAL.PS
```

## 3.2. Azureアプリに接続する

下記「テナントID」と「アプリケーション(クライアント)ID」にはアプリ作成時に控えた値を記載します。(Azure AD「アプリの登録」から作成したアプリを開いて「概要」からいつでも確認できます)

```PowerShell:アプリへ接続 (証明書名で)
$tenantId = '(AzureのテナントID)'
$clientId = '(作成したアプリのアプリケーションID)'
$clientCert = Get-ChildItem -Path "Cert:\CurrentUser\My" |
    Where-Object Subject -eq ("(作成した自己署名証明書の名称)")
$MSALToken = Get-MsalToken -TenantId $tenantId `
    -ClientId $clientId -ClientCertificate $clientCert
Connect-MgGraph -AccessToken $MSALToken.AccessToken
```

ここでは先ほど作成した自己署名証明書をわざわざ証明書名で検索していますが、証明書の拇印で直接指定してもよいです。

Windowsメニューから「ユーザー証明書の管理」を起動、作成した自己署名証明書をダブルクリック、「詳細」＞「拇印」をクリック、表示される英数字40文字を**大文字に変換**してから以下のように記述します。

```PowerShell:アプリへ接続 (証明書の拇印で)
$tenantId = '(AzureのテナントID)'
$clientId = '(作成したアプリのアプリケーションID)'
$clientCert = Get-ChildItem -Path "Cert:\CurrentUser\My\拇印の40文字"
$MSALToken = Get-MsalToken -TenantId $tenantId `
    -ClientId $clientId -ClientCertificate $clientCert
Connect-MgGraph -AccessToken $MSALToken.AccessToken
```

## 3.3. メールメッセージを組み立てる

最低限のメッセージをPowerShellのハッシュテーブルで組み立てます。

```PowerShell:メールメッセージを組み立てる
$params = @{
  Message = @{
    Subject = "Test Mail"  # 件名
    Body = @{
      ContentType = "HTML"  # 本文の形式 (ここではHTML形式)
      Content = "<b>This is a test mail.</b>"  # 本文
    }
    ToRecipients = @(  # 宛先
       @{ emailAddress = @{ address = "recipient01@example.com" } }
      ,@{ emailAddress = @{ address = "recipient02@example.com" } }    
    )
    CCRecipients = @(  # CCの宛先
       @{ emailAddress = @{ address = "CCrecipient01@example.com" } }
      ,@{ emailAddress = @{ address = "CCrecipient02@example.com" } }    
    )
    BCCRecipients = @(  # BCCの宛先
       @{ emailAddress = @{ address = "BCCrecipient01@example.com" } }
      ,@{ emailAddress = @{ address = "BCCrecipient02@example.com" } }    
    )
  }
  SaveToSentItems = $false  # 送信者の「送信済みトレイ」にメールを残さないため
}
```

## 3.4. メールを送信する

```PowerShell:メールを送信する
Send-MgUserMail -UserId "sender@example.com" -BodyParameter $params
```

## 3.5. 添付ファイルを追加する

添付ファイルはBASE64に変換する必要があります。

```PowerShell:添付ファイルを追加する
$base64Attachment01 = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("C:\hogehoge\hogehoge.xlsx"))
$base64Attachment02 = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes("C:\hogehoge\hogehoge.docx"))

$params = @{
  Message = @{
    Subject = "Test Mail"
    Body = @{
      ContentType = "HTML"
      Content = "<b>This is a test mail.</b>"
    }
    ToRecipients = @(
      @{ emailAddress = @{ address = "recipient01@example.com" } }
    )
    Attachments = @(  # 添付ファイルの指定
      @{
        "@odata.type" = "#microsoft.graph.fileAttachment"
        Name = "hogehoge.xlsx"
        ContentBytes = $base64Attachment01
      }
      ,@{
        "@odata.type" = "#microsoft.graph.fileAttachment"
        Name = "hogehoge.docx"
        ContentBytes = $base64Attachment02
      }
    )
  }
  SaveToSentItems = $false
}
Send-MgUserMail -UserId "sender@example.com" -BodyParameter $params
```

## 3.6. 複数アドレス指定の効率化

宛先、CC、BCCに複数アドレスを指定する部分を効率化するには以下のような方法があるようです。

```PowerShell:複数アドレス指定の効率化
function convertToMgAddresses($addresses) {  # 関数名や引数名は適当に
    foreach ($address in $addresses) {
        @{ emailAddress = @{ address = $address } }
    }
}

$Tos = @(
   "recipient01@example.com"
  ,"recipient02@example.com"
  ,"recipient03@example.com"
)
$CCs = @(
   "CCrecipient01@example.com"
  ,"CCrecipient02@example.com"
  ,"CCrecipient03@example.com"
)

$params = @{
  Message = @{
    Subject = "Test Mail"
    Body = @{
      ContentType = "HTML"
      Content = "<b>This is a test mail.</b>"
    }
    ToRecipients = @( (convertToMgAddresses -addresses $Tos) )
    CCRecipients = @( (convertToMgAddresses -addresses $CCs) )
  }
  SaveToSentItems = $false
}
Send-MgUserMail -UserId "sender@example.com" -BodyParameter $params
```

# 3.7. 複数添付ファイル指定の効率化

複数の添付ファイル指定も同様の方法で効率化できます。

```PowerShell:複数添付ファイル指定の効率化
function convertToMgAttachments($fullNames) {  # 関数名や引数名は適当に
    foreach ($fullName in $fullNames) {
        $fileName = Split-Path -Path $fullName -Leaf
        $base64Attachment = [convert]::ToBase64String([System.IO.File]::ReadAllBytes($fullName))
        @{
            "@odata.type" = "#microsoft.graph.fileAttachment"
            Name = $fileName
            ContentBytes = $base64Attachment
        }
    }
}

$attachments = @(
   "C:\hogehoge\hogehoge.xlsx"
  ,"C:\hogehoge\hogehoge.docx"
  ,"C:\hogehoge\hogehoge.pptx"
)

$params = @{
  Message = @{
    Subject = "Test Mail"
    Body = @{
      ContentType = "HTML"
      Content = "<b>This is a test mail.</b>"
    }
    ToRecipients = @( (convertToMgAddresses -addresses $Tos) )
    CCRecipients = @( (convertToMgAddresses -addresses $CCs) )
    Attachments  = @( (convertToMgAttachments -fullNames $attachments) )
  }
  SaveToSentItems = $false
}
Send-MgUserMail -UserId "sender@example.com" -BodyParameter $params
```

# おわりに

これでようやく```Send-MailMessage```のセキュアな代替手段が見つかったという個人的感想です。

もちろん```Invoke-WebRequest```で直接Microsoft Graph APIを呼び出すことは出来ますが、PowerShellコマンドレットの方が可読性は上がるでしょう。

# 参考ページ

['SENDING EMAIL WITH SEND-MGUSERMAIL (MICROSOFT GRAPH POWERSHELL)' Mike Crowley](https://mikecrowley.us/2021/10/27/sending-email-with-send-mgusermail-microsoft-graph-powershell/)

['Moving on from Send-MailMessage: Sending Email from PowerShell using the Graph API' (Practical 365)](https://practical365.com/upgrade-powershell-scripts-sendmailmessage/)