Slow Output in Outlook via Powershell - powershell

I'm using Powershell to access Outlook Mails by creating COM Object.
When I search for particular Mail . PowerShell Iterates through all the mails due to which my output result is really slow and taking very long time.
I have already tried using Descending paramater in Sort-Object or filtering out by date but still results are slow.
$outlook = New-Object -comobject outlook.application
$inbox = $outlook.GetNamespace("MAPI")
$find = $inbox.GetDefaultFolder(6)
$find.Items | Where-Object{$_.SentOn -gt '27-Oct-2019 12:00 PM'}| Select-
Object -Property Subject,SentOn
Can someone please help me to generate faster results or provide a way to filter my search for particular time period.

Never loop through all items in your code. After all, you wouldn't write a SELECT query in SQL without a WHERE clause, would you?
Use Items.Find/FindNext or Items.Restrict to let the store provider do the job.

From the comment from #bluuf I looked into the EWS (exchange web service) and came up with this solution and its about 50% faster than your script.. Maybe it helps for you
$startDate = Get-Date
$MailboxSMTP = "peter.parker#home.com"
$dllpath = "C:\Program Files (x86)\Microsoft SQL Server Management Studio 18\Common7\IDE\Mashup\Microsoft.Exchange.WebServices.dll"
[void][Reflection.Assembly]::LoadFile($dllpath)
$service = new-object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)
$service.UseDefaultCredentials = $true
$service.AutodiscoverURL($mailboxSMTP)
$mbMailbox = new-object Microsoft.Exchange.WebServices.Data.Mailbox($mailboxSMTP)
$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)
write-host "Number or unread Messages : " $inbox.UnreadCount
$emails = $inbox.FindItems(10000) | Where-Object { $_.DateTimeSent -gt '27-Oct-2019 12:00 PM'}
$endDate = Get-Date
New-TimeSpan -Start $startDate -End $endDate
Found the script here https://www.msxfaq.de/code/testews.htm
EDIT:
You can filter the results with EWS in a similar way as in #Dmitry solution
write-host "Number or unread Messages : " $inbox.UnreadCount
$view = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1)
$x = $inbox.FindItems($(New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsGreaterThanOrEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::DateTimeReceived, '2019-10-27')),$view)
Or you can use it to filter for different attributes
$filter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::IsRead, $false)
Or
$filter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsLessThanOrEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived,'2019-10-10')
$x = $inbox.FindItems($filter,$view)

Related

Outlook Powershell Script

I have been doing some digging and trying to find a way to remove a PST from Outlook with a script. I don't entirely know how to script but am trying to learn. I found this old Stackoverflow but I am unsure how to actually enter the information I need into it.
How to disconnect PST file from Outlook using Powershell?
The script in question is below.
$Outlook = new-object -com outlook.application
$Namespace = $Outlook.getNamespace("MAPI")
$PSTtoDelete = "c:\test\pst.pst"
$PST = $namespace.Stores | ? {$_.FilePath -eq $PSTtoDelete}
$PSTRoot = $PST.GetRootFolder()
$PSTFolder = $namespace.Folders.Item($PSTRoot.Name)
$namespace.GetType().InvokeMember('RemoveStore',[System.Reflection.BindingFlags]::InvokeMethod,$null,$namespace,($PSTFolder))
I understand the third line, where to enter the file path to the PST itself but I am not sure what to enter for the rest of the lines.
I know this is a total newbie question, but any help would be super appreciated.
Thank you!
Here is a function i just wrote hope it helps
function Remove-OutlookStore($StoreFilePath){
get-process | where { $_.Name -like "Outlook" }| kill
$Outlook = new-object -com outlook.application
$Namespace = $Outlook.GetNamespace("mapi")
$Store = $namespace.Stores | ?{$_.FilePath -like $StoreFilePath} | %{$_}
$namespace.RemoveStore($Store.GetRootFolder())
get-process | where { $_.Name -like "Outlook" }| kill
}
Remove-OutlookStore -StoreFilePath C:\Test\Test.pst

