I am working on a PowerShell script which sends emails multiple times with different subject and body each time.
I am trying to move Send-MailMessage into a function or something that I could use to reduce the code lines.
$Sender = 'jones#example.com'
$text = "<html><body>"
$text += "<p>Welcome</p>"
### A cmdlet that would give recipient email address
$Recipient = (Get-Details -user $user).email
$smtp = "server.example.com"
$subject = "welcome email"
Send-MailMessage -BodyAsHtml $text -from $Sender -SmtpServer $smtp -Priority high -to $Recipient -Subject $subject
Write-Output "executing commands to capture results"
Write-Output ""
### Few Commands executed in this step
Write-Output "Analyzing results"
### Few commands executed in this step
$newtext = "<html><body>"
$newtext += "Congrats, you are selected"
$newsubject = "results email"
Send-MailMessage -BodyAsHtml $newtext -from $Sender -SmtpServer $smtp -Priority high -to $Recipient -Subject $subject
You could create a function like this:
Function Send-Email($text,$subject,$recipient)
{
Send-MailMessage -BodyAsHtml $text -From "jones#example.com"
-SmtpServer "server.example.com" -Priority High -To $recipient -Subject $subject
}
You can call it like:
Send-Email -text "Hello" -subject "Test" -recipient "test#example.com"
You can add or remove arguments depending on what will change though. Assuming the smtp server won't change for example, this isn't needed as a parameter.
I am trying to move Send-MailMessage into a function or something that I could use to reduce the code lines.
Writing a function for one line can be useful if the options are many and never change. However you do have one that changes. There is another PowerShell feature that would work here just as well. Splatting!
$emailParameters = #{
From = $Sender
SmtpServer = $smtp
Priority = "high"
To = $Recipient
Subject = $subject
}
Send-MailMessage -BodyAsHtml $text #emailParameters
# ... other code and stuff
Send-MailMessage -BodyAsHtml $newtext #emailParameters
Now you still only have to make changes in one place and the code is arguably more terse.
Another point is that when you are making multi-line strings all at once, as supposed to building over the course of the script you can always use here strings. You only have two lines but if you code evolves over time it is a good tactic to start early instead of many $object += "string" lines
$text = #"
<html><body>
<p>Welcome</p>
"#
Note that indentation is preserved in the resulting here-string. The "# has to appear on its own line with no leading whitespace. Using double quotes means you can still expand variables as well in there.
Related
Testing this for another script Im working on, but having trouble with a new group of files. See below. $Attach2 causes the error while $Attachment1 works fine
$Attachment1 = "c:\temp\I went to the beach-and now too.txt"
$Attach2 = '\\qa-west\e$\orders\15557__45747457-Re_[EXTERNAL]SomeBoxShipmentTestName-JoeSmithers-FileWest-232264_42211_3674745752.msg'
$smtpServer = "mail.somewhare.com"
try{
Send-MailMessage -From 'nobody#somwhare.com' -To 'testdev#somewhare.com' -Subject 'test sub' -Body 'this is body' -SmtpServer $smtpServer -Attachments $Attachment1
}
catch {
# log the error
$ErrorMessage = $PSItem.Exception.Message
#-- test
Write-Host $ErrorMessage
}
Send-MailMessage fails with exception:
Exception:System.IO.FileNotFoundException: Unable to find the
specified file.\r\n at
System.Management.Automation.MshCommandRuntime.ThrowTerminatingError(ErrorRecorderrorRecord)
ErrorDetails:Cannot perform operation because the wildcard path did not resolve to a file FileName $null
I don't think the problem is the $- I think it's the square brackets. Square brackets are wildcards in PowerShell, and Send-MailMessage doesn't support wildcards in -Attachments
Add a backtick (`) in front of the two square brackets in your filename. There's some more details about this issue available here
I'm creating a PS script to automate email blast to 17k users. Our exchange security baseline is set to only accept 60 requests per minute. Because I'm looping through the email list (CSV) line by line (sleep 1 sec), it took hours for my script to complete. What I'm trying to achieve now is to send the email to 100 users per request. I'm figuring out how to store the emails in an array of 100 & send the mail before going for the next 100. Any suggestion?
$recipients = Get-Content "mailinglist.csv"
foreach($rcpt in $recipients)
{
Write-Host "Attempt sending email to $rcpt ..."
Send-MailMessage -ErrorAction SilentlyContinue -ErrorVariable SendError -From $From -to $rcpt -Subject $Subject -SmtpServer $SMTPServer -port $SMTPPort -UseSsl -Credential $Cred -BodyAsHtml ($Body -f $Subject, $Date, $Venue, $Description, $Image)
$ErrorMessage = $SendError.exception.message
If($ErrorMessage)
{
Write-Host "Failure - $ErrorMessage" -ForegroundColor Red
Start-Sleep -Seconds 60
Send-MailMessage -ErrorAction SilentlyContinue -ErrorVariable SendError -From $From -to $rcpt -Subject $Subject -SmtpServer $SMTPServer -port $SMTPPort -UseSsl -Credential $Cred -BodyAsHtml ($Body -f $Subject, $Date, $Venue, $Description, $Image)
}
ElseIf($SendError.exception.message -eq $null)
{
Write-Host "Email has been sent to $rcpt" -ForegroundColor Green
Start-Sleep -Seconds 1
$n++
}
}
Write-Host "Total sent = $n"
You could use a traditional for loop and access your array elements by index.
$recipients = Get-Content "mailinglist.csv"
$To = <SomeValidEmailAddress>
$LastIndex = $recipients.GetUpperBound(0)
for ($i = 0; $i -le $LastIndex; $i+=100) {
$upperRange = [Math]::Min(($i+99),$LastIndex)
$Params = #{
ErrorAction = 'SilentlyContinue'
ErrorVariable = 'SendError'
Bcc = $recipients[$i..$upperRange]
To = $To
From = $From
Subject = $Subject
SmtpServer = $SMTPServer
Port = $SMTPPort
Credential $Cred
Body = $Body -f $Subject, $Date, $Venue, $Description, $Image
BodyAsHTML = $true
UseSsl = $true
}
"Attempt sending email to $($recipients[$i..$upperRange]) ..." # You may want to alter this to be more readable
Send-MailMessage #Params
# Other code
}
Explanation:
I've opted to use Splatting here for readability and manageability with the $Params hash table. It is entirely optional.
The -bcc parameter of Send-MailMessage supports a string array (string[]). Using this over the -To parameter will preserve privacy of the recipients. You can then easily send an email to multiple recipients provided you pass it an array. However, -To is required for Send-Mailmessage to work. It is recommended to make the email address passed into -To something that can be spammed or has a way of handling these types of emails. I have set up the $To variable for you to provide that email address. If privacy is of no concern whatsoever, -Bcc can just be replaced with -To.
Since $recipients is an array, you can access its elements by index, which supports the range operator ... $recipients[0..99] would be the first 100 items in the list.
$LastIndex stores the last index of the list, which is the value returned by the Array.GetUpperBound(Int32) method with dimension 0. Since the array is one-dimensional, 0 is the only dimension.
$upperRange is the beginning index ($i) plus 99. Should $upperRange ever be larger than $LastIndex, it will be set to $LastIndex. Depending on your PowerShell version, the $i+99 and $LastIndex comparison may not be necessary. Accessing an upperbound range beyond the size of the array, will just return all of the remaining elements of the array without throwing an error. This is likely just for completeness.
Question:
I am writing a cmdlet that will accept parameters and send emails. "Cc" is one of the parameters; and is non-mandatory.
Today the code shows TWO lines invoking Send-MailMessage (like the following paragraph), but i am sure there is a better way to write it:
if( $cc -eq $null){
Send-MailMessage -From $from ... ## call without -Cc
} else {
Send-MailMessage -From $from ... -Cc $cc... ## call WITH -Cc
}
I would like to avoid branching and writing the line twice.
Or even worse than "twice", writing all the combinations for each optional parameter.
(Of course, the fact that the cmdlet sends email is not important here. The problem will stand for any cmdlet that needs to avoid optional parameters)
What is the best-practice way to do this?
THANK YOU
Use splatting:
$Params = #{}
if($ShouldUseCc) {
$Params.Add('Cc', $CcValue)
}
if($ShouldUseBcc) {
$Params.Add('Bcc', $BccValue)
}
Send-MailMessage -From $from ... #Params
How can I format my output to PowerShell's script
My output looks like
Hello #{SamAccountName=user1} is locked out
#{SamAccountName=user2} is locked out
My PowerShell code is
if ($users)
{
foreach($user in $users)
{
$message = $message + " " + $user + " is locked out" + "`r`n"
Write-Host $user
}
Send-MailMessage -To $to -Subject "Locked Accounts" -BodyAsHtml $message -From $from -Credential $cred -SmtpServer $server -Debug
}
Thank you
I would like my output to look like
Hello Administrator,
The following accounts are locked as of 10:31 AM on the 13th of April,
2015.
User1 User2
Thank You Automated System
As you can see in my current output, I can't figure out how to get a new line and it outputs #{SamAccountName=user1} instead of user1
Thank you
Here, try this approach instead. If you use a here-string (which is depicted like so:
$message = #"
So
this
keeps
track of spaces?
"#
You can put any amount of text inside and preserve the spacing of the message, while still having the ease of using this in your script by putting any variables inside that you might need.
As you'll see in the finished answer below, the line spacing is preserved. I'm using the $($variableName) format to allow me to pluck out one value of an object from within a bigger string. If I didn't use that format, the whole object would be listed, including all AD properties, which is not what we want.
if ($users)
{
$message = #"
Hello Administrator,
The following accounts are locked as of $((get-date).DateTime).
$($users | select -expand SamAccountName)
Thank You,
Automated System
"#
Send-MailMessage -To $to -Subject "Locked Accounts" -BodyAsHtml $message -From $from -Credential $cred -SmtpServer $server -Debug
}
The message will look like this:
Hello Administrator,
The following accounts are locked as of Monday, April 13, 2015 10:22:30 AM.
localadmin Guest Stephen RDV GRAPHICS SERVICE Jim SCVMM81221tqYYJ stephen.owen krbtgt _svc_sccm azure_adfs
Thank You,
Automated System
You need to directly access the sAMAccountName property on the $user object:
if ($users)
{
foreach($user in $users)
{
$message = $message + " " + $user.SamAccountName + " is locked out" + "`r`n"
}
Send-MailMessage -To $to -Subject "Locked Accounts" -BodyAsHtml $message -From $from -Credential $cred -SmtpServer $server -Debug
}
If you wanted to make something pretty, save the user names to an array first:
$lockedUsers = #()
foreach($user in $users)
{
$lockedUsers += ,$user.SamAccountName
}
Then you can construct your message like:
$message = #"
Hi Admin,
The following accounts are locked as of $(get-date)
$($lockedUsers -join ", ")
Thank you, Automated System
"#
I figured it out. Since the Body is HTML, I used standard HTML tags when forming the strings
<br> = new line
<strong></strong> = bold
<font color='red'></font> = red font
For the account names, I did
$message = $message + " " + $user.SamAccountName.ToLower() + " is locked out" + "<br>"
I have created a script to provision Lync users, important details (such as assigned LineURI) for new provisions need to be emailed. Also any errors need to be sent (fluffed up with some friendly error messages of course :)).
So I created a few CSVs with all relevant data..
Then I created a function:
Function Send-Email ($attachArray) {
# Get a list of to addresses
$toAddresses = "foo#corp.local","bar#corp.local"
# Process replacments
Replace-EmailMasks
# Send conditionaly
Switch ($attachArray) {
$null {
Send-MailMessage -SmtpServer "internalrelay.corp.local" `
-From "test#andylab.local" -To $toAddresses `
-Subject "There should really be something more informative here" `
-BodyAsHTML $SCRIPT:htmlBody
}
Default {
Send-MailMessage -SmtpServer "internalrelay.corp.local" `
-From "test#andylab.local" -To $toAddresses `
-Subject "There should really be something more informative here" `
-BodyAsHTML $SCRIPT:htmlBody
-Attachments $attachArray
}
}
}
Here's how I invoke it:
# Logic, then send
If (($npSuccess -gt 0) -AND ($errorsExist -gt 0)) {
# Attaching both
# Heres the summary paragraph
$SCRIPT:customSummary = '<p>Success and errors :|</p>'
# Now I'm sending it.
Send-Email "$($tempPlace.fullname)\NewProviSsion_Output.csv","$($tempPlace.fullname)\Errors_Output.csv"
} ElseIf ($npSuccess -gt 0) {..} # output-generating Success
ElseIf ($errorsExist -gt 0) {..} # Failed somewhere
Else {..} # no output-generating Success, no overall fails
Now this works; Email looks nice, goes to who it should, files attached etc..
Problem is:
For however many files I specify in $attachArray, that's how many emails get sent. The emails are all exactly the same, going to all the same people n many times.
It's as if i'm doing this:
ForEach ($item in $attachArray) {
Send-Email "$($tempPlace.fullname)\NewProviSsion_Output.csv","$($tempPlace.fullname)\Errors_Output.csv"
}
Except i'm not..
To clarify my objective, I want the email to be sent to all in $toAddresses only once.
Can anyone enlighten me as to what's going on here?
Maybe I've just had a bad Monday morning..
The switch statement fires for each element of the array. This behavior is documented (check Get-Help about_Switch):
If the test value is a collection, such as an array, each item in the collection is evaluated in the order in which it appears.
Use a regular conditional instead (since you have only 2 cases anyway):
if ($attachArray -eq $null) {
Send-MailMessage -SmtpServer "internalrelay.corp.local" `
-From "test#andylab.local" -To $toAddresses `
-Subject "There should really be something more informative here" `
-BodyAsHTML $SCRIPT:htmlBody
} else {
Send-MailMessage -SmtpServer "internalrelay.corp.local" `
-From "test#andylab.local" -To $toAddresses `
-Subject "There should really be something more informative here" `
-BodyAsHTML $SCRIPT:htmlBody
-Attachments $attachArray
}