Send-MailMessage full of "System.Object[]"after the first email - powershell

I have written a piece of powershell code to send emails to different users based on an extraction from a table (and some conditions).
#extraction skipped
foreach($row in $result) {
Clear-Variable template* -Scope Global
$template = (Get-Content "C:\template.html")
#user definition skipped
$message = "Hello..."
$html = $template |
ForEach-Object { if($_.Trim() -eq "<p>$[CONTENT]</p>") {
-join(($_).ToString().Replace("$[CONTENT]", $message),$html)
} else { -join($_, $html) }
}
Send-MailMessage -From $fromAddr -To $mail -Bcc $maintainer -Subject $sub -Body ($html | Out-String) -BodyAsHtml -SmtpServer $smtpserver -Encoding UTF8
}
If you need anything else feel free to ask, I felt like this was the relevant part of the code but I'm not 100% sure.
The Problem: Basically if i get only one user the email text looks fine. If there is more than one $row the second email is full of System.Object[].
I'm not sure what I've been doing wrong. My guess would be ForEach-Object, but why work for the first and not the following?
Thanks in advance for any help.

The problem in your code on the second iteration is that it's basically doing this:
$html = $template | ... -join($_, $html) ...
where $html still contains the value from the first iteration, so you're joining the new $message with the entirety of the first email text and getting into a bit of a pickle.
You can avoid this if you add:
$html = $null
$html = $template | ... -join($_, $html)
so each iteration gets a clean start.
Having said that, you can just do as #Mathias R. Jessen suggested and replace your entire templating logic with this:
$body = -join #((Get-Content .\template.html) -replace '\$\[CONTENT\]', $message)
or more succinctly, this:
$body = (Get-Content .\template.html -Raw).Replace("`$[CONTENT]", $message)

Related

Cannot Get Send-MailMessage to Send Multiple Lines

I apologize for this in advance because I see plenty of questions regarding it throughout the web but for some reason I'm still having issues.
I have a script that creates an array that has information dynamically added to it. When the script completes, I need it to email that information to me. The problem is that each line is combined to that I get 1 line. For example:
$Body = #()
$Body += "1"
$Body += "2"
$Body += "3"
$Body += "4"
Here's my send command:
Send-MailMessage -To $Recipients -From $Sender -Smtp $SMTP $Subject "Test" -Body ($Body | Out-String)
What I get in the email body is: 1234
I've tried this for loop to append `n to the beginning of each line (except the 1st) like so:
for ($i = 0; $i -lt Body.Count; $i++){
if($i -eq 0){
Write-Host $i
}else{
$Body[$i] = "`n" + $Body[$i]
Write-Host $Body[$i]
}
}
The results of that are better but I get an extra line:
1
2
3
4
Ultimately I just want this:
1
2
3
4
In the past, I've gotten the format I want by creating the variable like this:
$Body = #"
This is the email I want to send. It formats great if:
1) I want to make all the content static.
Is it possible to create the `$Body variable like this but add line by line dynamically and maintain a serpate line. (without extra lines)?
"#
What am I missing? It HAS to be something simple... Thanks for your help!
WOW so it's such a finicky thing... I found that using this would get everything to look right (as suggested by Ryan, above):
-Body ($Body -Join "`r`n")
But in the context of my actual script it wasn't working. Well, it appears that it all had to do with the way the date was going in there. Here's a reduction to show:
DOES NOT WORK
$Body = #()
$Body += "Beginning processing on: " + (Get-Date)
$Body += "No advertisements found, exiting"
ALSO DOES NOT WORK
$Body = #()
$DateTime = Get-Date
$Body += "Beginning processing on: $DateTime"
$Body += "No advertisements found, exiting"
When I removed anything that had to do with the Date, formatting was correct. Ultimately it just took a simple "." to make it work right.
WORKS - NOTE THE 3RD LINE
$Body = #()
$DateTime = Get-Date
$Body += "Beginning processing on: $DateTime." #Note the period at the end of the line.
$Body += "No advertisements found, exiting"
Presumably I could just concatenate and it would work but oh well, I'm keeping it as it. I didn't think that it would be that finicky about a period but now I know.

Best practice for emailing a body of appended strings?

I have a Powershell script that automates a process and emails the report of what happened.
Send-MailMessage -To $toAddress -From "no-reply#domain.org" -subject "Automation status" -body $bodystr -SmtpServer SERVER1 -EA Stop
So $bodystr is essentially an appended string throughout the script to report what happened and has multiple lines. Things like:
$bodystr = $bodystr + "Line found: 305`n"
$bodystr = $bodystr + "Moving line 305 to 574`n"
The Send-MailMessage command is at the bottom of the script outside any function. But most other code is in various different functions.
The issue is $bodystr does not seem accessible inside functions, and so the email is lacking a lot of information.
I believe I could use Set-Variable or passing arguments, but there are so many arguments it seems farther away from best practice to add a new argument for each function just to keep the string updated.
What's the best practice to handle this?
As a general rule, don't write data back to variables outside the scope of your function.
If you are compiling an email by gathering data from multiple sources, abstract it away in multiple functions that does one thing each and have them return a multiline string with the relevant output.
At the end of your script, collect the different message body parts and join them to a single string before sending.
In this example, we have a script that takes a path to a log file, defines a function to extract errors from a log file, and send an email with the errors in the body:
param(
[ValidateScript({Test-Path $_ -PathType Leaf })]
[string]$LogPath = 'C:\Path\To\File.log',
[string]$From = 'noreply#company.example',
[string]$To = #('ceo#company.example','finance#company.example'),
[string]$Subject = 'Super Important Weekly Report',
[string]$SmtpServer = $PSEmailServer,
[string]$Credential
)
# Define functions with a straight forward purpose
# e.g. Searching a logfile for errors
function Parse-Logfile {
param($LogPath)
[string[]]$LogErrors = #()
Get-Content $LogPath |ForEach-Object{
if($_ -contains $Error){
$LogErrors += $_
}
}
# Create and return a custom object has the error details as properties
New-Object psobject -Property #{
ErrorCount = $LogErrors.Count
Errors = $LogErrors
}
}
# Create a email template that's easy to maintain
# You could store this in a file and add a $TemplateFile parameter to the script ;-)
$EmailTemplate = #'
Hi there!
Found {0} errors in log file: {1}
{2}
Regards
Zeno
'#
# Use your function(s) to create and gather the details you need
$ErrorReport = Parse-Logfile -LogPath $LogPath
# If necessary, concatenate strings with -join
$ErrorString = $ErrorReport.Errors -join "`n"
# Use the format operator to the final Body string
$Body = $EmailTemplate -f $ErrorReport.ErrorCount, $LogPath, $ErrorString
# Set up a splatting table (Get-Help about_Splatting)
$MailParams = #{
To = $To
From = $From
Subject = $Subject
Body = $Body
SmtpServer = $SmtpServer
}
if($PSBoundParameters.ContainsKey('Credential')){
$MailParams['Credential'] = $Credential
}
# Send mail
Send-MailMessage #MailParams

Powershell - Combining Looped Results