Read attachments from Outlook which are from a particular sender using wildcards

I have trying to extract attachments from Outlook which are matching the wildcard of senderemailaddress attribute. As can be seen in the below code, I was trying out with two filters but to no avail.
When I use uncommented filter currently active in the code, the code doesn't throw any errors nor does it download the attachments matching the test case. However if I activate the commented filter and run it, I get the following error.
Exception calling "Restrict" with "1" argument(s): "Cannot parse condition. Error at
"like"."
At C:\Users\acer\Desktop\outlook.ps1:42 char:2
+ $filteredItems = $folder.items.Restrict($filter)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ComMethodTargetInvocation
Code:
$filepath = "C:\folder\subfolder\subsubfolder\"
function downloadFiles {
$filter = "[UnRead]=true AND [SenderEmailAddress] -match #example"
#$filter = "[UnRead]=true AND [SenderEmailAddress] -like '*#example*'"
Add-Type -Assembly "Microsoft.Office.Interop.Outlook" | Out-Null
$olFolders = "Microsoft.Office.Interop.Outlook.olDefaultFolders" -as [type]
$outlook = New-Object -ComObject Outlook.Application
$namespace = $outlook.GetNameSpace("MAPI")
$folder = $namespace.GetDefaultFolder($olFolders::olFolderInBox)
#$folder.Items | select SenderEmailAddress
$filteredItems = $folder.Items.Restrict($filter)
foreach ($objMessage in $filteredItems) {
$intCount = $objMessage.Attachments.Count
if ($intCount -gt 0) {
for ($i=1; $i -le $intCount; $i++) {
$objMessage.Attachments.Item($i).SaveAsFile($filepath+$objMessage.Attachments.Item($i).FileName)
}
}
$objMessage.Unread = $false
}
$outlook.Close
}
downloadFiles
Edit1 : Thanks everyone for the suggestions.
I was able to do it by filtering with unread = true and pattern matching the senderemailaddress from the properties of the filtered mails.
Adding the modified code:
$filepath = "C:\folder\subfolder\subsubfolder\"
function downloadFiles {
$filter="[UnRead]=true"
$emailfilter = "*#xyz.co.in"
$subjectfilter = "test file*"
Add-Type -Assembly "Microsoft.Office.Interop.Outlook" | Out-Null
$olFolders = "Microsoft.Office.Interop.Outlook.olDefaultFolders" -as [type]
$outlook = New-Object -ComObject Outlook.Application
$namespace = $outlook.GetNameSpace("MAPI")
$folder = $namespace.GetDefaultFolder($olFolders::olFolderInBox)
#$folder.Items | select SenderEmailAddress
$filteredItems = $folder.Items.Restrict($filter)
foreach ($objMessage in $filteredItems) {
$subject = $objMessage.Subject
$emailaddress = $objMessage.SenderEmailAddress
if(($emailaddress -like $emailfilter) -and ($subject -like $subjectfilter)){
$intCount = $objMessage.Attachments.Count
if ($intCount -gt 0) {
for ($i=1; $i -le $intCount; $i++) {
$objMessage.Attachments.Item($i).SaveAsFile($filepath+$objMessage.Attachments.Item($i).FileName)
}
}
$objMessage.Unread = $false
}
else {continue}
}
$outlook.Close
}
downloadFiles
Now the problem is scheduling this script? When I run this script using the powershell path in command prompt it's working fine. But when I schedule the same it's not completing. I could see the outlook process generated by the task scheduer in TaskManager and have to manually kill the process to terminate the same. Any ideas?
I'd use EWS. Saves having to allow programmatic access to Outlook.
Easiest way is to download from nuget. You can do this in PowerShell by first downloading nuget:
$sourceNugetExe = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
$targetNugetExe = "D:\Program Files\nuget\nuget.exe" # chaneg path to suit
Invoke-WebRequest $sourceNugetExe -OutFile $targetNugetExe
Set-Alias nuget $targetNugetExe -Scope Global -Verbose
Then download the EWS nuget package:
Set-Location D:\Temp # change to suit
nuget install 'Microsoft.Exchange.WebServices'
Now you can start using :)
# load the assembly
[void][Reflection.Assembly]::LoadFile("D:\Temp\Microsoft.Exchange.WebServices.2.2\lib\40\Microsoft.Exchange.WebServices.dll")
# set ref to exchange - may need to change the version
$s = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2)
# replace with your email address
$email = "your.email#domain.com"
# grab your own credentials
$s.UseDefaultCredentials = $true
# discover the url from your email address
$s.AutodiscoverUrl($email)
# get a handle to the inbox
$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($s,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)
#create a property set (to let us access the body & other details not available from the FindItems call)
$psPropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)
$psPropertySet.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text
$items = $inbox.FindItems(100) # change to suit
# loop through the emails - at this point, we don't have full info - we have to Load the email
# restrict on what we do know - if the email is read and if it has attachments
$items | where { !$_.IsRead -and $_.HasAttachments } | ForEach-Object {
# load the email, so we can get to other properties, like attachments, sender, etc
$_.Load()
# does the sender match our wildcard?
if ($_.Sender -like '*lmnopqr*') {
# loop through all file attachments
$_.Attachments | Where-Object { $_ -is [Microsoft.Exchange.WebServices.Data.FileAttachment] } | ForEach-Object {
# save them (yes, Load = Save in this instance!)
$_.Load("D:\Temp\$($_.Name)")
}
}
}
See the EWS link for more info on how to interact with EWS.
Also, see my other SO post which is out of date for where to get the EWS assembly from, but does have some useful info on extra EWS methods/properties. It also has details on how to use alternative credentials, if you're not using your own (or the process runing PowerShell doesn't have an Exchange account).
The provider does not allow the use of Like in the filter for this method. From this MSDN article:
There is no way to perform a "contains" operation. For example, you
cannot use Find or Restrict to search for items that have a particular
word in the Subject field. Instead, you can use the AdvancedSearch
method, or you can loop through all of the items in the folder and use
the InStr function to perform a search within a field.

