Skip to content

Instantly share code, notes, and snippets.

@stephanlinke
Last active March 22, 2024 08:29
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save stephanlinke/3f8222c1c12c4da8131303c228cbb290 to your computer and use it in GitHub Desktop.
[Send Custom HTML Notifications] Recently, we changed the way PRTG handles notification emails and simplified the approach so there is only one email template and the option to deliver plain text emails. For most of our customers this will be sufficient. But what about customers that have multiple clients and need customized notifications, langu…
#Requires -Version 4
<#
___ ___ _____ ___
| _ \ _ \_ _/ __|
| _/ / | || (_ |
|_| |_|_\ |_| \___|
HTMLMailNotifier
With this, you can send personalized HTML mails using templates and PRTGs EXE notification
#>
param(
## From
[string] $From = "",
## Sensor ID
[int] $SensorID = 0,
## The recipient of the mail message
[string] $Recipients = "",
## Set this if you want to use a PRTG group as recipient
[string] $PrtgGroup = "",
## The subjecty of the message
[string] $Subject = "",
## The template folder
[string] $TemplateFolder = "",
## The template we'll use
[string] $Template = "",
## Placeholder set
$Placeholders = @(),
## Use HTML or text based template (this will use the .htm instead of the .txt file for the template)
$HTML = $true,
## The priority of the mail
[ValidateSet("Low","Normal","High")][string]$priority = "High",
## Only use primary contact - if false, all mail contacts will receive the notification
[switch] $primaryOnly = $true,
## For the initial password setup, call it with -SetupPasswords
[switch] $SetupPasswords = $false
)
#region configuration
[string] $ConfigurationFilePath = ((Get-ItemProperty -Path "hklm:SOFTWARE\Wow6432Node\Paessler\PRTG Network Monitor\Server\Core" -Name "Datapath").DataPath) + "PRTG Configuration.dat"
[xml] $configuration = New-Object -TypeName XML;
$configuration.Load($ConfigurationFilePath)
## server configuration
[hashtable]$global:servers = @{
smtpFrom = $From
smtpHost = ""
smtpPort = 123
smtpUser = ""
smtpSsl = $true
smtpBackupFrom = $From
smtpBackupHost = ""
smtpBackupPort = 0
smtpBackupUser = ""
smtpBackupSsl = ""
prtgHost = ""
prtgPort = 80
prtgProtocol = "http"
prtgUsername = ""
prtgPasshash = ""
}
## mail configuration
[System.Collections.ArrayList]$global:recipientList = @();
[string] $priority = "high"
## template configuration
[string] $templateBaseDir = "C:\Program Files (x86)\PRTG Network Monitor\webroot\mailtemplates\custom"
[string] $templateDefaultLogo = "C:\Program Files (x86)\PRTG Network Monitor\webroot\images\prtglogo.png"
[string] $Global:graphGUID = [guid]::NewGuid()
[string] $iconFolder = "C:\Program Files (x86)\PRTG Network Monitor\webroot\icons"
[string] $tempFolder = "C:\temp";
[string] $global:stateIcon = "";
## miscallenious
[hashtable]$directories = @{
"templateBaseDir" = "C:\Program Files (x86)\PRTG Network Monitor\webroot\mailtemplates\custom";
"tempFolder" = "C:\temp";
"iconFolder" = "C:\Program Files (x86)\PRTG Network Monitor\webroot\icons";
"templatePath" = ""; }
###########################################################################
# /!\ DON'T CHANGE ANYTHING BELOW - Except you know what you're doing /!\ #
###########################################################################
[hashtable] $stateColors = @{
"1" = "#808282"; #unknown
"3" = "#b4cc38"; #up
"4" = "#ffcb05"; #warning
"5" = "#d71920"; #down
"8" = "#447fc1"; #paused-dependency
"9" = "#447fc1"; #paused-schedule
"10" = "#f99d1c"; #unusual
"11" = "#447fc1"; #paused-license
"12" = "#447fc1"; #paused-until
"13" = "#e77579"; #acknowledged
"14" = "#d71920"; #down-partial
}
[hashtable]$stateIcons = @{
"1" = "led_grey.png"; #unknown
"3" = "led_green.png"; #up
"4" = "led_yellow.png"; #warning
"5" = "led_red.png"; #down
"8" = "led_blue.png"; #paused-dependency
"9" = "led_blue.png"; #paused-schedule
"10" = "led_orange.png"; #unusual
"11" = "led_blue.png"; #paused-license
"12" = "led_blue.png"; #paused-until
"13" = "led_redok.png"; #acknowledged
"14" = "led_redgreen.png";#down-partial
}
## debugging
[switch] $verbose = $true;
#endregion
#region function library
## show messages, nicely formatted.
function Console-ShowMessage([string]$type,$message){
if($verbose){
Write-Host ("[{0}] [" -f (Get-Date)) -NoNewline;
switch ($type){
"done" { Write-Host "done" -ForegroundColor Green -NoNewline;}
"info" { Write-Host "info" -ForegroundColor DarkCyan -NoNewline; }
"warn" { Write-Host "warn" -ForegroundColor DarkYellow -NoNewline; }
"fail" { Write-Host "fail" -ForegroundColor Red -NoNewline; }
default{ Write-Host $type -NoNewline; }
}
Write-Host ("]`t{0}{1}" -f $message,$Global:blank)
}
}
## check the password files for completeness
function Check-PasswordFiles(){
$checklist = @("Primary","Secondary")
foreach($server in $checklist){
$fileCount = (Get-ChildItem "C:\Program Files (x86)\PRTG Network Monitor\Notifications\EXE\*" -Include "*$($server).key","*$($server).pass").Count
if(!($fileCount -eq 2)){
Console-ShowMessage "fail" "The credential set for the $($server) server is missing or incomplete."
Passwords Set $server
}
}
if($SetupPasswords){ Console-ShowMessage "info" "Credentials created successfully."; exit 0; }
if((Get-ChildItem "C:\Program Files (x86)\PRTG Network Monitor\Notifications\EXE\*" -Include "*.key","*.pass").Count -eq 4){
Console-ShowMessage "done" "All credentials found."
}
}
## check the mail server availability
function Check-MailServers(){
begin { Console-ShowMessage "info" "Checking mail server availability..." }
process {
## lets create a new TCP client first
$tcpClient = New-Object System.Net.Sockets.TCPClient
try{
$tcpClient.Connect($global:servers.smtpHost,$global:servers.smtpPort)
Console-ShowMessage "info" "Using primary mail server"
}
catch{
try {
$tcpClient.Connect($global:servers.smtpBackupHost,$global:servers.smtpBackupPort)
Console-ShowMessage "warn" "Primary mail server not responding, using fallback server"
$global:servers.smtpHost = $global:servers.smtpBackupHost
$global:servers.smtpPort = $global:servers.smtpBackupPort
$global:servers.smtpSsl = $global:servers.smtpBackupSsl
$global:servers.smtpUser = $global:servers.smtpBackupUser
}
catch {
Console-ShowMessage "fail" "None of the configured mailservers is responding, please check them for availability."
#exit 1;
}
}
finally { $tcpClient.Close(); }
}
}
function ValidateEmail{
param([string]$address)
($address -as [System.Net.Mail.MailAddress]).Address -eq $address -and $address -ne $null
}
## store the passwords
function Passwords([ValidateSet("Get","Set")][string]$action, [ValidateSet("Primary","Secondary")][string]$Server = ""){
switch($Server)
{
"primary" {
$passFile = "C:\Program Files (x86)\PRTG Network Monitor\Notifications\EXE\HTMLNotifyPrimary.pass";
$keyFile = "C:\Program Files (x86)\PRTG Network Monitor\Notifications\EXE\HTMLNotifyPrimary.key";
}
"secondary" {
$passFile = "C:\Program Files (x86)\PRTG Network Monitor\Notifications\EXE\HTMLNotifySecondary.pass"
$keyFile = "C:\Program Files (x86)\PRTG Network Monitor\Notifications\EXE\HTMLNotifySecondary.key";
}
}
if($action -eq "set"){
$Key = New-Object Byte[] 32
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key)
$Key | out-file $keyFile
Read-Host "Enter Password for the $($Server) server" -AsSecureString | ConvertFrom-SecureString -key $Key | Out-File $passFile
}
if($action -eq "get"){
$Key = Get-Content $KeyFile
$Credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $global:servers.smtpUser, (Get-Content $passFile | ConvertTo-SecureString -Key $key)
return $Credentials;
}
}
## set up the template paths
function Set-TemplatePaths(){
begin { Console-ShowMessage "info" "Setting up template paths..." }
process {
## set up the paths...
if($templateFolder.Length -ne 0)
{ $directories.templatePath = [string]::Format("{0}\{1}\{2}.htm",$templateBaseDir,$templateFolder,$Template) }
else
{ $directories.templatePath = [string]::Format("{0}\{1}.htm",$templateBaseDir,$Template) }
## Check the paths for existance
$directories.Keys | % {
if(!(Test-Path $directories.Item($_)))
{ Write-Host "Template or directory not found. Please make sure all folders and templates exist!" -ForegroundColor Red; exit 2; }
}
## if there's no logo, use the default
if(!(Test-Path -Path ([string]::Format("{0}\{1}\logo.png",$templateBaseDir,$templateFolder))))
{
$global:templateLogo = $templateDefaultLogo
Console-ShowMessage "info" "No customized logo found, using the specified default logo..."
}
else
{
Console-ShowMessage "info" "Customized logo found."
$global:templateLogo = [string]::Format("{0}\{1}\logo.png",$templateBaseDir,$templateFolder)
}
}
}
## this function will return the group ID of the given group
function Get-PrtgGroup( [string]$prtgGroup ){
if($prtgGroup.Length -ne 0){
## retrieve the group id of the corresponding PRTG user group
$group = $configuration.SelectSingleNode([string]::Format('//usergroup[@id][data/name[normalize-space(text())="{0}"]]',$prtgGroup))
## return the id, otherwise error if you found nothing
if($group -ne $null)
{ return $group.id.Trim(); }
else
{ Console-ShowMessage -type "fail" "The specified user group was not found. Please make sure that the name is correct." }
}
}
## this will retrieve the single users from PRTG and create the recipientList
function Get-PrtgUsers([string]$prtgGroup){
begin { Console-ShowMessage -type "info" "Retrieving recipients..." }
process {
if($prtgGroup.Length -ne 0){
Console-ShowMessage -type "info" "Retrieving users of PRTG group $($prtgGroup)"
## lets retrieve the ID of the corresponding group first.
$groupId = (Get-PrtgGroup -prtgGroup $prtgGroup)
## now lets get all active users within that group
$userList = $configuration.SelectNodes([string]::Format('//user[@id][data/primarygroup = {0}][data/active = 1]',$groupId))
$availableUsers = Foreach($user in $userList){
## we only want the primary contacts if PrimaryOnly is true.
if($PrimaryOnly)
{ $contacts = $user.SelectNodes('//user[@id='+$user.id+']/contacts/contact[data/status = 1][data/contacttype = 0][@id = -100]') }
else
{ $contacts = $user.SelectNodes('//user[@id='+$user.id+']/contacts/contact[data/status = 1][data/contacttype = 0]') }
$availableContacts = foreach($contact in $contacts)
{ $recipientList.Add($contact.data.recipient.Trim()) }
}
## create the final recipient list and remove duplicates
$recipients = $recipientList + $recipients.Trim().Split(",") | Select -Unique
}
else
{
$recipients = $recipients.Trim().Split(",") | Select -Unique
}
$correctedRecipients = @();
# remove all recipients that are not valid
foreach($recipient in $recipients){
if(ValidateEmail($recipient))
{ $correctedRecipients += $recipient }
}
}
end {
# stop it if we don't have any recipients
if($correctedRecipients.Length -eq 0) { Console-ShowMessage -type "fail" "No recipients found. Please check the group name and the recipient list for errors. Exiting."; exit 0; }
Console-ShowMessage -type "done" "Found $($userList.count) users in group $($prtgGroup), sending to $($correctedRecipients.count) contacts in total."
return $correctedRecipients;
}
}
## this function will retrieve the latest sensor informations
function Get-SensorLiveInfo([int]$SensorId){
begin { Console-ShowMessage -type "info" "Retrieving information for sensor #$($SensorID)" }
process {
## build the graph URL
$params = @($global:servers.prtgProtocol,$global:servers.prtgHost,$global:servers.prtgPort,$SensorId,$global:servers.prtgUsername,$global:servers.prtgPasshash)
$url = [string]::Format("{0}://{1}:{2}/api/getsensordetails.json?id={3}&username={4}&passhash={5}",$params);
## download the graph and store it with the current guid
$wc = New-Object System.Net.WebClient
$sensorInfo = ($wc.DownloadString($url)) | ConvertFrom-Json;
}
end { return $SensorInfo }
}
## retrieves the live graph from the given sensor id
function Get-SensorLiveGraph([int]$id){
begin { Console-ShowMessage -type "info" "Retrieving current live graph for sensor #$($SensorID)" }
process {
## build the graph URL
$params = @($global:servers.prtgProtocol,$global:servers.prtgHost,$global:servers.prtgPort,600,280,$SensorId,$global:servers.prtgUsername,$global:servers.prtgPasshash)
$url = [string]::Format("{0}://{1}:{2}/chart.png?type=graph&width={3}&height={4}&graphid=0&id={5}&username={6}&passhash={7}",$params);
## download the graph and store it with the current guid
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($url, [string]::Format("{0}\{1}.png",$directories.tempFolder,$Global:GraphGUID))
}
end { Console-ShowMessage -type "done" "Graph downloaded and stored as $($tempFolder)\$($Global:GraphGUID).png" }
}
## sets up the template and replaces the placeholders accordingly.
function Get-Template(){
begin { Console-ShowMessage "info" "Preparing notification mail" }
process {
## retrieve current sensor information
$SensorInfo = (Get-SensorLiveInfo($SensorID))
## set the status icon
$global:stateIcon = $stateIcons[$SensorInfo.sensordata.statusid]
## retrieve the template of the specified content
[string]$body = (Get-Content $directories.templatePath) -join "`n"
## check if the amount of placeholders does match
try
{$body = [string]::Format($body,$placeholders) }
catch
{ Console-ShowMessage "warn" "The amount of placeholders doesn't match with the template. Please make sure that they're equal." }
[hashtable]$placeholderTable = @{
"miscLOGO" = [string]::Format("<img src='{0}' />",(Split-Path -Path $global:templateLogo -Leaf))
"sensorNAME" = $SensorInfo.sensordata.name;
"sensorTYPE" = $SensorInfo.sensordata.sensortype;
"sensorLIVEGRAPH" = [string]::Format("<img src='{0}.png' />",$Global:GraphGUID);
"sensorSTATECOLOR" = $stateColors[$SensorInfo.sensordata.statusid];
"sensorSTATEICON" = [string]::Format("<img src='{0}' />",$global:stateIcon)
"sensorINTERVAL" = $SensorInfo.sensordata.interval;
"sensorPARENTGROUP" = $SensorInfo.sensordata.parentgroupname;
"sensorPARENTDEVICE" = $SensorInfo.sensordata.parentdevicename;
"sensorPARENTDEVICEID" = $SensorInfo.sensordata.parentdeviceid;
"sensorLASTVALUE" = $SensorInfo.sensordata.lastvalue;
"sensorLASTMESSAGE" = $SensorInfo.sensordata.lastmessage;
"sensorSTATUSTEXT" = $SensorInfo.sensordata.statustext;
"sensorSTATUSID" = $SensorInfo.sensordata.statusid;
"sensorLASTUP" = $SensorInfo.sensordata.lastup;
"sensorLASTDOWN" = $SensorInfo.sensordata.lastdown;
"sensorLASTCHECK" = $SensorInfo.sensordata.lastcheck;
"sensorUPTIME" = $SensorInfo.sensordata.uptime;
"sensorUPTIMETIME" = $SensorInfo.sensordata.uptimetime;
"sensorDOWNTIME" = $SensorInfo.sensordata.downtime;
"sensorDOWNTIMETIME" = $SensorInfo.sensordata.downtimetime;
"sensorUPDOWNTOTAL" = $SensorInfo.sensordata.uptimetotal;
"sensorUPDOWNSINCE" = $SensorInfo.sensordata.updownsince;
"sensorURL" = [string]::Format("{0}://{1}:{2}/sensor.htm?id={3}",$global:servers.prtgProtocol,$global:servers.prtgHost,$global:servers.prtgPort,$SensorID);
"deviceURL" = [string]::Format("{0}://{1}:{2}/device.htm?id={3}",$global:servers.prtgProtocol,$global:servers.prtgHost,$global:servers.prtgPort,$SensorInfo.sensordata.parentdeviceid);
}
## replace all existing placeholders within the template
foreach ($placeholder in $placeholderTable.Keys)
{
$body = ($body -replace $placeholder, $placeholderTable.$placeholder)
$subject = ($subject -replace $placeholder, $placeholderTable.$placeholder)
}
[hashtable]$mail = @{
Body = $body;
Subject = $subject;
};
}
end { return $mail }
}
## this function will format the mail and send it to the given recipient(s)
function Send-NotificationMail([hashtable]$mail){
## There will always be a logo and a livegraph available for every template.
## Use logo,livegraph,probeicon,breadcrumb as img src to embed them, like this:
## See the following placeholders to insert them in your mail
[string[]]$images = @(
$global:templateLogo,
[string]::Format("{0}\{1}.png",$directories.tempFolder,$Global:GraphGUID),
[string]::Format("{0}\{1}",$directories.iconFolder,$global:stateIcon)
)
$mailparameters = @{
SmtpServer = $global:servers.smtpHost;
Port = $global:servers.smtpPort;
Credential = (Passwords -Action Get -Server Primary);
From = $global:servers.smtpFrom;
To = Get-PrtgUsers($prtgGroup);
Body = $mail.Body;
Subject = $mail.Subject;
Attachments = $images;
BodyAsHtml = $HTML;
UseSsl = $global:servers.smtpSsl;
Priority = $priority;
}
try{
Send-MailMessage @mailparameters
Console-ShowMessage "info" "Recipient(s) should be notified now."
}
catch { Console-ShowMessage "fail" "The notification could not be sent." }
}
#endregion
function Main(){
begin{
Check-PasswordFiles;
Check-MailServers;
Set-TemplatePaths;
}
process{
Get-SensorLiveGraph;
Send-NotificationMail -mail (Get-Template)
}
end{
Console-ShowMessage "info" "Removing live graph image. Exiting."
Remove-Item ([string]::Format("{0}\{1}.png",$tempFolder,$Global:graphGUID))
}
}
Main;
@stephanlinke
Copy link
Author

Fixed a bug where the server variable was not read correctly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment