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
Related
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)
I really stink at scripting and I need your help. I pieced together this script from several places on the internet and it works, until I enable my IF statement...
I'm just trying to get a count of files of a folder from a UNC path, and if it's over a specified amount, then I want it to send an email letting me know with the current count.
However, if I uncomment the if ($count -gt 50) part, then I won't get an email if the count is over 50.
I don't know how to make the ".Count" a variable for me to use elsewhere in the script. Can someone please help?
Then I'll need to figure out how to run it. Was thinking just a scheduled task in windows and have it run every few minutes or something, but if you have any better ideas, I'd like to hear them!
$FolderList = #(
"\\server\path\test"
)
$Body = ($FolderList | ForEach-Object {
"Check to see if Sweep Service is running, file count for '$($_)': " + (Get-ChildItem -Path $_ -File -ErrorAction SilentlyContinue | Measure-Object).Count
}) -join "`r`n"
#if ($count -gt 50)
#{
$From = "me#you.com"
$To = "me#you.com"
$Subject = "Sweep Checker"
$SmtpServer = "webmail.you.com"
Send-MailMessage -From $From -to $To -Subject $Subject -Body $Body -SmtpServer $SmtpServer
#}
your major problem seems to be NOT saving the file count to any variable. instead, you are saving the value as part of a string - not a number. [grin]
the following code explicitly puts the file count for the current dir into a var, adds that to the total count, and then uses the current count to build your output string for the body of your msg.
$FolderList = #(
$env:TEMP
$env:USERPROFILE
$env:ALLUSERSPROFILE
)
$TriggerFileCount = 20
$TotalFileCount = 0
$Body = foreach ($FL_Item in $FolderList)
{
$CurrentFileCount = #(Get-ChildItem -LiteralPath $FL_Item -File -ErrorAction SilentlyContinue).Count
$TotalFileCount += $CurrentFileCount
# send out to the "$Body" collection
'Check to see if Sweep Service is running, file count for [ {0} ] = {1}' -f $FL_Item, $CurrentFileCount
}
if ($TotalFileCount -gt $TriggerFileCount)
{
$SMM_Params = #{
From = 'NotMe#example.com'
To = 'NotYou#example.com'
Subject = 'Sweep Checker'
Body = $Body -join [System.Environment]::NewLine
SmtpServer = $SmtpServer
}
$SMM_Params
#Send-MailMessage #SMM_Params
}
output ...
Name Value
---- -----
Subject Sweep Checker
From NotMe#example.com
To NotYou#example.com
SmtpServer webmail.you.com
Body Check to see if Sweep Service is running, file count for [ C:\Temp ] = 22...
the full content of the $Body variable ...
Check to see if Sweep Service is running, file count for [ C:\Temp ] = 22
Check to see if Sweep Service is running, file count for [ C:\Users\MyUserName ] = 5
Check to see if Sweep Service is running, file count for [ C:\ProgramData ] = 0
I am trying to create a PowerShell script that will send an email if a service goes into a stopped state. I would like to be able to read the email configuration from another file.
Email configuration file:
.\emailconfig.conf
$emailSmtpServer = "smtp.company.com"
$emailSmtpServerPort = "587"
$emailSmtpUser = "usera"
$emailSmtpPass = "passwordb"
$emailFrom = "userA#company.com"
$emailTo = "userB#company.com"
$emailcc= "userC#company.com"
And this is what I have so far in the PowerShell script:
.\emailservicecheck.ps1
$A = Get-Service "Service B"
if ($A.Status -eq "Stopped") {
Get-Content emailconfig.conf | Out-String
$emailMessage = New-Object System.Net.Mail.MailMessage($emailFrom, $emailTo)
$emailMessage.Cc.Add($emailcc)
$emailMessage.Subject = "subject"
#$emailMessage.IsBodyHtml = $true # true or false depends
$emailMessage.Body = Get-Service "Service B" | Out-String
$SMTPClient = New-Object System.Net.Mail.SmtpClient($emailSmtpServer, $emailSmtpServerPort)
$SMTPClient.EnableSsl = $False
$SMTPClient.Credentials = New-Object System.Net.NetworkCredential($emailSmtpUser, $emailSmtpPass);
$SMTPClient.Send($emailMessage)
}
The script works if I enter the text from the email config file into the script but I cannot seem to be able to read in the data from the file on the fly and get the script to work. It errors out and says that my variables are empty.
What you are searching for, (I think) are .psd1 files. I personally prefer them (along with JSON) over the other configuration formats. The link I'm referring to also describes other well-known formats and how to use them in PowerShell.
In short, module manifests work as follows:
configuration.psd1
#{
SmtpServer = "";
MailFrom = "";
Auth = #{
User = "";
Pass = "";
};
}
Script.ps1
$mailConfig = Import-LocalizedData -BaseDirectory C:\ -FileName configuration.psd1
$emailMessage = New-Object System.Net.Mail.MailMessage( $$mailConfig.mailFrom , $mailConfig.mailTo )
As Mark already pointed out, Get-Content emailconfig.conf | Out-String will just output the content of the file, it won't define the variables in your code. For that you'd need to dot-source the file, which requires a file with the extension ".ps1".
If you want to stick with a simple config file format I'd recommend changing the file to something like this:
emailSmtpServer = smtp.company.com
emailSmtpServerPort = 587
emailSmtpUser = usera
emailSmtpPass = passwordb
emailFrom = userA#company.com
emailTo = userB#company.com
emailcc = userC#company.com
And importing it into a hashtable via ConvertFrom-StringData:
$cfg = Get-Content emailconfig.conf | Out-String | ConvertFrom-StringData
The data in the hashtable can be accessed via dot-notation ($cfg.emailFrom) as well as via the index operator ($cfg['emailFrom']), so your code would have to look somewhat like this:
$msg = New-Object Net.Mail.MailMessage($cfg.emailFrom, $cfg.emailTo)
$msg.Cc.Add($cfg.emailcc)
$msg.Subject = 'subject'
$msg.Body = Get-Service 'Service B' | Out-String
$smtp = New-Object Net.Mail.SmtpClient($cfg.emailSmtpServer, $cfg.emailSmtpServerPort)
$smtp.EnableSsl = $false
$smtp.Credentials = New-Object Net.NetworkCredential($cfg.emailSmtpUser, $cfg.emailSmtpPass)
$smtp.Send($msg)
It looks like what you're trying to do is include some script from another file. This can be done by dot sourcing, however the file needs to be saved as a .ps1 file, you can't use .conf.
You'd do it as follows (in place of your existing Get-Content) line:
. .\emailconfig.ps1
Assuming the file is kept in the current working directory of the script.
Your script wasn't working because
get-content emailconfig.conf | Out-String
Was returning the contents of that file to the output pipeline, rather than including it in the script and executing it.
I'm not sure i understood correctly what you want.
If you want to use variables from external file, you need to dot source your external script, for example, create a file named variables.ps1 and put in the same folder
In the beginning of the main script use
. .\variables.ps1
If you are after expanding variables that are in external file to ues as an email template please do as following:
$HTMLBody = get-content "yourfilepath" | Foreach-Object {$ExecutionContext.InvokeCommand.ExpandString($_)}
This will expand all variables and put it in the $HTMLBody variable
Then use:
$emailMessage.Body = (ConvertTo-Html -body $HTMLBody)
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.
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.