2012/12/19

PowerShellでGoogle Apps Email Migration APIを呼び出す

PowerShell Advent Calendar 2012参加企画。

この記事では、PowerShellでGoogle Apps Email Migration APIを呼び出すC#サンプルスクリプトをPowerShellに移植したものを紹介する。

PowerShellの利点はいくつもあるが、特筆すべきは以下の点だろう。
  • 対話的コマンドラインインターフェース(CLI)を備えている
  • プログラミング言語として現代的な文法を備えている
  • .NET Frameworkを扱うことができる
最初と二番目は、PowerShellは、CMD.exeでできたことを網羅しつつ、現代的な文法を備えたスクリプト言語、と言うことだ。Windows標準環境では、CMD.exeを使うことができたが、文法が貧弱でプログラミングが困難だった。同じくWindows標準環境では、VBScriptやJScriptなどの現代的なスクリプト言語が使えたが、対話的CLIが無かった。
三番目だが、Windows標準環境では、.NET Frameworkの重要度が上がっているが、CMD.exeではこれを直接扱う方法が無かった。PowerShellでは可能になった。

そこで、この記事では、PowerShellから.NET Frameworkへアクセスするスクリプトを紹介する。ただ、PowerShellから標準的な.NET Frameworkのライブラリにアクセスする方法は、他にもいい記事があるので、改めて紹介する意味は薄い。ここでは少し趣向を変え、PowerShellからGoogle Apps Email Migration APIを使う例を紹介する。Google Apps Email Migration APIのクライアントライブラリは、Java版・Python版の他に.NET版も提供されている。このサンプルスクリプトmigrationsample.csをPowerShellに移植した。コードmigrationsample.ps1(ダウンロード)は以下の通り。
<#
.SYNOPSIS
Sample script to demonstrate the use of the Google Apps Domain Migration API client library.

.DESCRIPTION
Sample script to demonstrate the use of the Google Apps Domain Migration API client library.
The original C# version is from https://developers.google.com/google-apps/email-migration/.

.INPUTS
Nothing.

.OUTPUTS
Nothing.
#>