I previously posted this script and had some help but later deleted it to avoid confusion. I have it almost working with the one exception. The content of the email sent to each manager only includes the data from the last direct report in the array. Do I need to restructure the script? I feel like I am chasing my tail if I had one :)
EDIT: I added the recommended changes, but now I get:
Email 1: Manager and direct reports
Email 2: Content of Email 1 and the next Manager and direct reports.
Email 3: Content of Email 1 & 2, the next Manager and direct reports.....
RESOLVED: Thanks TheMadTechnician. Modifying the += and moving the $body to the proper location in the loops was the key.
My script Updated script:
Import-Module -Name ActiveDirectory
$today = (Get-Date).ToString()
# Html
$a = "<style>"
$a = $a + "BODY{background-color:Lavender ;}"
$a = $a + "TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}"
$a = $a + "TH{border-width: 1px;padding: 5px;border-style: solid;border-color: black;background-color:thistle}"
$a = $a + "TD{border-width: 1px;padding: 5px;border-style: solid;border-color: black;background-color:PaleGoldenrod}"
$a = $a + "</style>"
# Email Variables
$smtp = "192.168.1.1"
$to = "User#Company.com"
$from = "User#company.com"
$subject = "Managers - Direct Report's Group Membership"
$managers = Get-ADUser -Filter * -Properties name, directreports, EmailAddress | where {$_.directreports -ne $Null}
foreach ($i in $managers)
{
$mgrname = $i.Name
$mgremail = $i.EmailAddress
$dreports = $i.directreports
$body = "Report Date $today ."
$body = "`n"
$body = "<H3>The direct reports for $mgrname<H3>"
foreach ($d in $dreports)
{
$user = get-aduser $d -properties *
$mems = $user.memberof -replace '^CN=(.+?),(?:OU|CN)=.+','$1' | %{New-Object PSObject -Property #{'Group Membership'=$_}} | convertTo-html -Head $a -Body "<H2>Group Membership.</H2>"
$dreport = $d -replace '^CN=(.+?),(?:OU|CN)=.+','$1'
$body += "`n"
$body += "<H3>Direct Report: $dreport</H3>"
$body += "`n"
$body += $mems
$body += "`n"
}
Send-MailMessage -SmtpServer $smtp -To $to -From $from -Subject $subject -Body $body -BodyAsHtml
}
Ok, resolution for this issue was to move the bulk of the email generation outside of the ForEach($d in $dreports) loop. The header section ($Body='Report for $today', and the following two lines) was moved up, and the line to actually send the mail was moved down outside of the loop. Updated code is already in the OP I think, so I won't bother to repost it again.
This line:
$body = "Report Date $today ."
Is resetting the $body variable for each direct report. So by the time you get to Send-MailMessage all you have is the last direct report.
Try moving that line above the foreach loop that iterates through the direct reports.
Assuming you want to send all reports as individual emails to all managers, move the Send-MailMessage call inside the report foreach loop.
Or if you want to send one email containing all reports to each manager, declare $body outside of you report loop and change the first assigment within the loop to `$body += "Report Date $today ."
Every iteration of foreach ($d in $dreports) resets $body. Change the line
$body = "Report Date $today ."
to
$body += "Report Date $today ."

enumerate and email with powershell

ok fellas, I've been reading, researching, learning and testing about powershell. Within the last 20 days, here is what I've been able to come up with.
get-mailbox -identity $_.name | select name,userprincipalname,#{label="Size";expression={$size = Get-Mailboxstatistics $_.identity;$size.TotalItemSize.Value.ToMB()}},#{label="Items";expression={$items = Get-Mailboxstatistics $_.name;$item.ItemCount}}
I stored this in a script called accountsizes.ps1. It works exactly as I expected by outputting all the email accounts with the sizes, but in order for me to get only the mailboxes over 2048MB, I have to call it like this:
PS C:\accountsizes.ps1 | where size -gt "2048" | select userprincipalname,size
And this works by return the email addresses and mailbox sizes in MBs. But now my dilemma is; how do I enumerate through the results and extract each email address and send an email to that user and myself, warning them that their mailbox is too large and they need to archive. From what I been reading and learning, I would have to use a ForEach loop and the send-mailmessage cmdlet. I cannot figure out how to use the ForEach and incorporate it with the script: Here is I go brain dead with the ForEach:
PS C:\accountsizes.ps1 | where size -gt "2048" | select userprincipalname,size | ForEach($user in userprincipalname){$_.userprincipalname}
I do not know the right way to go about doing this (so, don't ask me why I'm doing this way :)), I have no previous knowledge about scripting and coding.
Here is my email part:
$smtpserver = "domain.com"
$smtpFrom = "me#domain.com"
$smtpTo = "you#domain.com"
$messageSubject = "Warning Email Mailbox Too Large"
$body = "blah blah blah blah"
send-mailmessage -from $smtpFrom -to $smtpTo -subject $messageSubject -body $body
Thanks in advance for your helpful advice.
The foreach keyword and the ForEach-Object cmdlet are two different things.
If you use the foreach keyword, you give a name to the iteration variable, and you iterate on a collection. For example:
$collection = #("one", "two")
foreach ($item in $collection) {
Write-Host $item
}
Instead, if you pipe commands outputs, you have to use the ForEach-Object cmdlet with a script block. Inside the script block, you refer to the iteration variable with the special variable $_. Example:
$collection = #("one", "two")
$collection | ForEach-Object {
Write-Host $_
}
You can shorten ForEach-Object with %:
$collection | % {
Write-Host $_
}
So, in your case you should probably do this:
C:\accountsizes.ps1 | where size -gt "2048" | select userprincipalname,size | % { $_.userprincipalname }
Paolo provided excellent information and deserves upvotes if nothing else. As to your question of how to enumerate and send email you probable need something like:
C:\accountsizes.ps1 | where size -gt "2048" | %{
$smtpServer = "smtp.domain.com"
#Creating SMTP server object
$SMTP = new-object Net.Mail.SmtpClient($smtpServer)
#Creating a Mail object
$EMail = new-object Net.Mail.MailMessage
#Construct Email
$EMail.From = "me#domain.com"
$EMail.ReplyTo = "me#domain.com"
$EMail.To.Add($_.userprincipalname)
$EMail.subject = "Warning Email Mailbox Too Large"
$EMail.body = "blah blah blah blah"
$SMTP.Send($EMail)
}
You could get a lot more fancy, go by size and send different emails depending on how large their mailbox is, or get content from files for subject and body depending on size, but that is just going to make things complicated. You could also use Send-MailMessage, and that works just fine, I just like this way because it makes it easier to work with in my opinion than one really long line with a ton of switches. If the message and subject are going to be generic you may want to do something more like:
$smtpServer = "smtp.domain.com"
#Creating SMTP server object
$SMTP = new-object Net.Mail.SmtpClient($smtpServer)
#Creating a Mail object
$EMail = new-object Net.Mail.MailMessage
#Construct Email
$EMail.From = "me#domain.com"
$EMail.ReplyTo = "me#domain.com"
C:\accountsizes.ps1 | where size -gt "2048" | %{$EMail.BCC.Add($_.userprincipalname)}
$EMail.subject = "Warning Email Mailbox Too Large"
$EMail.body = "blah blah blah blah"
$SMTP.Send($EMail)
That would make one email and BCC everybody on it. Then you could do another email to yourself stating who warnings got sent to.

Join an array with newline in PowerShell

I have an array of names that I'm trying to join using a new line character. I have the following code
$body = $invalid_hosts -join "`r`n"
$body = "The following files in $Path were found to be invalid and renamed `n`n" + $body
Finally, I send the contents via email.
$From = "myaddress#domain.com"
$To = "myaddress#domain.com
$subject = "Invalid language files"
Send-MailMessage -SmtpServer "smtp.domain.com" -From $From -To $To -Subject $subject -Body $body
When I receive the message, the line The following files in <filepath> were found to be invalid and renamed has the expected double space, but the contents of $invalid_hosts are all on one line. I've also tried doing
$body = $invalid_hosts -join "`n"
and
$body = [string]::join("`n", $invalid_hosts)
Neither way is working. What do I need to do to make this work?
Pipe the array to the Out-String cmdlet to convert them from a collection of string objects to a single string:
PS> $body = $invalid_hosts -join "`r`n" | Out-String
It is sufficient just pipe to Out-String (see https://stackoverflow.com/a/21322311/52277)
$result = 'This', 'Is', 'a', 'cat'
$strResult = $result | Out-String
Write-Host $strResult
This
Is
a
cat
I'm unsure about how to answer everything else, but for guaranteed newlines in Powershell, use:
[Environment]::NewLine in place of your "`n"
Had to solve this today; thought I'd share my answer since the question and other answers helped me find the solution. Instead of
$body = $invalid_hosts -join "`r`n"
$body = "The following files in $Path were found to be invalid and renamed `n`n" + $body
use
$MessageStr = "The following files in " + $Path + " were found to be invalid and renamed"
$BodyArray = $MessageStr + $Invalid_hosts
$Body = $BodyArray -join "`r`n"
I went about it differently and just replaced the newline
$result -replace("`r`n"," ")
I am certainly no expert in PowerShell, but I found a much easier way to do it. Simply pipe to Write-Host like this:
$array = 'This', 'Is', 'a', 'cat'
$array | Write-Host
Output:
This
Is
a
cat
This is a slightly different use case than the OP question. It does not join the array with newlines, but it does give newlines when writing the output.