How to get list for items within a week using PowerShell scripts?

I am writing a PowerShell script to get bookings from SharePoint 2007. Booking is made by creating an item in Calender list. The list has already been used for over 6 years. I want to write a script to send SMTP email reminders to users for bookings after 3 days. I used the following code to get the list items.
$service = New-WebServiceProxy -Uri $uri -UseDefaultCredential
...
$xmlDoc = new-object System.Xml.XmlDocument
$query = $xmlDoc.CreateElement("Query")
$viewFields = $xmlDoc.CreateElement("ViewFields","Week")
$queryOptions = $xmlDoc.CreateElement("QueryOptions")
$query.set_InnerXml("FieldRef Name='Full Name'")
$rowLimit = "50000"
...
if($service -ne $null){
try{
$list = ($service.GetListItems($listName, $viewName, $query, $viewFields, $rowLimit, $queryOptions, "")).data.row
$list | export-csv "$path\list.csv"
}catch {
Write-host "Error !! Cannot update the list.csv."
}
I don't want to get all the items in this 6 years every time i run the script, can i get items only in the next 3 days?
You could loop through your results and use some simple math to compare the dates, using CSOM will make the logic much easier to adapt in the future. You might be able to use the CAML query to do the date filtering too but that's not my forte:
Import-Module 'C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll'
$siteUrl = "http://Your.Site/Subsite"
$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl)
$lookupList = $ctx.Web.Lists.GetByTitle('Bookings')
$query = New-Object Microsoft.SharePoint.Client.CamlQuery
$query.ViewXml = "<View>
<RowLimit></RowLimit>
</View>"
$listItems = $lookupList.getItems($query)
$ctx.Load($listItems)
$ctx.ExecuteQuery()
foreach($item in $listItems)
{
if([DateTime]$item["DueDate"] -lt [DateTime]::Now.Add([TimeSpan]::FromDays(3)) -and [DateTime]$item["DueDate"] -gt [DateTime]::Now)
{
$item["Title"]
}
}

How to check if an email was read in Exchange 2010 using powershell?

