Unable to iterate through e-mail objects using powershell in Outlook - powershell

I'm writing a script, which takes Outlook folder as input and moves every unread mail to different folder. My code:
Add-Type -assembly "Microsoft.Office.Interop.Outlook"
$Outlook = New-Object -ComObject Outlook.Application
$namespace = $Outlook.GetNameSpace("MAPI")
$olFolderInbox = 6
$inbox = $namespace.GetDefaultFolder($olFolderInbox)
$myFolder = $namespace.pickfolder()
$toFolder = $inbox.Folders | where-object { $_.name -eq "UnreadMessages" }
$messages = $myFolder.Items
$messageCount = $messages.count
for ($i = $messageCount - 1; $i -ge 0; $i--)
{
if ($messages[$i].unread -eq $True)
{
$message.move($toFolder)
}
}
The problem is, that I can not iterate through "messages" objects. Error:
Unable to index into an object of type System.__ComObject.
If it's not possible, then how am I supposed to do it?
Thanks in advance.
EDIT. It's my first day using powershell :)

All Outlook collections are 1 based, not 0 - you need to iterate from Items.Count down to 1.
Secondly, do not just iterate through all messages in a folder, use Items.Find/FindNext. In your case, the search criteria would be "[Unread] = true".

Try to do a get-member on it, and see what your options are.
$Messages | Get-Member -force
If you figure out how you want to proceed, then you can use the ForEach-Object to loop through $Messages' content like this
$Messages | ForEach-Object { Script code }
Or use its abbreviation:
$Messages | % { Script code }

While you cannot use PowerShell's usual indexing syntax - $messages[$i] -
for accessing the elements of $messages, you can use .Items($i) on the folder object (see the docs):
Additionally, as noted in Dmitry Streblechenko's helpful answer, indices start at 1, not 0:
for ($i = $myFolder.Items.Count; $i -ge 1; $i--)
{
$message = $myFolder.Items($i)
if ($message.unread)
{
$message.move($toFolder)
}
}
For simple forward enumeration without indices you can use foreach:
foreach ($message in $myFolder.Items) {
if ($message.unread)
{
$message.move($toFolder)
}
}
($myFolder.Items | ForEach-Object { ... } should work too, but it will be slower.)
That said, Dmitry's answer answer also points to how to perform a filtered enumeration at the source, which performs much better.

Thanks for replying. All your answers were helpful. I finally used foreach loop. While I was trying to use $message = $myFolder.Items($i) following error appeared:
Method invocation failed because [System.__ComObject] doesn't cont
ain a method named 'Items'
so there is no possibility to iterate using indexes in this case.
Thank You all for your time :)

Related

for loop through 5 textboxes

