An exercise in playing what you're dealt - Automating emails with PowerShell without being able to run a .ps1 or use a task scheduler.
The other morning, my coworker (kind of jokingly) gave the team a challenge:
POWERSHELL CHALLENGE: Not related to scripts exactly, but I have a recurring reminder to tell myself to send an email to [emailaddress]@[domain].com every Wednesday. The script for this would be easy enough, the problem is that I don't have a method to schedule it other than using Task Scheduler (I think creating a "job" for this might be overkill, let me know if it's not).
The main thought of this is basically that we have limited ways to automate our tasks on our machines without the ability to run apps or scripts and use other tools. The email script that's being run is:
send-mailmessage -from "[emailaddress]@[domain].com" -to "[otheremailaddress]@[domain].com" -subject "Please clean up System Admin Request mailbox" -smtpserver SMTPServer -body "Please clean-up the system admin request inbox, here are the instructions: https://super.long.url/to/the/documentation_that_explained?how/to/do_the_process.defined"
Solutioning:
The approach I decided to use was to create a Powershell shortcut that executed on startup that waited for VPN to connect and then sends the email. This obviously had some limitations, including:
- Character limit of 260
- Runs every day assuming a nightly reboot
As you count the characters (it was longer without me censoring my org's info out) in the Send-MailMessage script, you can already see we're well over 260 characters. We're going to need to be much more efficient with our code if we are going to be able to get this working. A great challenge! And a super silly solution. Let's go.
Finding the folder:
The first thing we need to do is find the folder that's going to host our shortcut. Open your run dialog box from task manager or from the start menu and type in:
shell:startup
This will open a file explorer window where you might see application shortcuts that run when you boot your machine. Right click in this folder and create a new shortcut by going to New > Shortcut. On the prompt window that opens there, you just need to type in:
powershell.exe
Running code in a shortcut:
It's actually shockingly simple to run a quick line of code in a PowerShell or Command shortcut. Since we're using PowerShell, we'll look at this. First, right click on your new shortcut and open the Properties all the way at the bottom of the menu. This should open the "Shortcut" tab and you will see a field called "Target" that should be editable. This text box is where we have the limit of 260 characters. What is in there right now is:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
To run code in the shortcut, we simply need to tell it we're giving it code with the -Command parameter and put our code inside of quotes and start with an ampersand. Doing this makes the shortcut look like:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command "& "
Now you may be saying to yourself, "I thought we were already over our limit and we are at 71 characters before any commands." And you would be right. Now instead of a full 260 characters to work with, we are left with only 189 characters.
Waiting for the VPN Connection (sort of):
So, I know there are many ways to do this, including sending out a ping until we get a response, but since we're not in a hurry to send the message at the beginning of the day we're going to be a bit archaic with our method here. We're just going to kick off the script and immediately wait 10 minutes and assume that you'll connect to VPN in the first 10 minutes of starting your computer. To do this, we will use the Start-Sleep command. We will tell it to wait 600 seconds by passing it the -S seconds parameter. What we are adding will look like:
Start-Sleep -s 600
It's an addition of 18 characters to our original, bringing our total number of characters to just under 90. The shortcut Target now looks like this:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command "& Start-Sleep -s 600"
Determine if it's Wednesday:
To determine if today is Wednesday, the first thing we need to do is determine what day today is. In order to do this we need to run the command Get-Date and pull the day of the week from that value. The shortest way we can do that is the following line of code:
(Get-Date).DayOfWeek
And if we only want to run our code on Wednesdays, we can put this inside of an 'If' statement and use the -eq to indicate that the day of the week equals "Wednesday". The straight forward code for this would be:
If((Get-Date).DayOfWeek -eq 'Wednesday'){}
But since we are crammed for space and Wednesday is a long word, we can shorten this by using a like statement:
If((Get-Date).DayOfWeek -like 'W*'){}
Since there's only one day that starts with a W, we can define that and use the wild card character with the -like comparison, which gets all values that have a W at the beginning. This method saves us 5 characters and only adds 37 characters
Now with waiting 10 minutes for the user to connect to VPN after startup and making sure we run our command only on Wednesdays, our shortcut target looks like:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command "& Start-Sleep -s 600;If((Get-Date).DayOfWeek -like 'W*'){}"
You will notice the semi-colon. This is how we break commands up since we can't use separate lines in this code. We will use this more as we script. If you're keeping a count, our total characters used is up to 127 characters. Which is roughly half of our total allotted characters, and we haven't even gotten to the command that already was over the 260 character max! Now we have to dig in to the original script and see how we're going to get that command to 133 characters or under.
What we absolutely need:
Because we are using the Send-MailMessage, there are a certain number of parameters we absolutely need to include. On top of calling the command, we also must have
- -From
- -SMTPServer
- -To
- -Subject
- -Body
Without any content, that becomes:
Send-MailMessage -From -SMTPServer -To -Subject -Body
For a total of 53 characters. This has no content, but it is a far cry from the 441 characters we had in the original command and it makes the total 180 characters that we can't make any smaller, leaving us 80 characters to get all of our content into the script.
Getting our content into the script:
If we look at the original code, the message body we are sending is over the limit of characters we have left. We need to figure out how to get the details into the script without actually setting values in the script.
Ah-ha! moment:
Looking at what's left, we have 5 different string values we need to fill populate in the script. The structure of this actually fits a common tool used in PowerShell - the CSV!!!
We will try storing our string values in a CSV and pass them in to our Send-MailMessage command for the various parameters.
Setting up the CSV:
Normally setting up a CSV is a cakewalk and you don't need to think twice about it. Because we are up against a character limit, you and I don't have that luxury. We need to make sure we're being as efficient as possible in the setup of this CSV to ensure it takes up as little space as possible in our commands.
First up - our name and file location. My CSV is going to be named M.csv - one letter file name keeps the command short for when I get the CSV in the script. I'm also putting it in C:\AppDev\ instead of a longer file path in order to save space. The full file path of my csv is C:\AppDev\m.csv, which is 15 characters.
Next - headers. Because we have to call each header out at some point in the script, we will want these to be as short as possible as well. My CSV will look like:
- F, for the -From parameter, [emailaddress]@[domain].com
- R, For the -SMTPServer parameter, SMTPServer
- T, for the -To parameter, [otheremailaddress]@[domain].com
- S, for the -Subject parameter, Please clean up System Admin Request mailbox
- B, for the -Body parameter, Please clean-up the system admin request inbox, here are the instructions: https://super.long.url/to/the/documentation_that_explained?how/to/do_the_process.defined
Importing the values from the CSV to PowerShell:
This will control our code. Now we can have seemingly unlimited strings set for each parameter in our CSV and it will not impact our Target length at all. The first thing we need to do is get the values into our code. We do this using the Import-CSV command and call our CSV. For my code the full command is:
Import-CSV C:\appdev\m.csv
This is a 26 character command that pulls in as much data as we need for our Send-MailMessage parameters. This code gives us an "Object" with a bunch of values and if we run it individually, you can see what is returned.
In order to get these values into our Send-MailMessage command, we first need to pipe the result into a Foreach-Object command to let PowerShell know that we want to take all these values and do something with them as a batch. This expands our script to:
Import-CSV C:\appdev\m.csv|Foreach-Object{}
The total number of characters in that command is 43. We now have everything in the script and we have 37 characters left! Hey, we might make it since all we have to do is get our values into the parameters! This is what our script looks like now:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command "& Start-Sleep -s 600;import-csv c:\appdev\m.csv|Foreach-Object{If((Get-Date).DayofWeek -like 'W*'){Send-MailMessage -From -SMTPServer -To -Subject -Body}}"
Passing CSV values from the object to the command parameters:
All we have left to do is grab our Object values and drop them into the parameters. Since we piped in the values from the Import-CSV to our Foreach-Object action, we can call each value by its specific header using the $_ variable for the current object. For example, calling the value for the -From parameter is done by calling the current object variable and specifying the "F" header, like this:
$_.F
So now each of our parameters is 4 characters since our headers are only one character each. If we do the math that adds up to 25 characters (since you need to add a space at the end). And let's see, 37 characters remaining less 25 for the variables is 12 characters left over! Our script now looks like this:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Command "& Start-Sleep -s 600;import-csv c:\appdev\m.csv|Foreach-Object{If((Get-Date).DayofWeek -like 'W*'){Send-MailMessage -From $_.f -SMTPServer $_.r -To $_.t -Subject $_.s -Body $_.b}}"
Not only are we done, but we aren't even sweating the space anymore. We have taken a script, added the run, wait, and day check code, then made the original code more efficient by breaking the strings out into a CSV and calling the CSV into our script instead. We started with a script that was 181 characters over our limit and reduced it to being 12 characters under the limit, and as a side effect, we can now edit the values of our email message simply instead of modifying code!
This was a very silly solution, but I hope you found it educational and perhaps it will make your PowerShell scripting a bit more creative!
Cloud Architect at Redeploy
4 年Interesting, Wish I hade coworkers that could challenged me like this :) Your solution isn't bad but I would like to instead call an entire text file. if that wasn't allowed then just put the entire code into the CSV file that you where allowed to create/read. then I would use the command Import-CSV. Invoke-Command ([Scriptblock]::Create(([System.IO.File]::OpenText('C:\Temp\code.txt')).ReadToEnd())) We use [System.IO.File] as it's about 20% quicker then Get-Content
IT
4 年I don't understand, you're not allowed to use Task Scheduler? Task Scheduler would have been a better fit for this in my opinion.
IT and Security Technician at Arc Automation, Security & Electrical
4 年It's a neat challenge one of the things I noticed is you can compare DayOfWeek to a number (get-date).dayOfWeek -eq 3 to save you a couple more chars