PowerShell Pot Luck May 2024
Here we are again at the end of another month. I hope you learned a few things from the newsletter this month. Don't forget that premium subscribers have access to the full archive of articles, so there's a lot of content to catch up on. If you are a free subscriber, you can upgrade to a premium subscription at any time. Let's dive into this month's pot luck.
Send-MailKitMessage
I have been putting this off forever, but finally bit the bullet and began the process of weaning myself off of Send-MailMessage
. If you read the help for the command Microsoft makes it very clear that the command is obsolete and should not be used. But the command still works and I've built tooling around it. I work at home and am not sending anything sensitive through email, so I felt okay using the cmdlet.
Still, I knew I had to make a change. I knew the MailKit library is widely used but it is not a PowerShell module. So I searched the PowerShell Gallery for MailKit-related modules.
Find-Module -Tag MailKit
The Send-MailKitMessage
module appeared to be the most up-to-date so I first checked out its GitHub repository. I installed the module and starting testing. The module consists of a single function, that shares the same name as the module. The parameters are not that much different from Send-MailMessage
so I was able to make the switch without too much trouble.
Since I tend to use the same parameter values, I updated $PSDefaultParameterValues
with the new command.
$PSDefaultParameterValues["Send-MailKitMessage:From"] = "PSMail@jdhitsolutions.com"
The command can send plaintext or HTML email and you can add file attachments. I tested with my Quote of the day command. Using $PSDefaultParameterValues
means I don't have to specify all the parameters.
Send-MailKitMessage -TextBody (qotd -Format plain) -Subject "Quote of the Day"
I've started the process of modifying my scheduled jobs that email me information to use Send-MailKitMessage
. The module lacks help documentation, but the project's README file has useful examples.
I'd love to hear what you are using as a replacement for Send-MailMessage
.
PowerShell Summit Videos
Long-time readers will know how much I evangelize the PowerShell Summit. If PowerShell is a big part of your day job, this is the event you should be attending. I'm happy to report that all of the session recordings are now available on YouTube. Start your bingeing at https://www.youtube.com/watch?v=e4S7zeeMUOE&list=PLfeA8kIs7CoftSa3hQ9dQseIxdSMBZO_z.
Writing Better PowerShell Code
Last month, I shared a link to my presentation material from a talk I gave to the Research Triangle PowerShell User's Group. The recording of that session is now available on YouTube. You can watch it at https://www.youtube.com/watch?v=WxoO1KJqCxU. As a reminder, session material is at https://gist.github.com/jdhitsolutions/a2f3a246c929a91e494601fa1c44fa55.
SystemInformation.ps1
During my testing of Send-MailKit
message, I went through my script library looking for reporting scripts. I came across an old script that uses the systeminfo.exe tool. The script runs the command but formats the data as CSV. This means I can create a structured object.
systeminfo /fo csv | ConvertFrom-Csv
The script takes the object and creates an HTML report. I decided to polish up the script, add a new feature and share it with you.
#requires -version 5.1
<#
SystemInformation.ps1
turn the SystemInfo command line tool into a PowerShell Tool
Write-Host lines are used for development
#>
Using NameSpace System.Collections.Generic
Param(
[string]$ComputerName = $env:COMPUTERNAME,
[PSCredential]$Credential,
[string]$Path = 'C:\temp\SysInfo.html'
)
If ($IsMacOS -or $IsLinux) {
Write-Warning "This script requires a Windows platform"
#Abort and bail out
Return
}
#these values are used in an HTML footer
$ScriptVersion = '1.5.1'
$ScriptPath = $MyInvocation.MyCommand.Path
Write-Host "Getting system information for $($Computername.ToUpper())" -ForegroundColor Cyan
If ($Credential) {
Write-Host "Using alternate credential for $($credential.Username)" -ForegroundColor Cyan
$info = SystemInfo /S $ComputerName /U $Credential.UserName /P $Credential.GetNetworkCredential().Password /fo csv | ConvertFrom-Csv
}
else {
$info = SystemInfo /S $ComputerName /fo csv | ConvertFrom-Csv
}
#initialize a list for the HTML fragments
$fragments = [List[String]]::new()
$fragments.Add("<h1>$($info.'host name') System Information Report</H1>")
$fragments.AddRange( [string[]]($info | Select-Object * -ExcludeProperty 'Host name', 'Hotfix(s)', 'Network Card(s)' | ConvertTo-Html -Fragment -As List))
#add HotFix data as a separate table
$fragments.Add("<H2>$($info.'Hotfix(s)'.split('.')[0])</H2>")
#convert the hotfix data as an HTML fragment but treat it as an XML document
#so that I can manipulate the data
[xml]$hfData = $info.'Hotfix(s)'.split(',') |
Select-Object -Skip 1 | ForEach-Object {
$s = $_.split(':')
$KB = $s[1].Trim()
#get the hotfix title from the support web page
#do not use basic parsing since that has been removed from PowerShell 7
$uri = "https://support.microsoft.com/kb/$($KB.Substring(2))"
# write-Host $uri
Try {
$web = Invoke-WebRequest -Uri $uri -ErrorAction Stop
if ($web.RawContent) {
[regex]$TitleRx = '(?<=<title>).*(?=<\/title>)'
$Title = $TitleRX.Match($web.RawContent).value
#clearing the variable as a precaution to make sure
#a previous version isn't used.
Clear-Variable -Name web
}
else {
$Title = 'No support document found'
}
}
Catch {
$Title = 'No support document found'
}
[PSCustomObject]@{
KB = $KB
Title = $Title
}
} | ConvertTo-Html -Fragment -As Table
#insert support hyperlinks
For ($i = 1; $i -lt $hfData.table.tr.count; $i++ ) {
$KB = $hfData.table.tr[$i].td[0]
$Title = $hfData.table.tr[$i].td[1]
#Write-Host "$KB - $Title"
if ($Title -ne "No support document found" ) {
#Write-Host "Adding support link for $KB"
#update the text for $hfData.table.tr[$i].td[0] to insert a hyperlink to Microsoft
$tr = $hfData.DocumentElement.GetElementsByTagName('tr')[$i]
$td = $tr.GetElementsByTagName('td')[0]
$td.InnerText = "<a href='https://support.microsoft.com/kb/$KB' target='_blank'>$KB</a>"
}
}
#replace XML characters for <> in the body
$fragments.AddRange([string[]]($hfData.InnerXml.replace('<', '<').Replace('>', '>' )))
#add a footer to the report
$fragments.Add("<p class='footer'>Report run: $(Get-Date) by $($env:Userdomain)\$($env:username)</br>")
$fragments.Add("Script Version: $ScriptVersion</br>")
$fragments.Add("Script Path : $ScriptPath</br>")
$fragments.Add("Script Source : $($env:COMPUTERNAME)</p>")
#The body must be a single string, not an array of strings
$Body = $Fragments | Out-String
#define the HTML header with an embedded style sheet
$head = @'
<title>System Information"</title>
<style>
body {
background-color: #FFFFFF;
font-family: Monospace;
font-size: 12pt;
}
td,th {
border: 0px solid black;
border-collapse: collapse;
white-space: pre;
}
th {
color: white;
background-color: black;
}
table,tr,td,th {
padding: 5px;
margin: 0px;
white-space: pre;
}
tr:nth-child(odd) {
background-color: LightGray
}
table {
margin-left: 25px;
width: 85%
}
h1,h2 {
font-family: Tahoma;
margin-left: 25px;
}
.footer {
color: green;
margin-left: 25px;
font-family: Tahoma;
font-style: italic;
}
</style>
'@
#create the HTML file
ConvertTo-Html -Head $head -Body $Body | Out-File -FilePath $Path
#show the file
Get-Item $Path
I'm not going to spend too much time explaining it because, hopefully, I've commented the code sufficiently. The one thing you might not have seen before is how I handle the HotFix table. I create the HTML fragment, but save the variable as an [XML]
document.
[xml]$hfData = $info.'Hotfix(s)'.split(',') |
Select-Object -Skip 1 | ForEach-Object {
...
} | ConvertTo-Html -Fragment -As Table
This allows me to manipulate the document to insert a hyperlink for the associated support document. I know that historically Microsoft publishes hotfix documentation using the format 'https://support.microsoft.com/kb/Invoke-WebRequest
to retrieve the title of the support document. If the document is found, I update the table cell to include a hyperlink.
For ($i = 1; $i -lt $hfData.table.tr.count; $i++ ) {
$KB = $hfData.table.tr[$i].td[0]
$Title = $hfData.table.tr[$i].td[1]
#Write-Host "$KB - $Title"
if ($Title -ne "No support document found" ) {
#Write-Host "Adding support link for $KB"
#update the text for $hfData.table.tr[$i].td[0] to insert a hyperlink to Microsoft
$tr = $hfData.DocumentElement.GetElementsByTagName('tr')[$i]
$td = $tr.GetElementsByTagName('td')[0]
$td.InnerText = "<a href='https://support.microsoft.com/kb/$KB' target='_blank'>$KB</a>"
}
}
The systeminfo command doesn't understand PowerShell credentials, but I don't want to have plaintext password parameter. So I use the -Credential
parameter to accept a PSCredential object. If the parameter is used, I pass the username and password to the command.
If ($Credential) {
Write-Host "Using alternate credential for $($credential.Username)" -ForegroundColor Cyan
$info = SystemInfo /S $ComputerName /U $Credential.UserName /P $Credential.GetNetworkCredential().Password /fo csv | ConvertFrom-Csv
}
else {
$info = SystemInfo /S $ComputerName /fo csv | ConvertFrom-Csv
}
The script writes the file object to the pipeline so I can run a command like this and open the file in my default browser.
PS C:\> c:\scripts\SystemInformation.ps1 -ComputerName dom1 -Credential $artd -Path d:\temp\dom1.html | Invoke-Item
Sorry for the scrunched size. I wanted to get as much as I could into the report.
If I wanted, I could email the report using Send-MailKitMessage
. Remember, I have $PSDefaultParameterValues
defined for this command.
Send-MailKitMessage -HTMLBody (Get-Content d:\temp\dom1.html -raw) -Subject "SysInfo Report"
The HTML or Text body needs to be a single string, so I use Get-Content -Raw
to read the file as one long string.
I encourage you to look for ways to consume command-line tools and turn them into PowerShell tools. This script is a good example of how you can do this. If you can get CSV or JSON output, you are half-way there!
PowerShell Scripting and Toolmaking
Last month I told you about the update to The PowerShell Practice Primer. This month I'm even happier to tell you about a new version of The PowerShell Scripting and ToolMaking book. I have been wanting to add new content for a long time but could never find the time. I finally had a patch of time I could devote to the project. Here's a short list of what's new:
- Added content on ThreadJobs to
Scripting at Scale
- Added a chapter on writing TUI-based scripts
- Added a chapter on using Crescendo
- Added a chapter on using generic lists and collections
- Added a chapter on scripting with secrets
- Updated author information
- Updated the Pester section with Pester v5 updates
- Updated the WPF scripting chapter
- Updated the
Tools for Toolmaking
chapter - Updated the PowerShell 7 scripting chapter to cover
$PSStyle
The great thing about getting a Leanpub book is that you own it forever, and are entitled to all future updates. At least that's the way Don Jones and I have structured this book. We wanted to write the definitive book on PowerShell scripting and toolmaking. If I think of new things to cover, or if PowerShell 7 adds something new, I'll update the book, and owners can download a new version of the book. The book is over 500 pages and covers a lot of ground. I save a lot of trees with a digital edition.
Go to https://leanpub.com/powershell-scripting-toolmaking to read the table of contents or a free sample.
Wrap-Up
That wraps things up for this month. I never cease to be amazed at how fast the days go by. Remember, if there is a topic you'd like to see me cover or if you have a question, you can leave a comment or email behind@jdhitsolutions.com. Until next time, do good and keep on scripting!