I have created a GUI with 5 Textboxes. I call them $textboxHost1 - 5.
Now I have an array in which I'm gonna save up to 5 values and then write each value according to the order into the textboxes. The first value in the array should be written into the first $textboxHost1 box.
To do that, I would like to make a for loop and have written this code
#$hostnameneingabe: Array, in which the values are saved.
$hostnameneingabeCount = $hostnameneingabe.Count
for($i = 0; $i -le $hostnameneingabeCount; $i++) {
#code here
}
Now, I'm looking for a way to go down the order, so that the first $textboxHost1 comes firstly and so on.
To be accurate, the variable $textboxHost should be incrementally increased in the loop and the values at the position $i in the array should be written into that textbox.
sth like
for($i = 0; $i -le $hostnameneingabeCount; $i++) {
$textboxHost$i =
}
I suppose you would be liking something like this?
$textboxHosts = Get-Variable | ? {$_.Name -match "textBoxHost[0-9]" -and $_.Value -ne $null} | sort Name
After this you can process that var with eg. a foreach:
foreach ($textboxHost in $textboxHosts) {<# Do some stuff #>}
You have to use an array, because otherwise you can't loop through them:
$textboxHost = #(0..4)
#Textbox 0
$textboxHost[0] = New-Object System.Windows.Forms.TextBox
$textboxHost[0].Text = "test"
#Textbox 1
$textboxHost[1] = New-Object System.Windows.Forms.TextBox
$textboxHost[1].Text = "test"
foreach ($textbox in $textboxHost){
#Do whatever you want with the textbox
$textbox =
}

Powershell script exits ForEach-Object loop prematurely [duplicate]

This question already has answers here:
Why does 'continue' behave like 'break' in a Foreach-Object?
(4 answers)
Closed 5 years ago.
So I've been writing a script that will take all of the data that is stored in 238 spreadsheets and copy it into a master sheet, as well as 9 high level report sheets. I'm really not sure why, but after a specific document, the script ends prematurely without any errors being posted. It's very strange. I'll post some anonymized code below so maybe someone can help me find the error of my ways here.
As far as I can tell, the document that it exits after is fine. I don't see any data errors in it, and the info is copied successfully to the master document before powershell just calls it quits on the script completely.
I've tried changing the size of the data set by limiting only to the folder that contains the problem file. It still ends after the same file with no error output. I cannot upload the file due to company policy, but I really don't see anything different about the data on that one file when compared to any other file of the same nature.
Also, apologies in advance for the crappy code. I'm not a developer and have been relearning powershell since it's the only tool available to me right now.
$StartTime = Get-Date -Format g
Write-Host $StartTime
pushd "Z:\Shared Documents\IO"
$TrackTemplate = "C:\Users\USERNAME\Desktop\IODATA\MasterTemplate.xlsx"
# Initialize the Master Spreadsheet
$xlMaster = New-Object -ComObject Excel.Application
$xlMaster.Visible = $False
$xlMaster.DisplayAlerts = $False
$MasterFilePath = "C:\Users\USERNAME\Desktop\IODATA\Master.xlsx"
Copy-Item $TrackTemplate $MasterFilePath
$wbMaster = $xlMaster.Workbooks.Open($MasterFilePath)
$wsMaster = $wbMaster.Worksheets.Item(2)
$wsMaster.Unprotect("PASSWORD")
$wsMasterRow = 3
# Initialize L4 Document Object
$xlL4 = New-Object -ComObject Excel.Application
$xlL4.Visible = $False
$xlL4.DisplayAlerts = $False
# Initialize object for input documents
$xlInput = New-Object -ComObject Excel.Application
$xlInput.Visible = $False
$xlInput.DisplayAlerts = $False
# Arrays used to create folder path names
$ArrayRoot = #("FOLDER1","FOLDER2","FOLDER3","FOLDER4","FOLDER5","FOLDER6","FOLDER7","FOLDER8","FOLDER9")
$ArrayShort = #("SUB1","SUB2","SUB3","SUB4","SUB5","SUB6","SUB7","SUB8","SUB9")
# $counter is used to iterate inside the loop over the short name array.
$counter = 0
$FileNumber = 0
$TotalFiles = 238
$ArrayRoot | ForEach-Object {
$FilePathL4 = "C:\Users\USERNAME\Desktop\IODATA\ROLLUP\" + $ArrayShort[$counter] + "_DOC_ROLLUP.xlsx"
Copy-Item $TrackTemplate $FilePathL4
$wbL4 = $xlL4.Workbooks.Open($FilePathL4)
$wsL4 = $wbL4.Worksheets.Item(2)
$wsL4.Unprotect("PASSWORD")
$wsL4Row = 3
If ($ArrayShort[$counter] -eq "SUB7") {$FilePath = "Z:\Shared Documents\IO\" + $_ + "\" + $ArrayShort[$counter] + " - DOC v2\"}
Else {$FilePath = "Z:\Shared Documents\IO\" + $_ + "\!" + $ArrayShort[$counter] + " - DOC v2\"}
Get-ChildItem -Path $FilePath | ForEach-Object {
If ($_.Name -eq "SPECIFIC_DOC.xlsx") {Continue}
$FileNumber += 1
Write-Host "$FileNumber / $TotalFiles $_"
$wbInput = $xlInput.Workbooks.Open($_.FullName)
$wsInput = $wbInput.Worksheets.Item(2)
$wsInputLastRow = 0
#Find the last row in the Input document
For ($i = 3; $i -le 10000; $i++) {
If ([string]::IsNullOrEmpty($wsInput.Cells.Item($i,1).Value2)) {
$wsInputLastRow = $i - 1
Break
}
Else { Continue }
}
[void]$wsInput.Range("A3:AC$wsInputLastRow").Copy()
Start-Sleep -Seconds 1
[void]$wsMaster.Range("A$wsMasterRow").PasteSpecial(-4163)
Start-Sleep -Seconds 1
$wsMasterRow += $wsInputLastRow - 2
[void]$wsL4.Range("A$wsL4Row").PasteSpecial(-4163)
Start-Sleep -Seconds 1
$wsL4Row += $wsInputLastRow - 2
$wbInput.Close()
$wbMaster.Save()
}
$counter += 1
$wsL4.Protect("PASSWORD")
$wbL4.Save()
$wbL4.Close()
}
$wsMaster.Protect("PASSWORD")
$wbMaster.Save()
$wbMaster.Close()
$xlMaster.Quit()
$EndTime = Get-Date -Format g
$TimeTotal = New-Timespan -Start $StartTime -End $EndTime
Write-Host $TimeTotal
To continue pipeline processing with the next input object, use return - not continue - in the script block passed to the ForEach-Object cmdlet.
The following simple example skips the 1st object output by Get-ChildItem and passes the remaining ones through:
$i = 0; Get-ChildItem | ForEach-Object{ if ($i++ -eq 0) { return }; $_ }
There is currently (PSv5.1) no direct way to stop the processing of further input objects - for workarounds, see this answer of mine.
By contrast, as you've discovered, break and continue only work as expected in the script block of a for / foreach statement, not directly in the script block passed to the ForeEach-Object cmdlet:
For instance, the following produces no output (using break would have the same effect):
$i = 0; Get-ChildItem | ForEach-Object{ if ($i++ -eq 0) { continue }; $_ }
The reason is that continue and break look for an enclosing for / foreach statement to continue / break out of, and since there is none, the entire command is exited; in a script, the entire script is exited if there's no enclosing for / foreach / switch statement on the call stack.

Passing variables to runspaces and back

so I have the following code:
#region Initialize stuff
$files = gci "C:\logs\*.log"
$result = [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList))
$RunspaceCollection = #()
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$ScriptBlock = {
Param($file, $result)
$content = Get-Content $file.FullName -ReadCount 0
foreach ($line in $content) {
if ($line -match 'A002') {
[void]$result.Add($($line -replace ' +',","))
}}}
#endregion
#region Process Data
Foreach ($file in $files) {
$Powershell = [PowerShell]::Create().AddScript($ScriptBlock).AddArgument($file).AddArgument($result)
$Powershell.RunspacePool = $RunspacePool
[Collections.Arraylist]$RunspaceCollection += New-Object -TypeName PSObject -Property #{
Runspace = $PowerShell.BeginInvoke()
PowerShell = $PowerShell
}}
While($RunspaceCollection) {
Foreach ($Runspace in $RunspaceCollection.ToArray()) {
If ($Runspace.Runspace.IsCompleted) {
[void]$result.Add($Runspace.PowerShell.EndInvoke($Runspace.Runspace))
$Runspace.PowerShell.Dispose()
$RunspaceCollection.Remove($Runspace)
}}}
#endregion
#region Parse Data
$data = ConvertFrom-Csv -InputObject $result -Header "1","2","3","TimeIn","TimeOut","4","5","Dur"
foreach ($line in $data) {
if ($line.TimeIn -match "A002") { $TimeIn += [timespan]::Parse($line.Dur) }
else { $TimeOut += [timespan]::Parse($line.Dur) }}
#endregion
It works, but I don't completely understand how ;)
what is [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) and why does it work, while regular ArrayList doesn't? Why do I need this "Synchronized" and what it does? Could you please explain or point me to some materials about this? I can't seem to find anything relevant. Thank you!
To guarantee the thread safety of the ArrayList, all operations must
be done through this wrapper.
Enumerating through a collection is intrinsically not a thread-safe
procedure. Even when a collection is synchronized, other threads can
still modify the collection, which causes the enumerator to throw an
exception. To guarantee thread safety during enumeration, you can
either lock the collection during the entire enumeration or catch the
exceptions resulting from changes made by other threads.
Source:
https://msdn.microsoft.com/en-us/library/3azh197k(v=vs.110).aspx
This was found by googling
ArrayList Synchronized Method
It would be most beneficial for you to learn the fundamentals of object oriented script/programming so in the future you know exactly what to look for. There are many ways to implement .NET namespaces, classes, methods, and elements in a powershell host, [System.Collections.ArrayList]::Synchronized((New-Object System.Collections.ArrayList)) being one of them.

Powershell array of arrays loop process

I need help with loop processing an array of arrays. I have finally figured out how to do it, and I am doing it as such...
$serverList = $1Servers,$2Servers,$3Servers,$4Servers,$5Servers
$serverList | % {
% {
Write-Host $_
}
}
I can't get it to process correctly. What I'd like to do is create a CSV from each array, and title the lists accordingly. So 1Servers.csv, 2Servers.csv, etc... The thing I can not figure out is how to get the original array name into the filename. Is there a variable that holds the list object name that can be accessed within the loop? Do I need to just do a separate single loop for each list?
You can try :
$1Servers = "Mach1","Mach2"
$2Servers = "Mach3","Mach4"
$serverList = $1Servers,$2Servers
$serverList | % {$i=0}{$i+=1;$_ | % {New-Object -Property #{"Name"=$_} -TypeName PsCustomObject} |Export-Csv "c:\temp\$($i)Servers.csv" -NoTypeInformation }
I take each list, and create new objects that I export in a CSV file. The way I create the file name is not so nice, I don't take the var name I just recreate it, so if your list is not sorted it will not work.
It would perhaps be more efficient if you store your servers in a hash table :
$1Servers = #{Name="1Servers"; Computers="Mach1","Mach2"}
$2Servers = #{Name="2Servers"; Computers="Mach3","Mach4"}
$serverList = $1Servers,$2Servers
$serverList | % {$name=$_.name;$_.computers | % {New-Object -Property #{"Name"=$_} -TypeName PsCustomObject} |Export-Csv "c:\temp\$($name).csv" -NoTypeInformation }
Much like JPBlanc's answer, I kinda have to kludge the filename... (FWIW, I can't see how you can get that out of the array itself).
I did this example w/ foreach instead of foreach-object (%). Since you have actual variable names you can address w/ foreach, it seems a little cleaner, if nothing else, and hopefully a little easier to read/maintain:
$1Servers = "apple.contoso.com","orange.contoso.com"
$2Servers = "peach.contoso.com","cherry.contoso.com"
$serverList = $1Servers,$2Servers
$counter = 1
foreach ( $list in $serverList ) {
$fileName = "{0}Servers.csv" -f $counter++
"FileName: $fileName"
foreach ( $server in $list ) {
"-- ServerName: $server"
}
}
I was able to resolve this issue myself. Because I wasn't able to get the object name through, I just changed the nature of the object. So now my server lists consist of two columns, one of which is the name of the list itself.
So...
$1Servers = += [pscustomobject] #{
Servername = $entry.Servername
Domain = $entry.Domain
}
Then...
$serverList = $usaServers,$devsubServers,$wtencServers,$wtenclvServers,$pcidevServers
Then I am able to use that second column to name the lists within my foreach loop.

Powershell foreach loop with multiple if statements

I have a script snippet that basically gets some unformated xml type output from a command.
Then in a defined filter section I'm transforming that into xml and run a search loop on each node, as from the part below.
What I'm trying to figure out is how I can make a multiple if -and loop, like
if (($CimProperty.VALUE -eq $somevariable) -and ($CimProperty.VALUE -eq $something-else))
The only problem is that since it's a foreach loop it won't take it, as it takes each property at a time and then the 'if statement -and portion' for it, which doesn't work since it's the same xml type property section.
In other words the loop doesn't go through the entire array to identify both conditions from the if statement.
PS code snippet:
filter Import-CimXml
{
$CimXml = [Xml]$_
$CimObj = New-Object -TypeName System.Object
foreach ($CimProperty in $CimXml.SelectNodes(“/INSTANCE/PROPERTY”))
{
if ($CimProperty.VALUE -eq $somevariable)
{
write-host "found it"
}
}
}
I hope the scenario is clear, thanks everyone in advance!
For just two conditions, you can make it fairly straight forward like (the untested);
filter Import-CimXml
{
$foundfirst = $false
$foundsecond = $false
$CimXml = [Xml]$_
$CimObj = New-Object -TypeName System.Object
foreach ($CimProperty in $CimXml.SelectNodes(“/INSTANCE/PROPERTY”))
{
if ($CimProperty.VALUE -eq $somevariable)
{
$foundfirst = $true
}
if ($CimProperty.VALUE -eq $someothervariable)
{
$foundsecond = $true
}
}
if ($foundfirst -and $foundsecond)
{
write-host "found it"
}
}
For more conditions, you may want to use arrays of corresponding matchwords/booleans instead.
Just extend your xpath query to do it all. It's less code and should be more efficient.
filter Import-CimXml
{
$CimXml = [Xml]$_
$CimObj = New-Object -TypeName System.Object
if($CimXml.SelectNodes(“/INSTANCE[PROPERTY='$somevariable' and PROPERTY='$someothervariable']”).Count -gt 0) {
write-host "found it"
}
}