We are searching through inboxes for Exchange 2010 and trying to find out is a specific email was read by the specified user/mailbox.
We can leverage powershell for the search, unsure of how to do this
You could use Exchange Web Services. You will need to download the API. This will require the user running the script to have have authority to access the mailbox in question and you know the subject of email you're looking for.
Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\Microsoft.Exchange.WebServices.dll"
$exchService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010)
$exchService.AutodiscoverUrl("email#yourdomain.com",{$true})
$Mailbox = New-Object Microsoft.Exchange.WebServices.Data.Mailbox("email#yourdomain.com")
$Inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchservice,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)
$MessageView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(100) #Find Max 100 Items
$SerachFilter1 = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::Subject,"Test Subject")
$SerachFilter2 = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::IsRead, $false)
$SearchFilterCollection = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::And)
$SearchFilterCollection.Add($SerachFilter1)
$SearchFilterCollection.Add($SerachFilter2)
If($Messages.TotalCount > 0){
#Do something....
}

Powershell Bulk Find ActiveDirectory Objects

I'm trying to develop a powershell script to help with AD Group Membership management. We have a handful of large groups (30k-60k+ objects) that we want to update with data from another system.
The script loads the objects that should be in the group from a text file. Each object then has to located in AD using a System.DirectoryServices.DirectorySearcher. After that each object is added to the group membership.
The script spends some 80% of its time looking up each object, is there a bulk way to find objects in AD with powershell?
Thanks!
This is the fast way to query AD that I found in my experience, you need to change the query to find specific objects, in this code you'll find all user/person object in $objRecordSet.
$Ads_Scope_SubTree = 2
$objConnection = new-Object -com "ADODB.Connection"
$objCommand = new-Object -com "ADODB.Command"
$objConnection.Provider = "ADsDSOObject"
$objConnection.Open( "Active Directory Provider")
$objCommand.ActiveConnection = $objConnection
$objCommand.Properties.Item("Page Size").value = 1000
$objCommand.Properties.item("Searchscope").value = $Ads_Scope_SubTree
$objCommand.CommandText = "Select Name From 'LDAP://DC = int, DC= my, DC = local' Where objectCategory = 'Person'"
$objRecordSet = $objCommand.Execute()
$objRecordSet.RecordCount
More info here
You perhaps can try System.DirectoryServices.Protocols (S.DS.P) the native (non managed) version is quite efficient.
Here is a PowerShell starting script :
# ADDP-Connect.PS1
Clear-Host
# Add the needed assemblies
Add-Type -AssemblyName System.DirectoryServices.Protocols
# Connexion
$serverName = "WM2008R2ENT"
$ADDPConnect = New-Object System.DirectoryServices.Protocols.LdapConnection $serverName
$userName = "JPB"
$pwd = "PWD"
$domain = "Dom"
$ADDPConnect.Credential = New-Object system.Net.NetworkCredential -ArgumentList $userName,$pwd,$domain
# Create a searcher
$searchTargetOU = "dc=dom,dc=fr"
$searchFilter = "(samAccountName=user1)"
$searchScope = [System.DirectoryServices.Protocols.SearchScope]::Subtree
$searchAttrList = $null
foreach($user in "user1","user2","user3")
{
$searchFilter = "(samAccountName=$user)"
$searchRequest = New-Object System.DirectoryServices.Protocols.SearchRequest -ArgumentList $searchTargetOU,$searchFilter,$searchScope,$searchAttrList
$searchResponse = $ADDPConnect.SendRequest($searchRequest)
foreach($searchEntries in $searchResponse.Entries)
{
$searchEntries.DistinguishedName
}
}
If you start seeing timeout issues then set the timeout parameter appropriately like shown below
$ADDPConnect = New-Object System.DirectoryServices.Protocols.LdapConnection $serverName
$ADDPConnect.Timeout = "1000"
The below can help if you see timeout issues during execution
$ADDPConnect = New-Object System.DirectoryServices.Protocols.LdapConnection $serverName
$ADDPConnect.Timeout = "1000"