DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for How I customized Outlook notifications with PowerShell
mdgrs
mdgrs

Posted on

How I customized Outlook notifications with PowerShell

Motivation

I suppose most of the people who use Outlook desktop app turn on desktop banner notifications that pop up on the bottom right corner. They are helpful but I feel a bit distracted. I don't want to read even the summary of emails at random timing. I want to have better control over when to read them. So, I turned off banner notifications and instead, enabled this envelope badge on the taskbar icon you see when you have unread mails. I like this because it's more subtle than banners.

Outlook envelope icon is good as a subtle notification

However, this badge is only shown for your default inbox. If mails are moved to other folders by rules, you don't notice them unless you open the Outlook window. Do I have to give up folders? No, I can make my original folder notifications.

Why PowerShell?

When I do some kind of automation on Windows, I use PowerShell. It's great for testing how objects work using the terminal and easy to turn it into a script. I was just testing out and learning how the Outlook COM objects work on the terminal at first but I ended up making the whole app with PowerShell because as I progressed, most of the things I wanted to do turned out to be possible. That's the reason for me this time. It could also be rewritten in C# if some motivation arises.

Get unread count of an Outlook folder

If you have installed an Outlook desktop app, just open PowerShell and type the following three lines. You'll see your root folders.

$outlook = New-Object -ComObject Outlook.Application
$namespace = $outlook.GetNamespace("MAPI")
$namespace.Folders
Enter fullscreen mode Exit fullscreen mode

Folder object has its subfolders as a property so you can recursively search for the folder that you want to monitor. Each Folder object has an unique FolderPath property which looks like this \\your-email-address@sample.com\folder-name and can be used to identify it.

$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)
        {
            # this is the folder you are looking for
            $subFolder
            return
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Items property of Folder object contains emails. You need to find unread items from them to get the unread count. Items property has Restrict method for this kind of search purpose so you can get the unread count like this:

$unreadItems = $folder.Items.Restrict("[UnRead] = True")
$unreadItems.Count
Enter fullscreen mode Exit fullscreen mode

Refer to the official documentation for other examples of how Restrict function works.

Taskbar icon

Now we got the unread count. We need a place to notify it. As I said earlier, I like badge notifications on taskbar icons so I thought it would be the best if we could show a taskbar icon per Outlook folder and a badge counter on it. To get a taskbar icon, you need a window. Let's create a WPF window in PowerShell.

We don't need any UI elements this time so the xaml file can be like this minimum setup.

<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>
Enter fullscreen mode Exit fullscreen mode

Once you've created a xaml file, you can load it and show the window by the following code:

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()
Enter fullscreen mode Exit fullscreen mode

A window is created and an icon is shown but the problem is that in this way the PowerShell icon is always used.

Powershell icon is always used, this is not what we expect

To customize the icon image, we can create a shortcut to the PowerShell script and assign a custom icon to it. The PowerShell terminal can be also hidden by adding -WindowStyle Hidden argument to powershell.exe. Luckily, we can use PowerShell again to help create the shortcut:

$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()
Enter fullscreen mode Exit fullscreen mode

You'll get this by running the shortcutπŸ‘

You can customize the icon by using a shortcut

Badge counter

The badge on the taskbar icon can be accessed via Overlay property of TaskbarItemInfo class. Define a TaskbarItemInfo in the xaml file:

<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>
Enter fullscreen mode Exit fullscreen mode

then you can access it from PowerShell:

$iconSize = 20
$dpi = 96
$bitmap = New-Object System.Windows.Media.Imaging.RenderTargetBitmap($iconSize, $iconSize, $dpi, $dpi, [System.Windows.Media.PixelFormats]::Default)
# Render an unread count to the bitmap here...
$window.TaskbarItemInfo.Overlay = $bitmap
Enter fullscreen mode Exit fullscreen mode

This code just sets a transparent bitmap so nothing is shown yet. We need to render the unread count to the bitmap. The badge is made with a background and a number on it. The number is a variable part and the background is static so we want to use a template for the background and replace the number afterwards. For this purpose WPF provides ContentControl and DataTemplate. If a DataTemplate is specified to the ContentControl.ContentTemplate property, ContentControl applies the template to its Content and builds the UI. First, let's define a DataTemplate as a xaml resource:

<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>
Enter fullscreen mode Exit fullscreen mode

Then you can render a ContentControl using the template:

$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) # place the UI elements and set the size
$bitmap.Render($control)

$window.TaskbarItemInfo.Overlay = $bitmap
Enter fullscreen mode Exit fullscreen mode

In the xaml file, {Binding Path=Text} means that it is bound to the Text property of the binding source. Here, the binding source is ContentControl.Content so the Text property of the PSCustomObject is passed to the TextBlock.Text.

Unread count is shown as an overlay badge

We're getting close πŸ˜„

Timer function

We have a way to get an unread count and show a badge counter, so now we just need to call it at some interval. We have some Timer objects available in PowerShell but we use System.Windows.Threading.DispatcherTimer this time. It calls a given function in the WPF UI thread. All the UI components, the overlay badge in this case, must be updated from the UI thread so this is the right one.

$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()
Enter fullscreen mode Exit fullscreen mode

What should happen when clicked?

Normally, the associated window opens when its icon is clicked on the taskbar but it's not what we expect for this notification app. The ideal behavior for me is that if the unread count is 1, it opens the unread email directly and if the count is 2 or more, it opens the Outlook folder without showing the app window we created. This can be achieved by adding a StateChanged event handler like the following:

$window.add_StateChanged({
    if ($window.WindowState -eq [System.Windows.WindowState]::Minimized)
    {
        return
    }

    $unreadItems = $folder.Items.Restrict("[UnRead] = True")
    if ($unreadItems.Count -eq 1)
    {
        # show the unread email
        $unreadItems[1].Display()
    }
    else
    {
        $explorer = $outlook.ActiveExplorer()
        if ($explorer)
        {
            # if the Outlook window is already opened, show that window and change the folder
            $explorer.Activate()
            $explorer.CurrentFolder = $folder
        }
        else
        {
            # Outlook window is not yet opened so show a new Explorer
            $folder.Display()
        }
    }

    # hide this window immediately
    $window.WindowState = [System.Windows.WindowState]::Minimized
})
Enter fullscreen mode Exit fullscreen mode

Finally, we got this!

Clicking the icon opens the unread email

What feels intuitive as a click response might be different depending on the person but you can easily customize it using Outlook objects.

Conclusion

In this article I showed my process where I found an issue, came up with an idea and made a small app that meets my needs. I added some customizability to the app and made it public so check out the repository here if you are interested. Hope this article sparks someone's interest.

Top comments (1)

Collapse
 
petergroft profile image
petergroft

On the Outlook menu, click Preferences. Under Personal Settings, click Notifications & Sounds. Under Message arrival, choose the settings that you want for new messages.

Hope This Helps,
Peter

Stop by this week's meme thread!

CSS margins