2021-10-17

PowerShellでJISコードのメールを送信する (MailKit編) (2)

前回のMimeKit導入編に続き、今回は実装編です。

以下のサイトを参考にPowerShellでJISコード(ISO-2022-JP)でメールを送信するコードを書いてみます。

電子メールを送信するには?(MailKit編)[.NET 4.5、C#/VB]:.NET TIPS - @IT

これまで広く使われてきたSmtpClientクラスは現在、使用が推奨されていない。そこでオープンソースライブラリのMailKitでメールを送信する方法を説明する。


  • 一般的なメールソフトと同様、宛先、CC、件名、本文、添付ファイルを引数にした関数にしています。
  • 宛先、CC、添付ファイルについては複数項目を受け付けるため配列を引数にしています。簡略化のため、宛先とCCの Display Name はなしにしています。
function Send-JisMail {
    param (
        [string[]]$ToArray,
        [string[]]$CcArray,
        [string]$Subject,
        [string]$Body,
        [string[]]$FileArray
    ) 
    # If you don't want to use arrays as arguments
    # $ToArray = $To.Replace(" ","").Split(",")
    Add-Type -Path "C:\Program Files\PackageManagement\NuGet\Packages\Portable.BouncyCastle.1.8.10\lib\net40\BouncyCastle.Crypto.dll"
    Add-Type -Path "C:\Program Files\PackageManagement\NuGet\Packages\MimeKit.2.15.1\lib\net45\MimeKit.dll"
    Add-Type -Path "C:\Program Files\PackageManagement\NuGet\Packages\MailKit.2.15.0\lib\net45\MailKit.dll"

    $fromAddress = "sender@foo.com"
    $fromName = $null
    # 送信サーバ設定
    $smtpServer = "smtp@foo.com"
    $port = "587"
    $user = "user"
    $password = "password"

    $message = New-Object Mimekit.MimeMessage
    $jis = [Text.Encoding]::GetEncoding("iso-2022-jp")
    $from = New-Object MimeKit.MailboxAddress($jis, $fromName, $fromAddress)
    $message.From.Add($from)
    #$ToArray = $toAddress.Replace(" ","").Split(",")
    foreach ($to in $ToArray) {
        $message.To.Add($to)
    }
    $message.Subject = $Subject
    $textPart = New-Object MimeKit.TextPart([MimeKit.Text.TextFormat]::Plain)
    $textPart.SetText($jis, $Body)

    #添付ファイル設定
    if ($FileArray) {
        $multiPart = New-Object MimeKit.Multipart("mixed")
        $multiPart.Add($textPart)
        foreach ($file in $FileArray) {
            $attachment = New-Object MimeKit.MimePart
            $content = New-Object MimeKit.MimeContent([System.IO.File]::OpenRead($file), [MimeKit.ContentEncoding]::Default)
            $attachment.Content = $content
            $contentDisposition = New-Object MimeKit.ContentDisposition([MimeKit.ContentDisposition]::Attachment)
            $attachment.ContentDisposition = $contentDisposition
            $attachment.ContentTransferEncoding = [MimeKit.ContentEncoding]::Base64
            $attachment.FileName = [System.IO.Path]::GetFileName($file)        
            #https://github.com/jstedfast/MimeKit/blob/master/FAQ.md#UntitledAttachments
            # The following sentense will also work.
            # $attachment.ContentDisposition.Parameters[0].EncodingMethod = [MimeKit.ParameterEncodingMethod]::Rfc2047
            foreach ($param in $attachment.ContentDisposition.Parameters) {
                $param.EncodingMethod = [MimeKit.ParameterEncodingMethod]::Rfc2047
            }
            $multiPart.Add($attachment)
        }
        $message.Body = $multiPart
    } else {
        $message.Body = $textPart
    }   

    $client = New-Object MailKit.Net.Smtp.SmtpClient
    $client.Connect($smtpServer, $port, $false)
    $client.Authenticate($user, $password)
    $client.Send($message)
    $client.Disconnect($true)
}

使い方

関数を呼び出す際はSplattingを使うと分かりやすいと思います。

$args = @{
ToArray = @("recipient1@bar.com","recipient2@bar.com")
Subject = "件名"
Body = @"
こんにちは。
これは本文です。
"@
FileArray = @("C:\Temp\新しいテキスト ドキュメント.txt")
}
Send-JisMail @args

Outlookで受信すると添付ファイル名が"ATT0####.dat"になってしまう問題について

MimeKitのFAQにあるとおり、Outlookは RFC 2231 に対応しておらず文字化けすることがあります。上述のコードではFAQに合わせて RFC 2047 を使うようにしています。
参考: 添付ファイルにおける日本語のファイル名に関して

2021-10-16

PowerShellでJISコードのメールを送信する (MailKit編) (1)

SmtpClient編 に続きMailKit編です。

パッケージの確認

https://www.nuget.org/ にアクセスしてMailKitを検索します。
名前・バージョン・Dependenciesを確認して、依存パッケージを辿ります。
MailKit -> MimeKit -> Portable.BouncyCastleの順で依存関係があることが分かります。

MailKit のインストール

PowerShellを管理者モードで起動し、各パッケージをインストールします。

PS > Install-Package -Name Portable.BouncyCastle -Source https://www.nuget.org/api/v2

The package(s) come(s) from a package source that is not marked as trusted.
Are you sure you want to install software from 'https://www.nuget.org/api/v2'?
[Y] はい(Y)  [A] すべて続行(A)  [N] いいえ(N)  [L] すべて無視(L)  [S] 中断(S)  [?] ヘルプ (既定値は "N"): Y

Name                           Version          Source                           Summary
----                           -------          ------                           -------
Portable.BouncyCastle          1.8.10           https://www.nuget.org/api/v2     BouncyCastle portable version with ...


PS > Install-Package -Name MimeKit -Source https://www.nuget.org/api/v2                              
The package(s) come(s) from a package source that is not marked as trusted.
Are you sure you want to install software from 'https://www.nuget.org/api/v2'?
[Y] はい(Y)  [A] すべて続行(A)  [N] いいえ(N)  [L] すべて無視(L)  [S] 中断(S)  [?] ヘルプ (既定値は "N"): Y
Install-Package : Dependency loop detected for package 'MimeKit'.
発生場所 行:1 文字:1
+ Install-Package -Name MimeKit -Source https://www.nuget.org/api/v2
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : Deadlock detected: (MimeKit:String) [Install-Package]、Exception
    + FullyQualifiedErrorId : DependencyLoopDetected,Microsoft.PowerShell.PackageManagement.Cmdlets.InstallPackage
PS > Install-Package -Name MimeKit -Source https://www.nuget.org/api/v2 -SkipDependencies

The package(s) come(s) from a package source that is not marked as trusted.
Are you sure you want to install software from 'https://www.nuget.org/api/v2'?
[Y] はい(Y)  [A] すべて続行(A)  [N] いいえ(N)  [L] すべて無視(L)  [S] 中断(S)  [?] ヘルプ (既定値は "N"): Y

Name                           Version          Source                           Summary
----                           -------          ------                           -------
MimeKit                        2.15.1           https://www.nuget.org/api/v2     An Open Source library for creating...


PS > Install-Package -Name MailKit -Source https://www.nuget.org/api/v2

The package(s) come(s) from a package source that is not marked as trusted.
Are you sure you want to install software from 'https://www.nuget.org/api/v2'?
[Y] はい(Y)  [A] すべて続行(A)  [N] いいえ(N)  [L] すべて無視(L)  [S] 中断(S)  [?] ヘルプ (既定値は "N"): Y

Name                           Version          Source                           Summary
----                           -------          ------                           -------
MailKit                        2.15.0           https://www.nuget.org/api/v2     An Open Source .NET mail-client lib
...

Dependency loop detected について

Dependency loop detected というエラーが出たら、-SkipDependencies オプションを付けて実行します。
https://github.com/OneGet/oneget/issues/475 にあるとおり、現行の OneGetInstall-Package は単純な依存関係でないとエラーが出るようです。PackageManagementがOneGetではなくなるPowerShellGet v3で解消するようですが、現在はベータ版ということもあり確認しておりません。
なお、現在の私のInstall-Package コマンドレットのモジュールとバージョンは以下のとおりです。

PS > Get-Command -Name Install-Package | Select-Object -Property ModuleName

ModuleName
----------
PackageManagement


PS > Get-InstalledModule PackageManagement

Version    Name                                Repository           Description
-------    ----                                ----------           -----------
1.4.7      PackageManagement                   PSGallery            PackageManagement (a.k.a. OneGet) is a new way t...

アセンブリの読み込み

ダウンロードしてアセンブリを読み込んでみます。ダウンロードしたパッケージは -Destination を指定していなければ "C:\Program Files\PackageManagement\NuGet\Packages\” にあります。
Add-Type コマンドレットでMailKit.dllを読み込んでみます。

PS > Add-Type -Path "C:\Program Files\PackageManagement\NuGet\Packages\MailKit.2.15.0\lib\net45\MailKit.dll"
Add-Type : 要求された型のうち 1 つまたは複数を読み込めませんでした。詳細については、LoaderExceptions プロパティを取得してください。
発生場所 行:1 文字:1
+ Add-Type -Path "C:\Program Files\PackageManagement\NuGet\Packages\Mai ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Add-Type], ReflectionTypeLoadException
    + FullyQualifiedErrorId : System.Reflection.ReflectionTypeLoadException,Microsoft.PowerShell.Commands.AddTypeCommand

エラーが発生しました。

LoaderExceptions プロパティを取得してください。

とのことですので、try-catchブロックで囲んで実行します。

try
{
	Add-Type -Path "C:\Program Files\PackageManagement\NuGet\Packages\MailKit.2.15.0\lib\net45\MailKit.dll"
}
catch [System.Reflection.ReflectionTypeLoadException]
{
	Write-Host  "Message: $($_.Exception.Message)"
	Write-Host  "StackTrace: $($_.Exception.StackTrace)"
	Write-Host  "LoaderExceptions: $($_.Exception.LoaderExceptions)"
}
Message: 要求された型のうち 1 つまたは複数を読み込めませんでした。詳細については、LoaderExceptions プロパティを取得してください。
StackTrace:    場所 System.Reflection.RuntimeModule.GetTypes(RuntimeModule module)
   場所 System.Reflection.Assembly.GetTypes()
   場所 Microsoft.PowerShell.Commands.AddTypeCommand.LoadAssemblyFromPathOrName(List`1 generatedTypes)
   場所 Microsoft.PowerShell.Commands.AddTypeCommand.EndProcessing()
   場所 System.Management.Automation.CommandProcessorBase.Complete()
LoaderExceptions: System.IO.FileNotFoundException: ファイルまたはアセンブリ 'MimeKit, Version=2.15.0.0, Culture=neutral, PublicKeyToken=bede1c8a46c66814'、またはその 
依存関係の 1 つが読み込めませんでした。指定されたファイルが見つかりません。
ファイル名 'MimeKit, Version=2.15.0.0, Culture=neutral, PublicKeyToken=bede1c8a46c66814' です。'MimeKit, Version=2.15.0.0, Culture=neutral, PublicKeyToken=bede1c8a46c66814'

依存関係にあるMimeKitを先に読み込む必要があることが分かります。
したがって、

Add-Type -Path "C:\Program Files\PackageManagement\NuGet\Packages\Portable.BouncyCastle.1.8.10\lib\net40\BouncyCastle.Crypto.dll"
Add-Type -Path "C:\Program Files\PackageManagement\NuGet\Packages\MimeKit.2.15.1\lib\net45\MimeKit.dll"
Add-Type -Path "C:\Program Files\PackageManagement\NuGet\Packages\MailKit.2.15.0\lib\net45\MailKit.dll"

の順にするとエラーなく各アセンブリを読み込むことができます。
なお、以下のようにAdd-Type の代わりに .NET Framework のLoadFileメソッドを使うこともできます。その場合は依存関係によるエラーは出ません。

[System.Reflection.Assembly]::LoadFile("C:\Program Files\PackageManagement\NuGet\Packages\MailKit.2.15.0\lib\net45\MailKit.dll")

次回実装編に続く。

2021-10-08

PowerShellでJISコードのメールを送信する (SmtpClient編[obsoleted])

こちらにあるとおり

ℹ 重要
SmtpClientは多くの最新プロトコルをサポートしていないため、新規開発にSmtpClientクラスを使用することはお勧めしません。代わりにMailKitや他のライブラリを使用してください。詳細については、GitHubのSmtpClient shouldn’t be usedをご覧ください。

SmtpClientクラスは廃止(obsolete)となりました。

後継のMailkitを使用したバージョンはこちらです。


ここでは、SmtpClientクラスを使って

を参考にPowerShellでJISコード(ISO-2022-JP)でメールを送信するコードを書いてみます。

  • 元のコードからSMTP認証あり、添付ファイルありに変更しています。
  • 一般的なメールソフトと同様、宛先、CC、件名、本文、添付ファイルを引数にした関数にしています。
  • 宛先、CC、添付ファイルについては複数項目を受け付けるため配列を引数にしています。簡略化のため、宛先とCCの Display Name はなしにしています。
function EncodeBase64([string]$str, [Text.Encoding]$enc) {
    if (!$str) {
        return $null
    }
    $base64str = [Convert]::ToBase64String($enc.GetBytes($str))
    Return [string]::Format("=?{0}?B?{1}?=", $enc.BodyName, $base64str)
}

function Send-JisMail {
    param (
        [string[]]$toArray,
        [string[]]$ccArray,
        [string]$subject,
        [string]$body,
        [string[]]$fileArray
    ) 
    # If you don't want to use arrays as arguments
    # $toArray = $to.Replace(" ","").Split(",")
 
    $fromAddress = "sender@foo.com"
    ## If you dont't need display nama, set empty
    $fromName = ""
    # 送信サーバ設定
    $server = "smtp@foo.com"
    $port = "587"
    $user = "user"
    $password = "password"
    
    $client = New-Object Net.Mail.SmtpClient($server, $port)
    $client.Credentials = New-Object Net.NetworkCredential($user, $password)
    
    $message = New-Object Net.Mail.MailMessage
    $jis = [Text.Encoding]::GetEncoding("iso-2022-jp")
    
    # 送信元
    $from = New-Object Net.Mail.MailAddress($fromAddress, $fromName)
    $message.From = New-Object Net.Mail.MailAddress($from)
    # 送信先
    foreach ($to in $toArray) {
        $message.To.Add($to)
    }
    # CC
    foreach ($cc in $ccArray) {
        $message.CC.Add($cc)
    }
    # 件名
    #$message.Subject = EncodeBase64 $subject $jis
    # for .NET Framework 4.5
    $message.Subject = EncodeBase64 (EncodeBase64 $subject $jis) $jis
    
    # 本文
    $view = [Net.Mail.AlternateView]::CreateAlternateViewFromString($body, $jis, [Net.Mime.MediaTypeNames]::Text.Plain)
    $view.TransferEncoding = [Net.Mime.TransferEncoding]::SevenBit
    $message.AlternateViews.Add($view)
    
    # 添付ファイル
    foreach ($file in $fileArray) {
        $attachment = New-Object Net.Mail.Attachment($file)
        $message.Attachments.Add($attachment)    
    }
    
    $client.Send($message)
    $message.Dispose()
}

使い方

関数を呼び出す際はSplattingを使うと分かりやすいと思います。

$args = @{
toArray = @("recipient1@bar.com","recipient2@bar.com")
subject = "件名"
body = @"
こんにちは。
これは本文です。
"@
fileArray = @("C:\Temp\新しいテキスト ドキュメント.txt")
}
Send-JisMail @args

.NET Framework 4.5 の System.Net.Mail は、エンコードした件名をデコードしてしまう(base64を指定してもQuoted-printableになる)とのことです。
そのため、上記のコードでは二重にエンコードしています。