[CmdletBinding()param(
    # The hosted domain (e.g. example.com) in which the migration will occur.
    [String]
    [parameter(Mandatory=$true)]
    $domain,

    # The username of the administrator or user migrating mail.
    [String]
    [parameter(Mandatory=$true)]
    $adminUsername,

    # The password of the administrator or user migrating mail.
    [String]
    [parameter(Mandatory=$true)]
    $adminPassword,

    # The username to which emails should be migrated.
    # End users can only transfer mail to their own mailboxes.
    # If unspecified, will default to login_email.
    [String]
    [parameter(Mandatory=$true)]
    $destinationUser = ''
)

#####
$REDIST_PATH = "$Env:ProgramFiles\Google\Google Data API SDK\Redist"
Add-Type -Path "$REDIST_PATH\Google.GData.Apps.dll"
Add-Type -Path "$REDIST_PATH\Google.GData.Client.dll"
Add-Type -Path "$REDIST_PATH\Google.GData.Extensions.dll"

$LabelFriend = New-Object Google.GData.Extensions.Apps.LabelElement("Friends")
$LabelEventInvitations = New-Object Google.GData.Extensions.Apps.LabelElement("Event Invitations")

$PropertyElementINBOX = [Google.GData.Extensions.Apps.MailItemPropertyElement]::INBOX
$PropertyElementSTARRED = [Google.GData.Extensions.Apps.MailItemPropertyElement]::STARRED
$PropertyElementUNREAD = [Google.GData.Extensions.Apps.MailItemPropertyElement]::UNREAD


[String]$rfcTxt = @"
Received: by 10.143.160.15 with HTTP; Mon, 16 Jul 2007 10:12:26 -0700 (PDT)
Message-ID: <_message_id_ mail.gmail.com="mail.gmail.com">
Date: Mon, 16 Jul 2007 10:12:26 -0700
From: "Mr. Serious" <serious adomain.com="adomain.com">
To: "Mr. Admin" <testadmin apps-provisioning-test.com="apps-provisioning-test.com">
Subject: Random Subject
MIME-Version: 1.0
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
Delivered-To: testadmin@apps-provisioning-test.com

This is a message delivered via DMAPI
"@

$random = New-Object Random


if($destinationUser -eq '') {
    $destinationUser = $adminUsername
}


$mailItemService = New-Object Google.GData.Apps.Migration.MailItemService($domain, "Sample Migration Application")
$mailItemService.setUserCredentials("$adminUsername@$domain", $adminPassword)


<#
.DESCRIPTION
Generates a random RFC822 message based on the template in rfcTxt.
We have to randomly modify the subject and message ID to prevent duplicate
supression by the Gmail server.

.INPUTS
Nothing.

.OUTPUTS
The randomly-modified RFC822 message.
#>
Function GenerateRandomRfcText
{
    ($rfcTxt -replace "Random Subject", "Random Subject $($random.Next())") `
        -replace "_message_id_", "$($random.Next())"
}


<#
.DESCRIPTION
Helper method to set up a new MailItemEntry.

.INPUTS
Nothing.

.OUTPUTS
The newly created MailItemEntry.
#>
Function SetupMailItemEntry
{
    param(
        # The batch ID for this entry.
        [String]$batchId
    )

    $entry = New-Object Google.GData.Apps.Migration.MailItemEntry

    [void]$entry.Labels.Add($LabelFriend)
    [void]$entry.Labels.Add($LabelEventInvitations)

    [void]$entry.MailItemProperties.Add($PropertyElementINBOX)
    [void]$entry.MailItemProperties.Add($PropertyElementSTARRED)
    [void]$entry.MailItemProperties.Add($PropertyElementUNREAD)

    $entry.Rfc822Msg = New-Object Google.GData.Extensions.Apps.Rfc822MsgElement(GenerateRandomRfcText)

    $entry.BatchData = New-Object Google.GData.Client.GDataBatchEntryData
    $entry.BatchData.Id = $batchId

    $entry
}


<#
.DESCRIPTION
Demonstrates inserting several mail items in a batch.

.INPUTS
Nothing.

.OUTPUTS
A MailItemFeed with the results of the insertions.
#>
Function BatchInsertMailItems
{
    param(
        # The number of entries to insert.
        [Int]$numToInsert
    )

    [Google.GData.Apps.Migration.MailItemEntry[]]$entries = @()

    # Set up the mail item entries to insert.
    foreach($index in 0..($numToInsert-1)) {
        $entries += SetupMailItemEntry([String]$index)
    }

    # Execute the batch request and print the results.
    [Google.GData.Apps.Migration.MailItemFeed]$batchResult = $mailItemService.Batch($domain, $destinationUser, $entries)
    [Google.GData.Client.AtomEntry]$entry = $Null
    foreach($entry in $batchResult.Entries) {
        [Google.GData.Client.GDataBatchEntryData]$batchData = $entry.BatchData

        Write-Host "Mail message $($batchData.Id): $($batchData.Status.Code) $($batchData.Status.Reason)"
    }

    $batchResult
}

try {
    # Insert several emails in a batch.
    [Google.GData.Apps.Migration.MailItemFeed]$batchResults = BatchInsertMailItems(10)
} catch [Google.GData.Client.GDataRequestException] {
    Write-Host ("Operation failed ({0}): {1}" -f $Error[0].Message, $Error[0].ResponseString)
}

使い方は次の通り。
migrationsample.ps1 domain userEmail password [destinationEmail]
ここで、domainは、Google Appsのドメイン名、userEmailpasswordは、対象となるユーザの資格情報。もしdestinationEmailを与えると、対象となるユーザを指定したことになり、その場合、userEmailpasswordは、Google Appsのドメイン管理者の資格情報を与える必要がある。

以下はその実行例。
PS C:\Users\user01> .\migrationsample.ps1 example.co.jp appsmaster 'password' appsuser
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
PS C:\Users\user01>
この例では、10通すべてのメールデータの処理結果が状態コード201で、Google Apps上に移行されたことが判る。

なお、このコードを実行するためには、事前に次の作業を実行しておく必要がある。
  1. Google Data APIクライアントライブラリのダウンロードおよびインストール
  2. PowerShell実行ポリシの変更

実際のメールデータ移行作業での注意点については、こちらを参照して欲しい。

2012/12/04

Outlookのメッセージ作成で意図しないフォントが使われる

Outlookには、妙な「仕様」がある。メッセージを作成するときに、意図しないフォントが使われることがあるのだ(詳細は後述)。
これは、『KB414804: [OL2002] 半角英数字の入力時に フォントが Arial で表示される』でも触れられているのだが、MSによれば「仕様」であって、バグではないそうだ。これを回避するには、レジストリを操作する必要があるが、今回は、これをPowerShellで実行する方法を紹介する。

[Windows]+[R]でpowershellを起動し、以下の通り実行する。
PS C:\> New-ItemProperty 'HKCU:\Software\Microsoft\Office\11.0\Outlook\Preferences' -Name 'DontUseDualFont' -value 1 -propertyType DWORD


DontUseDualFont : 1
PSPath          : Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\Microsoft\Office\11.0\Outlook\Preferences
PSParentPath    : Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\Microsoft\Office\11.0\Outlook
PSChildName     : Preferences
PSDrive         : HKCU
PSProvider      : Microsoft.PowerShell.Core\Registry



PS C:\>
なお、上はOutlook2003の例。引数中の「11.0」は、Outlookのバージョンによって適宜換える必要がある。

以下、詳細。

Outlook 2003の場合、「ツール(T)」→「オプション(O)...」で、「オプション」ダイアログを表示される。


メッセージ形式に「テキスト形式」が選択されていることが確認できる。

さらに、「メール形式」タブ→「フォント(F)...」ボタンを押下すると、「フォント」ダイアログが表示される。


メッセージのフォントに「MS 明朝, 9 pt」が選択されていることが確認できる。

この状態でメッセージを作成した場合、常識的に考えれば、全角文字・半角文字双方にMS明朝が使われることが期待されるが、実際にはそうならない。メッセージを新規作成し、「テストtest」と入力し、その行をコピーして、次の行にペーストしてみよう。


 一行目と二行目の「test」のフォントが違うのが判る。


この二行をMS-Word等にコピー&ペーストしてみると判るが、一行目はArialで、二行目はMS明朝だ。

ここで、上の対策を実施し、Outlookを再起動して、同じようにメッセージを作成してみる。


期待通り、一行目と二行目の「test」のフォントが同じになった。


この二行をMS-Word等にコピー&ペーストすると、一行目・二行目共にMS明朝になっているのが判る。

この現象は、「テキスト形式」の場合だけでなく、「リッチテキスト形式」の場合にも発生する。

2012/12/03

The Sample Code of Google Apps Email Migration API in PowerShell

Google Apps Email Migration APIの.NET版のサンプルコードmigrationsample.csは、拡張子から判る通り、C#で記述してある。これをコンパイルして実行する方法は、『Google Apps Email Migration APIのC#サンプルプログラムをコンパイルし実行』で紹介した。

しかし、メールデータの移行と言った、一度しか行わないような作業に関して、C#の様なコンパイラ言語を使うのは牛刀割鶏、大げさすぎる。そこでこの記事では、Windowsの新スクリプト言語であるPowerShellを使う方法を紹介する。

コードmigrationsample.ps1(ダウンロード)は以下の通り。
<#
.SYNOPSIS
Sample script to demonstrate the use of the Google Apps Domain Migration API client library.

.DESCRIPTION
Sample script to demonstrate the use of the Google Apps Domain Migration API client library.
The original C# version is from https://developers.google.com/google-apps/email-migration/.

.INPUTS
Nothing.

.OUTPUTS
Nothing.
#>

[CmdletBinding()param(
    # The hosted domain (e.g. example.com) in which the migration will occur.
    [String]
    [parameter(Mandatory=$true)]
    $domain,

    # The username of the administrator or user migrating mail.
    [String]
    [parameter(Mandatory=$true)]
    $adminUsername,

    # The password of the administrator or user migrating mail.
    [String]
    [parameter(Mandatory=$true)]
    $adminPassword,

    # The username to which emails should be migrated.
    # End users can only transfer mail to their own mailboxes.
    # If unspecified, will default to login_email.
    [String]
    [parameter(Mandatory=$true)]
    $destinationUser = ''
)

#####
$REDIST_PATH = "$Env:ProgramFiles\Google\Google Data API SDK\Redist"
Add-Type -Path "$REDIST_PATH\Google.GData.Apps.dll"
Add-Type -Path "$REDIST_PATH\Google.GData.Client.dll"
Add-Type -Path "$REDIST_PATH\Google.GData.Extensions.dll"

$LabelFriend = New-Object Google.GData.Extensions.Apps.LabelElement("Friends")
$LabelEventInvitations = New-Object Google.GData.Extensions.Apps.LabelElement("Event Invitations")

$PropertyElementINBOX = [Google.GData.Extensions.Apps.MailItemPropertyElement]::INBOX
$PropertyElementSTARRED = [Google.GData.Extensions.Apps.MailItemPropertyElement]::STARRED
$PropertyElementUNREAD = [Google.GData.Extensions.Apps.MailItemPropertyElement]::UNREAD


[String]$rfcTxt = @"
Received: by 10.143.160.15 with HTTP; Mon, 16 Jul 2007 10:12:26 -0700 (PDT)
Message-ID: <_message_id_ mail.gmail.com="mail.gmail.com">
Date: Mon, 16 Jul 2007 10:12:26 -0700
From: "Mr. Serious" 
To: "Mr. Admin" 
Subject: Random Subject
MIME-Version: 1.0
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
Delivered-To: testadmin@apps-provisioning-test.com

This is a message delivered via DMAPI
"@

$random = New-Object Random


if($destinationUser -eq '') {
    $destinationUser = $adminUsername
}


$mailItemService = New-Object Google.GData.Apps.Migration.MailItemService($domain, "Sample Migration Application")
$mailItemService.setUserCredentials("$adminUsername@$domain", $adminPassword)


<#
.DESCRIPTION
Generates a random RFC822 message based on the template in rfcTxt.
We have to randomly modify the subject and message ID to prevent duplicate
supression by the Gmail server.

.INPUTS
Nothing.

.OUTPUTS
The randomly-modified RFC822 message.
#>
Function GenerateRandomRfcText
{
    ($rfcTxt -replace "Random Subject", "Random Subject $($random.Next())") `
        -replace "_message_id_", "$($random.Next())"
}


<#
.DESCRIPTION
Helper method to set up a new MailItemEntry.

.INPUTS
Nothing.

.OUTPUTS
The newly created MailItemEntry.
#>
Function SetupMailItemEntry
{
    param(
        # The batch ID for this entry.
        [String]$batchId
    )

    $entry = New-Object Google.GData.Apps.Migration.MailItemEntry

    [void]$entry.Labels.Add($LabelFriend)
    [void]$entry.Labels.Add($LabelEventInvitations)

    [void]$entry.MailItemProperties.Add($PropertyElementINBOX)
    [void]$entry.MailItemProperties.Add($PropertyElementSTARRED)
    [void]$entry.MailItemProperties.Add($PropertyElementUNREAD)

    $entry.Rfc822Msg = New-Object Google.GData.Extensions.Apps.Rfc822MsgElement(GenerateRandomRfcText)

    $entry.BatchData = New-Object Google.GData.Client.GDataBatchEntryData
    $entry.BatchData.Id = $batchId

    $entry
}


<#
.DESCRIPTION
Demonstrates inserting several mail items in a batch.

.INPUTS
Nothing.

.OUTPUTS
A MailItemFeed with the results of the insertions.
#>
Function BatchInsertMailItems
{
    param(
        # The number of entries to insert.
        [Int]$numToInsert
    )

    [Google.GData.Apps.Migration.MailItemEntry[]]$entries = @()

    # Set up the mail item entries to insert.
    foreach($index in 0..($numToInsert-1)) {
        $entries += SetupMailItemEntry([String]$index)
    }

    # Execute the batch request and print the results.
    [Google.GData.Apps.Migration.MailItemFeed]$batchResult = $mailItemService.Batch($domain, $destinationUser, $entries)
    [Google.GData.Client.AtomEntry]$entry = $Null
    foreach($entry in $batchResult.Entries) {
        [Google.GData.Client.GDataBatchEntryData]$batchData = $entry.BatchData

        Write-Host "Mail message $($batchData.Id): $($batchData.Status.Code) $($batchData.Status.Reason)"
    }

    $batchResult
}

try {
    # Insert several emails in a batch.
    [Google.GData.Apps.Migration.MailItemFeed]$batchResults = BatchInsertMailItems(10)
} catch [Google.GData.Client.GDataRequestException] {
    Write-Host ("Operation failed ({0}): {1}" -f $Error[0].Message, $Error[0].ResponseString)
}
このコードを実行するには、Google Data APIをインストールしておく必要がある。『Google Data API Installer MSIのダウンロード』および『Google Data API Installer MSIのインストール』の通りにインストールする。
また、『PowerShell実行ポリシの変更』の通り、ローカルスクリプトを実行できるようPowerShellの実行ポリシを変更しておく必要もある。

以下の通り実行する。
PS C:\Users\user01> .\migrationsample.ps1 example.co.jp appsmaster 'password' appsuser
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
Mail message : 201 Created
PS C:\Users\user01>
この例では、10通すべてのメールデータの処理結果が状態コード201で、Google Apps上に移行されたことが判る。

なお、実際のメールデータを移行した場合、以下のような結果を得る。
状態コード201 / Created
正常終了。Google Apps上にメールデータが作成された。ただし、このメッセージを受けてから、Gmailのウェブインターフェース上から参照できるようになるまでには、若干(数分間~数十分間)時間がかかることがある。
状態コード400 / Permanent failure: BadAttachment
添付ファイル形式の異常。Gmailでは、実行形式の添付ファイルが禁止されているが、その様なメールを移行しようとした場合。
状態コード400 / Permanent failure: Insert failed, badly formed message.
メールのサイズが大きすぎる。最大一通当り10MB。
状態コード400 / Bad Request
RFC5322形式(所謂RFC822形式)として正しくないデータ。ドキュメントによると、From:To:、およびDate:のヘッダフィールドが必須。未検証。
状態コード503 / The server is currently busy and could not complete your request. Please try again in 30 seconds.
クラウド側が高負荷状態のため、処理に失敗した。ドキュメントによると、この状態コードを得た場合、xponential backoff方式で再試行せよ、とある。例えば、1回この状態なら次の再試行は30秒後、続けて2回この状態なら次の再試行は60秒後、…と言う様に、続けてn回この状態なら次の再試行は30×2^(n-1)秒後、と言う様に処理する。
実行時エラー
以下の様な実行時エラーが発生する場合がある。バッチ呼び出しの最大サイズ制限に抵触している場合などに発生する。一回のバッチ呼び出しで、メールサイズの合計は、25MB以下に抑えなければならない。
"3" 個の引数を指定して "Batch" を呼び出し中に例外が発生しました: "転送接続にデータを書き込めません: 既存の接続はリモート ホストに強制的に切断されました。。"
発生場所 C:\path\to\script.ps1:line number 文字:charactor number
+                 [Google.GData.Apps.Migration.MailItemFeed]$result = $script:mailItemService.Batch <<<< ($domain, $user, $list)
    + CategoryInfo          : NotSpecified: (:) []、MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException
"3" 個の引数を指定して "Batch" を呼び出し中に例外が発生しました: "Execution of request failed: https://apps-apis.google.com/a/feeds/migration/2.0/example.com/username/mail/batch"
発生場所 C:\path\to\script.ps1:line number 文字:charactor number
+                 [Google.GData.Apps.Migration.MailItemFeed]$result = $script:mailItemService.Batch <<<< ($domain, $user, $list)
    + CategoryInfo          : NotSpecified: (:) []、MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

コーディング上注意して欲しいのは、メールデータはUTF-8でなければならない点。日本語のメールデータは通常、ISO-2022-JPで符号化されているため、そのままでは不正なデータとされ、処理されない。これを回避するためには、メールデータ全体をBASE64で符号化する必要がある。