I am writing small utility scripts in Powershell. While trying to follow the principle of doing one job extremely well per script, I am running into an issue with one script that returns a null value on success. When I try to run the command, and pipe it through a command such as Send-MailMessage, I get an error when it should be successful. I am trying to get Send-MailMessage to only fire if there is output in the body (otherwise, there would be many emails coming in).
Currently, I'm trying to run this from the Powershell command line:
Send-MailMessage -From "powershell#example.com" -To "tech#example.com" -Subject "Disk usage on $($env:COMPUTERNAME)" -SmtpServer "192.168.0.1" -Body (.\diskUsage.ps1 | Out-String) -ErrorAction Continue
The error I'm receiving:
Send-MailMessage : Cannot validate argument on parameter 'Body'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.
At line:1 char:156
+ ... SmtpServer "192.168.0.1" -Body (.\diskUsage.ps1 | Out-String) -ErrorA ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Send-MailMessage], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.SendMailMessag
My goal is to run the command remotely to many computers, and only get email notifications when the disk usage is above a threshold. My script currently returns nothing if the disk space check is OK, and this is my expected behaviour. I need Send-MailMessage, on the command itself, to simply not run if there is nothing to put in the body, without an error appearing in the console that the body is empty.
If specified, the body parameter is going to be verified for not being null or empty. Here is the body part from the open source version of PowerShell:
/// <summary>
/// Specifies the body (content) of the message
/// </summary>
[Parameter(Position = 2)]
[ValidateNotNullOrEmpty]
public String Body
{
get { return _body; }
set
{
_body = value;
}
}
private String _body;
Notice that if no Body is assigned the default value is going to be used which is not defined but rather an implementation detail and in this example null (Send-MailMessage). Therefore not specifying the Body is what you would like to todo.
To ensure clean code you can use PowerShells splatting features or alternativly use simple branching using if statements.
Simple If
$diskUsage = (.\diskUsage.ps1 | Out-String)
If ($diskUsage){
# Disk usage was set
Send-MailMessage -From "powershell#example.com" -To "tech#example.com" -Subject "Disk usage on $($env:COMPUTERNAME)" -SmtpServer "192.168.0.1" -Body $diskUsage -ErrorAction Continue
} Else {
# Disk usage was not set notice that -body is omitted which is different from setting it to null or empty
Send-MailMessage -From "powershell#example.com" -To "tech#example.com" -Subject "Disk usage on $($env:COMPUTERNAME)" -SmtpServer "192.168.0.1" -ErrorAction Continue
}
First, emails are usuallly UTF8 to set the encoding of the body to UTF8 by using -Encoding UTF8
If you are trying to capture the output of diskUsage.ps1 it probably won't return a string like you think.
You can test it by running:
$output = & C:\path\to\diskUsage.ps1 | Out-String
if([String]::IsNullOrEmpty($output)) { echo 'no string is being returned' } else { echo 'String returned' }
If it says no string is being returned, then you should rethink this command into a script.
e.g. put the diskUsage.ps1 and this Send-MailMessage command into one script.
Related
**Problem I am trying to solve: **
We want to be notified whenever one of our kiosk machines restart due to a Windows Update, so we can remotely connect right away and reset them to the desired state.
After trying different options (i.e. send email via SMTP triggered by an event via Task Scheduler, which proved unreliable due to script taking too long before restart occurs), this is the next attempted solution (perhaps it occurs faster?).
**Current solution attempted: **
Write data to a GoogleSheet via PowerShell script (below), triggered via Task Scheduler when a specific event occurs (i.e. Event Id: 1074). From there, GoogleSheet can easily send us the notification we want using Apps Script (details not relevant for this post).
Import-Module UMN-Google
# Set security protocol to TLS 1.2 to avoid TLS errors
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Google API Authozation
$scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
$certPath = "C:/Scripts/GoogleService/my-cert-path.p12"
$iss = 'name#projectname.iam.gserviceaccount.com'
$certPswd = 'mypassword'
try {
$accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss
} catch {
$err = $_.Exception
$err | Select-Object -Property *
"Response: "
$err.Response
}
$accessToken
# Define the GoogleSheet and the target Sheet
$spreadSheetID = 'google-sheet-id-i-omitted-from-this-sample'
$sheetName = 'MachineActivityData'
$EventId = 1074
$A = Get-WinEvent -MaxEvents 1 -FilterHashTable #{Logname = "System" ; ID = $EventId }
$Message = $A.Message
$EventID = $A.Id
$MachineName = $A.MachineName
$Source = $A.ProviderName
# Set the arrayValues
$arrayValues = #($MachineName, $Source, $EventId)
$appendValue = 'Append'
# Write to GoogleSheet (appending data)
Set-GSheetData -accessToken $accessToken -append $appendValue -sheetName $sheetName -spreadSheetID $spreadSheetID -values $arrayValues
Issue encontered:
When running the above script, I get the following error:
Set-GSheetData : A positional parameter cannot be found that accepts argument 'Append'.
At C:\Scripts\script_GoogleSheetConnection.ps1:40 char:1
+ Set-GSheetData -accessToken $accessToken -append $appendValue -sheetN ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Set-GSheetData], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,Set-GSheetData
As you can see, I am using UMN-Google. This documentation helps, but I couldn't find much more than this to help with this error.
This error has something to do with the -append value not being correct.
Per the linked documentation, it appears to be a string (as in the example, and function definition). But I tried other options as well (i.e. bool...).
Any idea why the function Set-GSheetData is not accepting this parameter?
Quick note, I am fairly new to using PowerShell. I appreciate any suggestions.
Thank you kindly,
I tried the above script, and expected data to be appended to the specified GoogleSheet. However, the function Set-GSheetData returned an error, as a parameter is invalid. Per documentation, the parameter I entered is correct.
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 have a batch file that uses PowerShell's Send-MailMessage cmdlet to send emails out with status updates:
powershell -command " & {Send-MailMessage -SMTPServer smtp.somewhere.com -From 'Sender <sender#email.com>' -To 'Recipient <recipient#email.com>' -Subject 'Subject' -Body 'Body' -Attachments 'D:\Logs\Log.log';}"
if %ERRORLEVEL% neq 0 (
echo ERROR - Something went wrong sending the email
goto Error
)
If the Send-MailMessage command fails (e.g. if attachment file not found, or other error), the %ERRORLEVEL% is not raised above 0.
Other answers suggest adding explicit exit codes into a PowerShell script to return a code, but I can't find anything about how to capture an error if using a built in PS cmdlet like Send-MailMessage. Can it be done, or do I need to wrap Send-MailMessage in another PS script?
Wrap the whole command in a try catch block and throw an explicit error. This will return a non-zero exit code.
powershell -command "Try {Send-MailMessage ... -ErrorAction Stop;} Catch { Exit 1 }"
It's a little harder to read in a one line but:
powershell -command "Try {Send-MailMessage -SMTPServer smtp.somewhere.com -From 'Sender <sender#email.com>' -To 'Recipient <recipient#email.com>' -Subject 'Subject' -Body 'Body' -Attachments 'D:\Logs\Log.log' -ErrorAction Stop;} Catch { Exit 1 }"
if %ERRORLEVEL% neq 0 (
echo ERROR - Something went wrong sending the email
goto Error
)
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
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