Outlookのバッジ通知をPowerShellでカスタマイズする
Outlookの未読メール数をフォルダごとに通知するアプリをPowerShellで作成しました。その作り方をご紹介します。
作成したアプリはこちら。
本記事の英語版はこちら。
フォルダ分けすると封筒マークが表示されない
デスクトップ版Outlookのメール通知に、デスクトップバナー通知を使っている方はおそらく多いのではないでしょうか。デスクトップ通知は気づきやすく便利なのですが、ポップアップした時に内容を読んでしまい、個人的には少し気が散ってしまう印象がありました。そこでデスクトップ通知をOFFにして、このタスクバーの封筒マークの通知を代わりに使ってみることにしました。バナーより控えめでいい感じです。
ところがこの封筒マーク、デフォルトの受信箱にメールが届いたときにしか表示されません。仕分けルールでフォルダ分けされると、未読メールに気がづきません。フォルダ通知ぐらいなら自分で作れるのでは?ということで試作を始めました。
なぜPowerShell?
PowerShellは.NetオブジェクトやCOMオブジェクトを作成して扱うこともできます。これらのオブジェクトの挙動をターミナル上でインタラクティブにテストし、うまく動いたらスクリプト化していくということが気軽にできるので個人的には重宝しています。
OutlookのデスクトップアプリをCOMオブジェクト経由で操作できることを知り、初めはこのCOMオブジェクトの挙動をPowerShellでテストしていました。徐々にテストコードが増えていき、特に障壁もなかったためそのまま最後まで作ってしまった、というのが経緯です。今回の理由としてはそれだけでけなので、本来はC#などで書くほうが向いていると思います。
フォルダの未読メール数を取得する
PowerShellを開いて次の3行をペーストして実行してみてください。デスクトップ版Outlookがインストールされていれば、ルートのフォルダがリストされるはずです。
$outlook = New-Object -ComObject Outlook.Application
$namespace = $outlook.GetNamespace('MAPI')
$namespace.Folders
Folder
オブジェクトはそのサブフォルダをプロパティとして保持しているので、目的のフォルダを再帰的に検索することができます。Folder
オブジェクトはそれぞれユニークなFolderPath
プロパティ(\\your-email-address@sample.com\folder-name
のような値を持ちます)を持っているのでそちらで識別することができます。
$folderPath = '\\your-email-address@sample.com\folder-name'
$rootFolders = $namespace.Folders
foreach ($rootFolder in $rootFolders) {
$subFolders = $rootFolder.Folders
foreach ($subFolder in $subFolders) {
if ($subFolder.FolderPath -eq $folderPath) {
# 目的のフォルダ
$subFolder
return
}
}
}
メールはFolder
オブジェクトのItems
プロパティに格納されています。ここから未読のメールをフィルタするために、Items
プロパティのRestrict
メソッドを使用します。
$unreadItems = $folder.Items.Restrict('[UnRead] = True')
$unreadItems.Count
他にどのようなフィルタ処理ができるかはオフィシャルのドキュメントを参照してみてください。
タスクバーアイコン
未読メールの数はこれで取得できました。ここから、フォルダごとにタスクバーアイコンを作成し、バッジで未読数を通知できるようにします。タスクバーアイコンを表示するにはアプリケーションのウィンドウが必要です。PowerShellでWPFウィンドウを作ってみましょう。
このアプリのウィンドウには何もUI要素が必要ないので、xamlファイルはこのような最低限のものにします。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="320" Height="180"
AllowsTransparency="True"
Background="Transparent"
WindowStyle="None">
</Window>
xamlファイルを作成したら、これをロードしてウィンドウを生成します。
Add-Type -AssemblyName PresentationFramework
$xaml = [xml](Get-Content $xamlFilePath)
$nodeReader = New-Object System.Xml.XmlNodeReader $xaml
$window = [System.Windows.Markup.XamlReader]::Load($nodeReader)
$window.ShowDialog()
これでウィンドウが作成されアイコンも表示されますが、デフォルトではPowerShellのアイコンが使用されてしまいます。
アイコンをカスタマイズするにはショートカットを作成してアイコンファイルを指定する方法があります。powershell.exe(またはpwsh.exe)へのショートカットを作成し、今回作成したスクリプトを実行するよう引数を指定します。コンソールを非表示にするため、-WindowStyle Hidden
も引数に追加しておきます。このショートカットもせっかくなのでPowerShellで作成します。
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut('D:\out.lnk')
$shortcut.TargetPath = 'powershell.exe'
$shortcut.Arguments = '-ExecutionPolicy Bypass -WindowStyle Hidden -File "D:\script.ps1"'
$shortcut.WindowStyle = 7 # Minimized
$shortcut.IconLocation = 'D:\sample.ico, 0'
$shortcut.Save()
作ったショートカットを実行するとこうなります。
バッジカウンター
タスクバーアイコンのバッジはTaskbarItemInfo
クラスのOverlay
プロパティでアクセスします。まず、xamlファイルにTaskbarItemInfo
を追加します。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="320" Height="180"
AllowsTransparency="True"
Background="Transparent"
WindowStyle="None">
<Window.TaskbarItemInfo>
<TaskbarItemInfo />
</Window.TaskbarItemInfo>
</Window>
PowerShellからはこのようにアクセスします。
$iconSize = 20
$dpi = 96
$bitmap = New-Object System.Windows.Media.Imaging.RenderTargetBitmap($iconSize, $iconSize, $dpi, $dpi, [System.Windows.Media.PixelFormats]::Default)
# ここで未読数をレンダーする...
$window.TaskbarItemInfo.Overlay = $bitmap
このコードでは透明なビットマップを設定しているだけなので、まだ何も表示されません。このビットマップに未読数をレンダリングしていきます。未読数のバッジは、固定の背景と動的に変化する数字で構成されています。背景とレイアウトを含むテンプレートに数値を入力して、最終的なUI要素を生成したいということになります。WPFはこのような用途のためにContentControl
とDataTemplate
を提供しています。DataTemplate
がContentControl.ContentTemplate
プロパティに指定されると、ContentControl
はそのContent
にテンプレートを適用してUIを構築します。
まず、DataTemplate
をxamlファイルに定義します。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="320" Height="180"
AllowsTransparency="True"
Background="Transparent"
WindowStyle="None">
<Window.TaskbarItemInfo>
<TaskbarItemInfo />
</Window.TaskbarItemInfo>
<Window.Resources>
<DataTemplate x:Key="OverlayIcon">
<Grid Width="20" Height="20">
<Rectangle Fill="DeepPink"
Stroke="White"
StrokeThickness="1"
RadiusX="4"
RadiusY="4"/>
<TextBlock Text="{Binding Path=Text}"
TextAlignment="Center"
VerticalAlignment="Center"
Foreground="White"
FontWeight="Bold"
Height="20"
FontSize="14">
</TextBlock>
</Grid>
</DataTemplate>
</Window.Resources>
</Window>
PowerShell側では以下のようにして、このテンプレートを使いContentControl
をレンダリングすることができます。
$unreadCount = 1
$iconSize = 20
$dpi = 96
$bitmap = New-Object System.Windows.Media.Imaging.RenderTargetBitmap($iconSize, $iconSize, $dpi, $dpi, [System.Windows.Media.PixelFormats]::Default)
$rect = New-Object System.Windows.Rect 0, 0, $iconSize, $iconSize
$control = New-Object System.Windows.Controls.ContentControl
$control.ContentTemplate = $window.Resources['OverlayIcon']
$control.Content = [PSCustomObject]@{
Text = $unreadCount
}
$control.Arrange($rect) # UI要素を配置してサイズを設定する
$bitmap.Render($control)
$window.TaskbarItemInfo.Overlay = $bitmap
xamlファイルの中の{Binding Path=Text}
はそれがBinding SourceのText
プロパティにバインドされることを意味しています。ここでのBinding SourceはContentControl.Content
になります。つまり、PSCustomObject
のText
プロパティがTextBlock.Text
に渡されることになります。
かなり近づいてきました😄
タイマー関数
未読数を取得して表示することができるようになりました。あとは、未読数を更新するためにこれを定期的に実行する必要があります。PowerShellにはいくつかのタイマーオブジェクトがありますが、今回はSystem.Windows.Threading.DispatcherTimer
を使います。DispathcerTimerは指定された関数をWPFのUIスレッドで定期的に実行します。オーバーレイバッジを含むすべてのUI要素はUIスレッドで実行されなければならないので、このタイマーが今回の用途には適切です。
$intervalInSeconds = 5
$func = {
$unreadCount = GetUnreadCount
UpdateIconOverlay $unreadCount
}
$timer = New-Object System.Windows.Threading.DispatcherTimer
$timer.interval = New-Object TimeSpan(0, 0, $intervalInSeconds)
$timer.add_tick($func)
$timer.Start()
クリックしたとき何をするべきか?
タスクバーアイコンをクリックしたとき、通常はウィンドウが開きますが、このアプリについてはそれは望まれる挙動ではありません。ウィンドウを開くことなく、未読メール数が1件の場合はその未読メールを開き、それ以上の場合はOutlookで対象のフォルダを開くのが個人的な理想の挙動です。これはWindowオブジェクトのStateChanged
イベントハンドラを以下のように実装することで実現できます。
$window.add_StateChanged({
if ($window.WindowState -eq [System.Windows.WindowState]::Minimized) {
return
}
$unreadItems = $folder.Items.Restrict('[UnRead] = True')
if ($unreadItems.Count -eq 1) {
# 未読メールを表示する
$unreadItems[1].Display()
}
else {
$explorer = $outlook.ActiveExplorer()
if ($explorer) {
# もしOutlookウィンドウがすでに存在すればそれを表示し、フォルダを変更する
$explorer.Activate()
$explorer.CurrentFolder = $folder
} else {
# Outlookウィンドウが存在しないので新たにExplorerを表示する
$folder.Display()
}
}
# ウィンドウをすぐに隠す
$window.WindowState = [System.Windows.WindowState]::Minimized
})
こうなりました。
クリックしたときの挙動は好みがあると思いますが、Outlookオブジェクトを利用して自由にカスタマイズすることができます。
おわりに
OutlookのCOMオブジェクトを使用してフォルダ通知をカスタマイズする方法をご紹介しました。今回作成したものにさらに拡張性を持たせ、アプリとしてこちらで
公開しているので、興味がありましたらお試しください。
Discussion