OutlookMonitor.ps1
<#PSScriptInfo
.VERSION 5.3.0 .GUID 992ee67a-f846-4563-9a00-18291f13798c .AUTHOR julianzs .COMPANYNAME .COPYRIGHT .TAGS .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .PRIVATEDATA #> <# .DESCRIPTION To install, run PowerShell.exe -executionpolicy bypass -command "& .\OutlookMonitor.ps1 -install" in an administrative command prompt window" #> [CmdletBinding()] Param( #Do not hide Powershell console so you can see error and debug output [switch]$doNotHideConsole, #Install script and create scheduled task [switch]$install, #Uninstall Install script and remove scheduled task [switch]$uninstall, #Send calendar actionable message and close [Parameter(Mandatory = $False, ParameterSetName='sendCalendarActionableMessage')] [switch]$sendCalendarActionableMessage, [Parameter(Mandatory=$False,ParameterSetName='sendCalendarActionableMessage')] [string]$calendarActionableMessageSender, [Parameter(Mandatory=$False,ParameterSetName='sendCalendarActionableMessage')] [string]$calendarActionableMessageRecipient ) Begin { If ($PSBoundParameters['Debug']) { $DebugPreference = 'Continue' } ##########Types and objects for generating actionable messages########## #Template for Actionable Message JSON $jsonTemplate = @' { "type": "AdaptiveCard", "originator": "42559281-d9f8-40da-8db9-682c137000d8", "body": [ { "type": "Container", "spacing": "None", "items": [ { "type": "TextBlock", "text": "Meeting Usage Survey", "wrap": true, "size": "ExtraLarge", "weight": "Bolder", "horizontalAlignment": "Left" } ], "padding": "Default", "id": "header", "backgroundImage": "https://coolcolors.lbl.gov/LBNL-Pigment-Database/assets/film-images/fullsize/half-height/0648-half-height.jpg" }, { "type": "Container", "items": [ { "type": "TextBlock", "text": "The Workplace Intelligence Team would like to learn more about meetings at Microsoft. We are inviting you to take a **one question** survey on how you use meetings.", "wrap": true, "spacing": "Medium" } ] }, { "type": "Container", "items": [ { "type": "TextBlock", "size": "Medium", "weight": "Bolder", "text": "Which of these meetings did you or will you attend?", "wrap": true }, { "type": "ColumnSet", "columns": [ { "type": "Column", "width": "auto", "spacing": "None", "padding": "None" }, { "type": "Column", "width": "stretch", "items": [ { "type": "Input.ChoiceSet", "id": "meetingList", "choices": [ { "title": "Cat", "value": "Dog" }, { "title": "Cat2", "value": "Dog2" } ], "style": "expanded", "isMultiSelect": true, "isRequired": true, "wrap": true, "width": "32px", "spacing": "Small" } ], "padding": "None" } ], "spacing": "Small", "horizontalAlignment": "Left", "padding": "None" }, { "type": "ColumnSet", "id": "2b0cc00e-5f5c-0779-597f-a0511b95842f", "columns": [ { "type": "Column", "id": "93ee484f-6c32-2b35-f9ff-fb2b1d51a22a", "padding": "None", "width": "auto", "items": [ { "type": "ActionSet", "actions": [ { "type": "Action.Http", "title": "Submit", "body": "", "method": "POST", "url": "https://prod-02.westcentralus.logic.azure.com:443/workflows/9dbce91ff591424889c7c5820b33888d/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=FMv4psfYoQVgasWtECMDiGMuJ6qPhUjCxmQ6V6RFup4", "headers": [ { "name": "Authorization", "value": "" } ] } ], "id": "submitButton" } ], "horizontalAlignment": "Center" } ], "padding": "None" } ], "padding": "Default", "spacing": "Medium", "id": "meetingChoices", "separator": true, "style": "emphasis" }, { "type": "Container", "items": [ { "type": "TextBlock", "id": "a1ab257b-a410-032c-a115-de406910048f", "text": "We'll use this response to help optimize time spent in meetings and focusing on personal work. Thanks for your help and please share any questions with [Workplace Intelligence Research](mailto:wpirsrch@microsoft.com).", "wrap": true, "separator": true }, { "type": "TextBlock", "text": "[Privacy and Confidentiality Statement](https://microsoft.sharepoint-df.com/:w:/t/OutlookMonitor/EchY7eRFgiFLklwcjbUqnvEB3YJcSaTkn6BuwlP9ScPe6g?e=ExLtDk)", "wrap": true, "spacing": "ExtraLarge", "size": "Small", "weight": "Lighter", "horizontalAlignment": "Center" } ] } ], "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.0", "padding": "None", "@type": "AdaptiveCard", "@context": "http://schema.org/extensions", "hideOriginalBody": true } '@ #Header prepended to JSON to generate HTML message $HTMLHeader = @" <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <script type="application/adaptivecard+json"> "@ #Footer appended to JSON to generate HTML message $HTMLFooter = @" </script> </head> <body> Thanks for helping us make meetings better at Microsoft. Unfortunately, your client does not support viewing this message due to a known issue. Please view this item in Outlook desktop until it is fixed. </body> </html> "@ ##########Functions for generating actionable messages########## #Get calendar folder object Function Get-OutlookCalendarFolder { Param( [string]$emailAddress ) $olFolders = "Microsoft.Office.Interop.Outlook.OlDefaultFolders" -as [type] $namespace = $outlook.GetNameSpace("MAPI") $recipient = $namespace.CreateRecipient($emailAddress) #$namespace.getDefaultFolder($olFolders::olFolderCalendar) $namespace.GetSharedDefaultFolder($recipient,$olFolders::olFolderCalendar) } #Get Outlook calendar appointments for today that aren't meetings with onself (MeetingStatus = 0) Function Get-OutlookCalendarAppointmentsToday { Param( [string]$emailAddress ) $folder = Get-OutlookCalendarFolder -emailAddress $emailAddress #Calculate today as 24 hours starting at midnight of the current day $Start = (Get-Date -Hour 0 -Minute 00 -Second 00).ToShortDateString() + " 00:00" $End = (Get-Date).AddDays(+1).ToShortDateString() + " 00:00" #$filter = "[MessageClass]='IPM.Appointment' AND NOT [MeetingStatus]=0 AND (([Start] >= '$Start' AND [End] <= '$End') OR ([Start] <= '$Start' AND [End] >= '$Start') OR ([Start] <= '$end' AND [End] >= '$start'))" #$filter = "[MessageClass]='IPM.Appointment' AND NOT [MeetingStatus]=0 AND [Start] is Null AND (([Start] >= '$Start' AND [End] <= '$End') OR ([Start] <= '$Start' AND [End] >= '$Start') OR ([Start] <= '$end' AND [End] >= '$end'))" $filter = "[MessageClass]='IPM.Appointment' AND NOT [MeetingStatus]=0 AND [Start] is Null AND (([Start] >= '$Start' AND [End] <= '$End') OR ([Start] < '$Start' AND [End] > '$Start') OR ([Start] < '$end' AND [End] > '$end'))" Write-Debug -Message "Outlook calendar filter being used is $($filter)" $Appointments = $folder.Items $Appointments.IncludeRecurrences = $true $Appointments.Sort("[Start]") $Appointments.restrict($filter) Release-OutlookComObject -comObject $folder #Write-Debug -Message "Appointments found on calendar $($Appointments | Select-Object subject,start,end)" } #Get overlapping meetings in today's appointments ###BUGBUG I don't think it captures meetings that don't start and end today properly. Function Get-OverlappingMeetings { Param( $appointmentList ) $appointmentCount = $appointmentList.count #Iterate though each appointment and check for overlap with other appointments. http://wiki.c2.com/?TestIfDateRangesOverlap is the algorithm I used $appointmentListForActionableMessageList = @(@()) for($i=0; $i -lt $appointmentCount; $i++) { $appointmentListForActionableMessage = @() #put the first item in the overlapping array and add any overlapping items. If none are added and count remains one, I assume there's no overlap. $appointmentListForActionableMessage += $appointmentList[$i] #If this isn't the first appointment in the list, check if its start and end times match the previous one. If the do, skip any further comparison as the prevoius one captured all the overlaps. if(!(($i -gt 0) -and ($appointmentList[$i].start -eq $appointmentList[$i-1].start) -and ($appointmentList[$i].end -eq $appointmentList[$i-1].end))) { for($j=0; $j -lt $appointmentCount; $j++) { #Make sure you're not comparing the item to itself. if($appointmentList[$i] -ne $appointmentList[$j]) { if(($appointmentList[$i].start -lt $appointmentList[$j].end) -and ($appointmentList[$j].start -lt $appointmentList[$i].end)) { write-debug "$($appointmentList[$i].start)-$($appointmentList[$i].end) overlaps with $($appointmentList[$j].start)-$($appointmentList[$j].end)" $appointmentListForActionableMessage += $appointmentList[$j] } else { write-debug "$($appointmentList[$i].start)-$($appointmentList[$i].end) does not overlap with $($appointmentList[$j].start)-$($appointmentList[$j].end)" } } } #If there's only one item, it's the meeting I'm comparing to the others. If more, there were overlaps. if($appointmentListForActionableMessage.count -gt 1) { $appointmentListForActionableMessageList += ,@($appointmentListForActionableMessage) } } } ,$appointmentListForActionableMessageList } #Get overlapping meetings in today's appointments ###BUGBUG I don't think it captures meetings that don't start and end today properly. Function Get-RandomMeetingAndOverlaps { Param( $appointmentList ) #Select a random appointment $randomMeetingAndOverlaps = @() $randomMeetingAndOverlaps += $appointmentList | get-random Write-Debug "Meeting $($randomMeetingAndOverlaps.subject) selected to find overlaps" #Iterate though each appointment and check for overlap with other appointments. http://wiki.c2.com/?TestIfDateRangesOverlap is the algorithm I used foreach($appointment in $appointmentList) { #Make sure you're not comparing the item to itself. if($randomMeetingAndOverlaps[0]-ne $appointment) { if(($randomMeetingAndOverlaps[0].start -lt $appointment.end) -and ($appointment.start -lt $randomMeetingAndOverlaps[0].end)) { write-debug "$($randomMeetingAndOverlaps[0].start)-$($randomMeetingAndOverlaps[0].end) overlaps with $($appointment.start)-$($appointment.end)" $randomMeetingAndOverlaps += $appointment } else { write-debug "$($randomMeetingAndOverlaps[0].start)-$($randomMeetingAndOverlaps[0].end) does not overlap with $($appointment.start)-$($appointment.end)" } } } $randomMeetingAndOverlaps } #Get owner of a given item, which may be in another mailbox Function Get-OutlookItemOwner { param ( $OutlookAppointment ) #Get PR_Store_EntryID and parse out the owner name as desribed here: https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcdata/d34cabc3-b99f-4a6f-84d3-d0dc2cc2fcc3 $PR_Store_EntryID = $OutlookAppointment.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x0FFB0102") $PR_Store_EntryIDString = [System.Text.Encoding]::ASCII.GetString($PR_Store_EntryID) $emailAddress = $PR_Store_EntryIDString.Substring(60,$PR_Store_EntryIDString.IndexOf(0,60)-60) $nameSpace = $outlook.GetNamespace("MAPI") $recipient = $namespace.createrecipient($emailAddress) $null = $recipient.Resolve() $recipient.Name Release-OutlookComObject -comObject $nameSpace Release-OutlookComObject -comObject $recipient } #Generate Actionable Message Body from a list of appointments Function New-ActionableMessageBody { Param ( $jsonTemplate, $overlappingMeetingList, $HTMLHeader, $HTMLFooter ) #Create a unique ID for the message that will be included in all items so I can correlate them later $sessionID = (new-guid).Guid #Iterate through meetings and create a choice to appear in the Adaptive card and an object with the full item metadata $overlappingMeetingListforJson = @() $overlappingMeetingChoiceListforJson = @() foreach($overlappingMeeting in $overlappingMeetingList) { $overlappingMeetingChoiceListforJson += [pscustomobject]@{ Title = "$($overlappingMeeting.subject) ($($overlappingMeeting.Start.DayOfWeek) $($overlappingMeeting.Start.ToShortTimeString())-$($overlappingMeeting.end.ToShortTimeString()))" Value = $overlappingMeeting.GlobalAppointmentID } $overlappingMeetingListforJson += [pscustomobject]@{ SessionID = $sessionID Subject = $overlappingMeeting.Subject Start = $overlappingMeeting.Start End = $overlappingMeeting.End MailboxOwner = Get-OutlookItemOwner -OutlookAppointment $overlappingMeeting #$overlappingMeeting.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x4038001F") #$overlappingMeeting.session.CurrentUser.name Attachments = $overlappingMeeting.Attachments Categories = $overlappingMeeting.Categories Companies = $overlappingMeeting.Companies ConversationIndex = $overlappingMeeting.ConversationIndex CreationTime = $overlappingMeeting.CreationTime EntryID = $overlappingMeeting.EntryID Importance = $overlappingMeeting.Importance LastModificationTime = $overlappingMeeting.LastModificationTime MessageClass = $overlappingMeeting.MessageClass NoAging = $overlappingMeeting.NoAging Sensitivity = [Microsoft.Office.Interop.Outlook.OlSensitivity].GetEnumName($overlappingMeeting.Sensitivity) Size = $overlappingMeeting.Size UnRead = $overlappingMeeting.UnRead AllDayEvent = $overlappingMeeting.AllDayEvent BusyStatus = [Microsoft.Office.Interop.Outlook.OlBusyStatus].GetEnumName($overlappingMeeting.BusyStatus) Duration = $overlappingMeeting.Duration IsRecurring = $overlappingMeeting.IsRecurring Location = $overlappingMeeting.Location MeetingStatus = [Microsoft.Office.Interop.Outlook.OlMeetingStatus].GetEnumName($overlappingMeeting.MeetingStatus) NetMeetingAutoStart = $overlappingMeeting.NetMeetingAutoStart NetMeetingOrganizerAlias = $overlappingMeeting.NetMeetingOrganizerAlias NetMeetingServer = $overlappingMeeting.NetMeetingServer NetMeetingType = $overlappingMeeting.NetMeetingType OptionalAttendees = $overlappingMeeting.OptionalAttendees Organizer = $overlappingMeeting.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x5D01001F") Recipients = $overlappingMeeting.Recipients | foreach {"$($_.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x39FE001E"));"} RecurrenceState = [Microsoft.Office.Interop.Outlook.OlRecurrenceState ].GetEnumName($overlappingMeeting.RecurrenceState) ReminderMinutesBeforeStart = $overlappingMeeting.ReminderMinutesBeforeStart ReminderOverrideDefault = $overlappingMeeting.ReminderOverrideDefault ReminderPlaySound = $overlappingMeeting.ReminderPlaySound ReminderSet = $overlappingMeeting.ReminderSet ReminderSoundFile = $overlappingMeeting.ReminderSoundFile ReplyTime = $overlappingMeeting.ReplyTime RequiredAttendees = $overlappingMeeting.RequiredAttendees Resources = $overlappingMeeting.Resources ResponseRequested = $overlappingMeeting.ResponseRequested ResponseStatus = [Microsoft.Office.Interop.Outlook.OlResponseStatus].GetEnumName($overlappingMeeting.ResponseStatus) Links = $overlappingMeeting.Links GlobalAppointmentID = $overlappingMeeting.GlobalAppointmentID StartUTC = $overlappingMeeting.StartUTC EndUTC = $overlappingMeeting.EndUTC StartInStartTimeZone = $overlappingMeeting.StartInStartTimeZone EndInEndTimeZone = $overlappingMeeting.EndInEndTimeZone StartTimeZone = $overlappingMeeting.StartTimeZone.id EndTimeZone = $overlappingMeeting.EndTimeZone.id ConversationID = $overlappingMeeting.ConversationID DoNotForwardMeeting = $overlappingMeeting.DoNotForwardMeeting FOthersAppt = $overlappingMeeting.FOthersAppt IsOnlineMeeting = try { $overlappingMeeting.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/string/{00020329-0000-0000-C000-000000000046}/SkypeTeamsMeetingUrl") | Out-Null $true } catch { $false } } Release-OutlookComObject -comObject $overlappingMeeting } #Add an option for not attending any of the meetings $overlappingMeetingChoiceListforJson += [pscustomobject]@{ Title = "None of these" Value = "No conflicting meeting attended" } #add choices to JSON $json = Convertfrom-Json -InputObject $jsonTemplate -Verbose $json.body[2].items[1].columns[1].items[0].choices = $overlappingMeetingChoiceListforJson #Add meeting list with full details to JSON $HTTPPOSTBody = @() $HTTPPOSTBody += [pscustomobject]@{ attendedItemID = "{{meetingList.value}}" } $HTTPPOSTBody += ,@($overlappingMeetingListforJson) $json.body[2].items[2].columns[0].items[0].actions[0].body = $HTTPPOSTBody | ConvertTo-Json $HTMLBody = $HTMLheader + ($json | ConvertTo-Json -Depth 100) + $HTMLFooter $HTMLBody } #Create new actionable message and send it Function Send-ActionableMessage { Param ( $jsonTemplate, $overlappingMeetingList, $HTMLHeader, $HTMLFooter, [string]$senderEmail, [string]$recipientEmail ) #$outlook = new-object -comobject outlook.application Write-Debug "Creating actionable message" $Mail = $Outlook.CreateItem(0) #$mail.to = $outlook.Session.CurrentUser.address $mail.to = $recipientEmail Write-Debug "Recipient $($Mail.To)" $mail.SentOnBehalfOfName = $senderEmail Write-Debug "Sender $($mail.SentOnBehalfOfName)" $Mail.Subject = "Could you please answer one question to make meetings better at Microsoft?" Write-Debug "Subject $($Mail.subject)" $Mail.HTMLBody = New-ActionableMessageBody -jsonTemplate $jsonTemplate -overlappingMeetingList $overlappingMeetingList -HTMLHeader $HTMLHeader -HTMLFooter $HTMLFooter Write-Debug "Sending actionable message" try { $mail.send() } catch { Write-Debug -Message "Sending message failed with $($_.exception.message)" } Release-OutlookComObject -comObject $Mail } #Generate a new actionable message Function New-ActionableMessage { Param( [string]$senderEmailAddress, [string]$recipientEmailAddress ) #$appointmentListForActionableMessageList = @(@()) $AppointmentList = @() $AppointmentList += Get-OutlookCalendarAppointmentsToday -emailAddress $recipientEmailAddress #$appointmentListForActionableMessage = Get-RandomMeetingAndOverlaps -appointmentList $AppointmentList #$appointmentListForActionableMessage = $AppointmentList if($AppointmentList.count -eq 0) { Write-Debug "There are no meetings, not sending an actionable message" } else { Write-Debug "Selected the following meetings for Actionable Message:" foreach ($overlappingMeeting in $appointmentList) { Write-Debug "$($overlappingMeeting.subject), $($overlappingMeeting.globalappointmentid)" } Send-ActionableMessage -jsonTemplate $jsonTemplate -overlappingMeetingList ($AppointmentList | Get-Random -Count $appointmentList.count) -HTMLHeader $HTMLHeader -HTMLFooter $HTMLFooter -senderEmail $senderEmailAddress -recipientEmail $recipientEmailAddress } <#$appointmentListForActionableMessageList += Get-OverlappingMeetings -appointmentList $AppointmentList if($appointmentListForActionableMessageList.count -eq 0) { Write-Debug "There are no overlapping meetings, not sending an actionable message" } else { if($appointmentListForActionableMessageList.count -eq 1) { Write-Debug "One overlapping meeting found, sending actionable message" $appointmentListForActionableMessage = $appointmentListForActionableMessageList[0] } else { Write-Debug "Multiple overlapping meetings found, choosing one at random and sending actionable message" $appointmentListForActionableMessage = $appointmentListForActionableMessageList[(get-random -Minimum 0 -Maximum $appointmentListForActionableMessageList.count)] } Write-Debug "Selected the following meetings for Actionable Message:" foreach ($overlappingMeeting in $appointmentListForActionableMessage) { Write-Debug "$($overlappingMeeting.subject), $($overlappingMeeting.globalappointmentid)" } Send-ActionableMessage -jsonTemplate $jsonTemplate -overlappingMeetingList $appointmentListForActionableMessage -HTMLHeader $HTMLHeader -HTMLFooter $HTMLFooter } #> } #Check time of day and see if it's the scheduled time or after. If so, send the message. Function Send-ActionableMessageOnSchedule { $calendarFolder = Get-OutlookCalendarFolder -emailAddress $outlook.Session.CurrentUser.address try { $nextActionableMessageSendDate = get-date -Date $calendarFolder.description } catch { $nextActionableMessageSendDate = get-date } $actionableMessageSendTime = "4:00 PM" $currentDate = get-date Write-Debug "Next actionable message will be sent on date $nextActionableMessageSendDate and messages are configured to be sent at $actionableMessageSendTime" if(($currentDate -ge $nextActionableMessageSendDate) -and ($currentDate -ge (get-date $actionableMessageSendTime))) { Write-Debug "Current date and time is $currentDate, sending actionable message" New-ActionableMessage -senderEmailAddress $outlook.Session.CurrentUser.address -recipientEmailAddress $outlook.Session.CurrentUser.address $calendarFolder.description= $currentDate.AddDays(2).ToShortDateString() } else { Write-Debug "Current date and time is $currentDate, will not send actionable message" } Release-OutlookComObject -comObject $calendarFolder } ##########Types and objects for Outlook Monitor########### #Add types to get which window is in focus to detect if Outlook is in use Add-Type @" using System; using System.Runtime.InteropServices; public class UserWindows { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId); } "@ #Add types to hide the console window Add-Type -Name Window -Namespace Console -MemberDefinition ' [DllImport("Kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow); ' #Add type to get machine idle time to determine if user walked away but left Outlook open Add-Type @' using System; using System.Diagnostics; using System.Runtime.InteropServices; namespace PInvoke.Win32 { public static class UserInput { [DllImport("user32.dll", SetLastError=false)] private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); [StructLayout(LayoutKind.Sequential)] private struct LASTINPUTINFO { public uint cbSize; public int dwTime; } public static DateTime LastInput { get { DateTime bootTime = DateTime.UtcNow.AddMilliseconds(-Environment.TickCount); DateTime lastInput = bootTime.AddMilliseconds(LastInputTicks); return lastInput; } } public static TimeSpan IdleTime { get { return DateTime.UtcNow.Subtract(LastInput); } } public static int LastInputTicks { get { LASTINPUTINFO lii = new LASTINPUTINFO(); lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO)); GetLastInputInfo(ref lii); return lii.dwTime; } } } } '@ #Create Outlook state object that will maintain the Outlook status $Script:OutlookState = [pscustomobject]@{ #Item info ItemSubject = $null ItemToRecipientList = $null ItemCCRecipientList = $null ItemBCCRecipientList = $null ItemSender = $null ItemType = $null ItemBodyLength = 0 ItemImportance = $null ItemSize = 0 ItemCreationTime = $null ItemFolderName = $null ItemVisibleAttachmentCount = 0 ItemEmbeddedAttachmentCount = 0 ItemMessageID = $null #Machine user UserName = $env:USERNAME #Outlook state info DateTimeUTC = $null OutlookInFocus = $false UserIsReading = $false UserIsEditing = $false } ##########Functions concerning getting machine and application idle/in use state########## #Monitors Outlook state Function Get-OutlookUsageState { $timer.Stop() if(!$Outlook) { Write-Debug "There's no Outlook COM object, checking if Outlook is running and retrieving Outlook COM object if so." $script:outlook = Get-OutlookComObject } $script:OutlookState.DateTimeUTC = (get-date).ToUniversalTime() #Check if Outlook is the window in focus and the machine is not idle to determine if Outlook is in use if((Get-OutlookFocusState) -and !(Get-MachineIdleState -timeLimit 120)) { $OutlookState.OutlookInFocus = $true #Get the active Outlook window (assuming non-active windows likely aren't in use)" try { $activeWindow = $outlook.ActiveWindow() } catch [System.Management.Automation.MethodInvocationException] { #If Outlook is closed and reopened, get a new com object Write-Debug "Caught an exception using an Outlook COM object, will get a new one." $script:outlook = new-object -comobject outlook.application $activeWindow = $outlook.ActiveWindow() } #Retreive info about the open window and any items within it switch ($activeWindow.class) { 34 {Get-OutlookExplorerState -explorer $activeWindow} 35 {Get-OutlookInspectorState -inspector $activeWindow} } #Send the log file based on the schedule Send-LogFileOnSchedule #Send the actionable message based on the schedule Send-ActionableMessageOnSchedule Release-OutlookComObject($activeWindow) } else { #Outlook is not in use $OutlookState.OutlookInFocus = $false $OutlookState.UserIsEditing = $false $OutlookState.UserIsReading = $false Clear-OutlookItemInfo } #Output current state to a CSV file $OutlookState | Export-Csv $logFilePath -Append -Force Set-ItemProperty -Path $logFilePath -Name IsReadOnly -Value $true Update-OutlookMonitorGUI -OutlookState $OutlookState $timer.Start() } #Check if the Outlook Window is in focus on the machine, get the foreground window and map that to process name to confirm it's Outlook. Function Get-OutlookFocusState { $processID = 0 try { $ActiveHandle = [UserWindows]::GetForegroundWindow() $null = [UserWindows]::GetWindowThreadProcessId($ActiveHandle,[ref]$processID) if((Get-Process -id $processID).name -eq "Outlook") { $true } else { $false } } catch { Write-Error "Failed to get active Window details. More Info: $_" } } #Check if machine is idle Function Get-MachineIdleState { Param( [int]$timeLimit ) $idleTime = [PInvoke.Win32.UserInput]::IdleTime.TotalSeconds if($idleTime -gt $timeLimit) { $true } else { $false } } ##########Functions concerning getting Outlook folder and item info########## #Get recipient Info Function Get-OutlookItemRecipientInto { Param( $outlookItemRecipientList ) #####BUGBUG##### it counts a DG as one recipient, can we fix that? Not for external DGs, but maybe internal foreach($outlookItemRecipient in $OutlookItemRecipientList) { if($outlookItemRecipient.type -eq [Microsoft.Office.Interop.Outlook.OlMailRecipientType]::Olto) { $OutlookState.ItemToRecipientList += "$($outlookItemRecipient.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x39FE001E"));" } if($outlookItemRecipient.type -eq [Microsoft.Office.Interop.Outlook.OlMailRecipientType]::OlCC) { $OutlookState.ItemCCRecipientList += "$($outlookItemRecipient.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x39FE001E"));" } if($outlookItemRecipient.type -eq [Microsoft.Office.Interop.Outlook.OlMailRecipientType]::OlBCC) { $OutlookState.ItemBCCRecipientList += "$($outlookItemRecipient.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x39FE001E"));" } Release-OutlookComObject($outlookItemRecipient) } } #Get metadata from an Outlook item Function Get-OutlookItemInfo { Param ( $outlookItem ) #If the message ID hasn't changed, the same item is in focus, no need to retreive the data again. If it's "saved", it has been modified since last save so capture that. write-debug "Checking to see if item has changed by comparing messageid of current and previous item and checking for saved state." Write-Debug "Message ID at time of last check: $($OutlookState.ItemMessageID)" Write-Debug "Message ID at current check: $($outlookitem.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x1035001F"))" Write-Debug "Message has not been changed since last save: $($outlookItem.saved)" if($OutlookState.ItemMessageID -ne $outlookitem.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x1035001F") -or !$outlookItem.saved) { Write-Debug "Message has been determined to need to be checked again, either because it's a new message ID or it's changed since last save" Clear-OutlookItemInfo $OutlookState.ItemMessageID = $outlookitem.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x1035001F") #Get recipient info $recipients = $outlookItem.recipients Get-OutlookItemRecipientInto -outlookItemRecipientList $recipients Release-OutlookComObject($recipients) #Fill out remaining message properties $OutlookState.ItemSender = $outlookItem.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x5D01001F") $OutlookState.ItemBodyLength = $outlookItem.body.length ###BUGBUG#### see if you can get the length of the most recent reply $OutlookState.ItemFolderName = $outlookItem.parent.FullFolderPath $OutlookState.ItemImportance = [Microsoft.Office.Interop.Outlook.OlImportance]$outlookItem.importance $OutlookState.ItemcreationTime = $outlookItem.creationTime $OutlookState.ItemSize = $outlookItem.size $OutlookState.ItemSubject = $OutlookItem.Subject $OutlookState.ItemType = [Microsoft.Office.Interop.Outlook.OlObjectClass]$outlookItem.class Get-OutlookAttachmentInfo -item $outlookItem } else { Write-Debug "Message has been determined to not need to be checked again, message ID is the same or it's an item being composed that hasn't been changed since last save" } } #Gets counts of attachment on an item Function Get-OutlookAttachmentInfo { Param( $item ) #Loop through each attachmetn and look at various flags to determine if it's a visible attachment or embedded foreach($attachment in $item.attachments) { $attachFlags = $attachment.PropertyAccessor.GetProperty('http://schemas.microsoft.com/mapi/proptag/0x37140003') $attachMethod = $attachment.PropertyAccessor.GetProperty('http://schemas.microsoft.com/mapi/proptag/0x37050003') $attachContentID = $attachment.PropertyAccessor.GetProperty('http://schemas.microsoft.com/mapi/proptag/0x3712001F') $attachContentLocation = $attachment.PropertyAccessor.GetProperty('http://schemas.microsoft.com/mapi/proptag/0x3713001F') Write-Debug "Attachment name: $($attachment.filename)" write-debug "PR_ATTACH_CONTENT_ID: $attachContentID" write-debug "PR_ATTACH_CONTENT_LOCATION: $attachContentLocation" write-debug "attach flags: $attachFlags" write-debug "attach method: $attachmethod" write-debug "attach type: $($attachment.type)" #Cloudy attachment: $attachContentID -and -not ($attachFlags -eq 4 -and $attachMethod -eq 7) #Other two checks from http://social.msdn.microsoft.com/Forums/en-SG/outlookdev/thread/4e005509-56de-47c0-99cb-b5ac5a972789 if(($attachContentID -ne "" -and $outlookItem.HTMLBody.Contains($attachContentID)) -and !($attachFlags -eq 4 -and $attachMethod -eq 7) -or ($attachMethod -eq 6)) { $outlookState.ItemEmbeddedAttachmentCount++ Write-Debug "Attachment determined to be embedded" } else { $outlookState.ItemVisibleAttachmentCount++ Write-Debug "Attachment determined to be visible" } Release-OutlookComObject($attachment) } } #If no Outlook item is selected, remove any item info Function Clear-OutlookItemInfo { $OutlookState.ItemBCCRecipientList = $null $OutlookState.ItemBodyLength = $null $OutlookState.ItemCCRecipientList = $null $Outlookstate.ItemSender = $null $OutlookState.ItemFolderName = $null $OutlookState.ItemImportance = $null $OutlookState.ItemcreationTime = $null $OutlookState.ItemSize = $null $OutlookState.ItemSubject = $null $OutlookState.ItemToRecipientList = $null $OutlookState.ItemType = $null $OutlookState.ItemEmbeddedAttachmentCount = 0 $OutlookState.ItemVisibleAttachmentCount = 0 $OutlookState.ItemMessageID = $null } #Get info on the explorer window Function Get-OutlookExplorerState { Param( $explorer ) #If it's an explorer and the folder has items, assume user is reading. Note that we can't check which item is selected since this is broken for group folders, it always shows nothign selected write-debug "Checking if the user is reading, composing, or doing nothing in the current explorer" write-debug "Item count of current folder is $($explorer.CurrentFolder.items.count) and number of items selected is $($explorer.CurrentFolder.items.count)" if(($explorer.CurrentFolder.items.count -eq 0) -and ($explorer.selection.Count -eq 0)) { write-debug "User has been determined to not be reading or editing." $OutlookState.UserIsReading = $false $OutlookState.UserIsEditing = $false Clear-OutlookItemInfo } else { #An item is selected so the user is either reading or editing. if($response = $explorer.activeinlineresponse) { write-debug "User has been determined to be editing due to an active inline response." $OutlookState.UserIsEditing = $True $OutlookState.UserIsReading = $false Get-OutlookItemInfo -outlookItem $response Release-OutlookComObject($response) } else { write-debug "User has been determined to be reading since there's no active inline response." $OutlookState.UserIsEditing = $false $OutlookState.UserIsReading = $true #If an item is selected, get its info. If not, it may be a group folder with no item. if($explorer.selection.count -gt 0) { $currentItem = $explorer.selection.item(1) Get-OutlookItemInfo -outlookItem $currentItem Release-OutlookComObject($currentItem) } else { Clear-OutlookItemInfo $OutlookState.ItemFolderName = $explorer.currentFolder.fullFolderPath } } } } #Get info on an inspector window Function Get-OutlookInspectorState { Param( $inspector ) #if an item isn't listed as "sent", it's being edited if($inspector.currentitem.sent) { $OutlookState.UserIsEditing = $false $OutlookState.UserIsReading = $true } else { $OutlookState.UserIsEditing = $True $OutlookState.UserIsReading = $false } $currentItem = $inspector.CurrentItem Get-OutlookItemInfo -outlookItem $currentItem Release-OutlookComObject($currentItem) } ##########Functions to create and update Forms GUI########## #Create Outlook monitor GUI window Function New-OutlookMonitorGUI { Add-Type -AssemblyName System.Windows.Forms [System.Windows.Forms.Application]::EnableVisualStyles() $Form = New-Object system.Windows.Forms.Form $Form.ClientSize = '475,871' $Form.text = "Outlook Monitor" $Form.TopMost = $false $UserIsReading = New-Object system.Windows.Forms.Label $UserIsReading.text = "User is reading" $UserIsReading.AutoSize = $true $UserIsReading.width = 25 $UserIsReading.height = 10 $UserIsReading.location = New-Object System.Drawing.Point(28,61) $UserIsReading.Font = 'Microsoft Sans Serif,16' $UserIsEditing = New-Object system.Windows.Forms.Label $UserIsEditing.text = "User is editing" $UserIsEditing.AutoSize = $true $UserIsEditing.width = 25 $UserIsEditing.height = 10 $UserIsEditing.location = New-Object System.Drawing.Point(28,110) $UserIsEditing.Font = 'Microsoft Sans Serif,16' $ItemInfoDataGridView = New-Object system.Windows.Forms.DataGridView $ItemInfoDataGridView.width = 416 $ItemInfoDataGridView.height = 594 $ItemInfoDataGridView.location = New-Object System.Drawing.Point(25,165) $OutlookInFocus = New-Object system.Windows.Forms.Label $OutlookInFocus.text = "Outlook in focus" $OutlookInFocus.AutoSize = $true $OutlookInFocus.width = 25 $OutlookInFocus.height = 10 $OutlookInFocus.location = New-Object System.Drawing.Point(28,13) $OutlookInFocus.Font = 'Microsoft Sans Serif,16' $LogFileLabel = New-Object system.Windows.Forms.Label $LogFileLabel.text = "Log file path:" $LogFileLabel.AutoSize = $false $LogFileLabel.width = 403 $LogFileLabel.height = 87 $LogFileLabel.location = New-Object System.Drawing.Point(33,770) $LogFileLabel.Font = 'Microsoft Sans Serif,10' $Form.controls.AddRange(@($UserIsReading,$UserIsEditing,$ItemInfoDataGridView,$OutlookInFocus,$LogFileLabel)) $Form.Add_FormClosing({ Close-Form -sender $this -cancelEventArgs $_ }) $Form.MaximizeBox = $false $form.showintaskbar = $false $Form.Add_Shown({New-OutlookMonitor -checkIntervalMilliseconds 500 -sendLogFileIntervalMinutes 2880}) $form.windowstate = [System.Windows.Forms.FormWindowState]::Minimized $LogFileLabel.text = "Outlook Monitoring log is stored here: $logFilePath" #Configure datagridview to show item properties $ItemInfoDataGridView.RowHeadersVisible = $false $ItemInfoDataGridView.AutoSizeRowsMode = [System.Windows.Forms.DataGridViewAutoSizeColumnMode]::AllCells $ItemInfoDataGridView.ColumnCount = 2 $ItemInfoDataGridView.ColumnHeadersVisible = $true $ItemInfoDataGridView.ColumnHeadersHeightSizeMode = 2 $ItemInfoDataGridView.Columns[0].Name = "Property" $ItemInfoDataGridView.Columns[0].AutoSizeMode = 6 $ItemInfoDataGridView.Columns[1].DefaultCellStyle.WrapMode = [System.Windows.Forms.DataGridViewTriState]::True $ItemInfoDataGridView.Columns[1].Name = "Value" $ItemInfoDataGridView.Columns[1].AutoSizeMode = 16 if(!$doNotHideConsole) { Hide-PowerShellConsole } Initialize-SystemTrayIcon [void]$Form.ShowDialog() } #Hide powershell console as the GUI runs Function Hide-PowerShellConsole { # Hide PowerShell Console $consolePtr = [Console.Window]::GetConsoleWindow() $null = [Console.Window]::ShowWindow($consolePtr, 0) } #If the form is closed, minimize to system tray and keep running Function Close-Form { Param ( [Object]$sender, [Object]$cancelEventArgs ) #If user clicks the X to minimize, keep it running, if closed from context menu then really close. if($sender -eq $form) { $systemTrayIcon.BalloonTipText = "Outlook Monitor will continue runnning in your system tray, right click to close" $systemTrayIcon.ShowBalloonTip(1000) $form.WindowState = "minimized" $cancelEventArgs.cancel = $true } else { #Dispose of all variables Get-Variable -exclude Runspace | Where-Object { $_.Value -is [System.IDisposable] } | Foreach-Object { try { $_.Value.Dispose() Remove-Variable $_.Name -ErrorAction Stop } catch { } } } } #Create the sytem tray icon Function Initialize-SystemTrayIcon { #Configure the icon $script:systemTrayIcon = New-Object System.Windows.Forms.NotifyIcon $systemTrayIcon.Icon = "$($env:windir)\syswow64\OneDrive.ico" $systemTrayIcon.BalloonTipText = "Outlook Monitor $((Get-InstalledScript "OutlookMonitor").version) is running in your system tray" $systemTrayIcon.Visible = $True $systemTrayIcon.ShowBalloonTip(1000) #enable it pop up the form on click $systemTrayIcon.add_click({$form.WindowState = "Normal"; $form.Activate()}) #Create context menu with option to exit the form $contextMenu = New-Object System.Windows.Forms.ContextMenu $exitMenuItem = New-Object System.Windows.Forms.MenuItem $exitMenuItem.text = "Exit" $exitMenuItem.add_Click({Close-Form -sender $this -cancelEventArgs $_}) [void]$contextMenu.MenuItems.Add($exitMenuItem) $systemTrayIcon.contextMenu = $contextMenu } #Run the process that gets the Outlook state and updates the GUI using a timer Function New-OutlookMonitor { Param( [int]$checkIntervalMilliseconds, [int]$sendLogFileIntervalMinutes ) #Set up timer to check usage state $script:timer = New-Object System.Windows.Forms.Timer $timer.Interval = $checkIntervalMilliseconds $timer.Add_Tick({Get-OutlookUsageState}) $timer.Enabled = $True #Set up stopwatch to send log files on regular intervals $script:sendLogFileTimeSpan = new-timespan -minutes $sendLogFileIntervalMinutes $script:stopwatch = [diagnostics.stopwatch]::StartNew() Write-Debug "Setting up stopwatch to send log files every $sendLogFileIntervalMinutes minutes" } #Update the GUI Function Update-OutlookMonitorGUI { Param( $OutlookState ) if($OutlookState.OutlookInFocus) { $OutlookInFocus.BackColor = "#00FF00" if($OutlookState.UserIsReading) { $UserIsReading.BackColor = "#00FF00" } else { $UserIsReading.BackColor = "#d0021b" } if($OutlookState.UserIsEditing) { $UserIsEditing.BackColor = "#00FF00" } else { $UserIsEditing.BackColor = "#d0021b" } #Fill the datagrid with the item info $ItemInfoDataGridView.rows.clear() if(!$OutlookState.UserIsEditing -and !$OutlookState.UserIsReading) { $ItemInfoDataGridView.rows.Add("No item selected") } else { foreach($property in $OutlookState.psobject.Properties) { $ItemInfoDataGridView.Rows.Add($property.name,$property.value) } } } else { $OutlookInFocus.BackColor = "#d0021b" $UserIsReading.BackColor = "#d0021b" $UserIsEditing.BackColor = "#d0021b" $ItemInfoDataGridView.rows.clear() $ItemInfoDataGridView.rows.Add("No item selected") } $form.Refresh() } ##########Functions concerning manipulating Outlook COM objects######## #Checks if Outlook is running and returns a COM object if so, otherwise null. Function Get-OutlookComObject { if(Get-Process -Name OUTLOOK -ErrorAction SilentlyContinue) { Write-Debug "Retrieving Outlook COM object" new-object -comobject outlook.application } else { $null } } #Release Outlook COM object Function Release-OutlookComObject { Param ( [object]$comObject ) try { $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($comObject) } catch { Write-Debug "Encountered exception releasing COM object $($_.exception.message). This can happen if Outlook is closed while the monitor is running." } } #########Functions involving creating and sending the log file############ #set up log file Function Initialize-Log { $folderPath = "$($env:LOCALAPPDATA)\OutlookMonitorLogs" if(!(Test-Path $folderPath)) { $null = New-Item $folderPath -ItemType Directory } "$($folderPath)$($MyInvocation.ScriptName.Replace((Split-Path $MyInvocation.ScriptName),'').TrimStart(''))_$($env:computername)_$($env:username).csv" } #Check if a log file needs to be sent on a regular schedule Function Send-LogFileOnSchedule { Write-Debug "Log file configured to be sent every $($sendLogFileTimeSpan.TotalMinutes) minutes, $($stopwatch.Elapsed.TotalMinutes) minutes have passed since last send" if($stopwatch.elapsed -gt $sendLogFileTimeSpan) { Write-Debug "Sending log file" Send-LogFile -emailAddress 'outlookmonitor@service.microsoft.com' -logFilePath $logFilePath $stopwatch.Restart() Update-OutlookMonitor } else { Write-Debug "Not sending log file" } } #Email the log file to a modern group Function Send-LogFile { Param ( [string]$emailAddress, [string]$logFilePath ) Write-Debug "Compressing log file $logFilePath in preparation to send" $compressedLogFilePath = "$($logFilePath).zip" Compress-Archive -LiteralPath $logFilePath -Update -DestinationPath $compressedLogFilePath if((Get-Item $compressedLogFilePath).Length -gt 104857600) { Write-Debug "Compressed file is > 100MB, log will not be sent" } else { write-debug "Creating message to send log file" try { $message = $outlook.CreateItem([Microsoft.Office.Interop.Outlook.OlItemType]::olMailItem) } catch { Write-Debug "Couldn't create message to send log file due to exception $($_.exception.mesage). This may occur because Outlook is closed." $message = $null } if($message) { $message.To = $emailAddress $message.Subject = "$($MyInvocation.ScriptName.Replace((Split-Path $MyInvocation.ScriptName),'').TrimStart(''))_$($env:computername)_$($env:USERNAME)" $message.Attachments.Add($compressedLogFilePath) Write-Debug "Sending log file to $emailAddress" $null = $message.send() Release-OutlookComObject($message) } else { Write-Debug "Message object does not exist, log file will not be sent. It will try again next time" } } } #########Functions involving installation, uninstallation, and updating############ #Copies script, and creates scheduled task, then runs it Function Install-OutlookMonitor { Write-Debug "Installing Nuget package provider" Install-PackageProvider Nuget -Force Write-Debug "Installing PowerShellGet" Install-Module -Name PowerShellGet -Force Write-Debug "Installing script" Install-script "OutlookMonitor" -Force -Scope CurrentUser $scriptFilePath = "$((Get-InstalledScript "OutlookMonitor").installedlocation)\outlookmonitor.ps1" Write-Debug "Creating scheduled task" Add-OutlookMonitorScheduledTask -filePath $scriptFilePath Write-Debug "Starting scheduled task" Start-ScheduledTask -TaskName "Outlook Monitor" } #Create Outlook Monitor scheduled tasks Function Add-OutlookMonitorScheduledTask { Param( [string]$filePath ) if(Get-ScheduledTask "Outlook Monitor" -ErrorAction SilentlyContinue) { Write-Debug "Outlook Monitor scheduled task exists, not creating it again" } else { Write-Debug "Outlook Monitor scheduled task does not exist, creating it" $argument = "-executionpolicy bypass -file `"$filePath`"" $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $argument $trigger = New-ScheduledTaskTrigger -AtLogOn #$trigger.delay = "PT20S" $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -MultipleInstances IgnoreNew Register-ScheduledTask -Action $action -Trigger $trigger -TaskName "Outlook Monitor" -Description "Runs Outlook activity monitor" -Settings $settings #-User "System" #-CimSession $CIMSession -RunLevel Highest } } #Uninstall Outlook Monitor Function Uninstall-OutlookMonitor { if(Get-ScheduledTask "Outlook Monitor" -ErrorAction SilentlyContinue) { Write-Debug "Removing Outlook monitor task" Stop-ScheduledTask -taskname "Outlook Monitor" Unregister-ScheduledTask "Outlook Monitor" -Confirm:$false } Write-Debug "Removing Outlook Monitoring Log Folder" Remove-Item "$($env:LOCALAPPDATA)\OutlookMonitorLogs" -Force -Confirm:$false -Recurse uninstall-script "OutlookMonitor" -Force } #Update script file and restart script Function Update-OutlookMonitor { $onlineScriptVersion = (Find-Script "OutlookMonitor").version $currentScriptVersion = (Get-InstalledScript "OutlookMonitor").version Write-Debug "Checking for script update, current version is $currentScriptVersion" if($onlineScriptVersion -gt $currentScriptVersion) { Write-Debug "Script update found, online version is $onlineScriptVersion, installing update" Update-Script -Name "OutlookMonitor" -Force Write-Debug "Restarting with new script version and exiting old one" start-process "PowerShell.exe" -argumentList "-WindowStyle hidden -command Start-ScheduledTask -TaskName `'Outlook Monitor`'" Close-Form } else { write-debug "No script update found, online version is $onlineScriptVersion" } } } Process { ########Script Body######## $logFilePath = Initialize-Log if($install) { Write-Debug "Install parameter used, will create scheduled task" Install-OutlookMonitor } elseif($uninstall) { Write-Debug "Uninstall parameter used, will remove scheduled task" Uninstall-OutlookMonitor } elseif ($sendCalendarActionableMessage) { Write-Debug "sendCalendarActionableMessage parameter used, will send message to specified email address and close" $script:Outlook = Get-OutlookComObject New-ActionableMessage -senderEmailAddress $calendarActionableMessageSender -recipientEmailAddress $calendarActionableMessageRecipient } else { #initialize the GUI $script:Outlook = Get-OutlookComObject New-OutlookMonitorGUI } } ##TODO #maybe make sure no other version of the same script is running. <# #Check if a recipient is a DG Function Get-OutlookItemRecipientIsDG { Param( $outlookItemRecipient ) $GAL = $outlook.Session.AddressLists("All Distribution Lists") $outlookItemRecipientAddressEntryFromGAL = $gal.AddressEntries($outlookItemRecipient.name) if($outlookItemRecipient.address -eq $outlookItemRecipientAddressEntryFromGAL.address) { $outlookItemRecipientAddressEntryFromGAL } else { $null } } #> <# #Count number of recipients, 1 for regular user and more for a DG Function Get-OutlookItemRecipientCount { Param( $outlookItemRecipient ) #Doesn't work for all microsoft DG, also doesn't work for Nimrod since there are 2 GAL entreis with same name... #Search for the recipient by name in the "All Distribution lists" address list then check to see if the search result address matches your recipient #If not, it found the closest user name, which means there was no exact match for your recipient and it's in the list of DGs. if($outlookItemRecipientAddressEntryFromGAL = Get-OutlookItemRecipientIsDG -outlookItemRecipient $outlookItemRecipient) { write-host "Recipient name: $($outlookItemRecipient.name) is a DG and PR_display_type is $($outlookitemrecipient.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x39050003"))"#> <#foreach($outlookItemRecipientAddressEntryFromGALL2 in $outlookItemRecipientAddressEntryFromGAL.members) { if($outlookItemRecipientAddressEntryFromGALL3 = Get-OutlookItemRecipientIsDG -outlookItemRecipient $outlookItemRecipientAddressEntryFromGALL2) { write-host "Recipient name: $($outlookItemRecipientAddressEntryFromGALL2.name) is a DG" } else { write-host "Recipient name: $($outlookItemRecipientAddressEntryFromGALL2.name) is not a DG " $memberCount++ } }#> <# $memberCount } else { write-host "Recipient name: $($outlookItemRecipient.name) is a not DG and PR_display_type is $($outlookitemrecipient.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x39050003"))" 1 } } #> |