powershell - get file, parse it and if "value=" < X then function sendMail - powershell

i have a text file that is automatically downloaded and updated with Task Scheduler.
It looks like this:
C:\PrintLog\GetUsageReportFromPrinterWebService\10.22.17.102:58:<input type="hidden" name="AVAILABELBLACKTONER" value="60">
C:\PrintLog\GetUsageReportFromPrinterWebService\192.167.10.140:58:<input type="hidden" name="AVAILABELBLACKTONER" value="80">
C:\PrintLog\GetUsageReportFromPrinterWebService\192.167.14.128:58:<input type="hidden" name="AVAILABELBLACKTONER" value="80">
I would like to:
delete "C:\PrintLog\GetUsageReportFromPrinterWebService\" and "input type="hidden" name="AVAILIBLETONER""
replace that IP adress with printer name (example 10.51.17.122:58 equals HP-01, 192.168.10.150:58 equals HP-02 etc)
check if "value=" is smaller than 20 and if so than send an email with function sendMail
It doesn't matter what is in that email (after that I will check it manually with webservice anyway).
I just need this as an remainder/alerter that some printer is getting low on toner, so I am not forced to manually check that txt file everyday(I most certainly would forget that :) ). These printers are offsite so I need to know in advance that the printer is low.
Note1: There are empty lines and spaces at the beginning of that txt
Note2: And no, there is no send report via email when low on toner to be configured on those printers (I double checked).
Note3: using C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
I guess point one and two are optional. The third one is important for me, I googled for something similar but just got lost as everyone wanted something little bit different.
Thanks

Here's another possibility (I think it requires Powershell 4 because of Send-Mailmessage, but I could be wrong):
#requires -version 4.0
$logs = Get-Content "F:\scripts\ParseLog\file.log"
$warning = $false
$lowPrinters = ""
# Mail Settings
$Subject = "Low Toner Warning"
$To = "printeradmin#contoso.com"
$From = "admin#contoso.com"
$SMTPServer = "smtp.contoso.com"
$Priority = "High"
$printerList = #{
"10.51.17.122:58" = "HP-01";
"192.168.10.150:58" = "HP-02";
"10.22.17.102:58" = "HP-03";
"192.167.10.140:58" = "HP-04";
}
foreach ( $log in $logs ) {
if ( $log -match '^C:\\PrintLog\\GetUsageReportFromPrinterWebService\\([^:]+:[^:]+):.*value="(.*)">$' ) {
$printer = $Matches[1]
$toner = [int]$Matches[2]
}
if( $toner -lt 20 ) {
$warning = $true
if( $printerList.ContainsKey( $printer ) ) {
$printerName = $printerList[ $printer ]
} else {
$printerName = $printer
}
$lowPrinters += "Printer {0} has a low toner level of {1}.`n" -f $printerName, $toner
}
}
if( $warning ) {
Send-MailMessage -From $From -To $To -Subject $Subject -body $lowPrinters -SmtpServer $SMTPServer
}
Along about line 8 we setup some stuff for sending email. Starting at line 15, we build a hash table mapping printer IPs/Ports with Printer Names (since printer queues aren't always listed in DNS, I decided to use a hash table instead). On line 23, we use a regular expression to grab the ip and port, and the toner value by using the -match operator. Stuff grabbed by the regex is stored in an array called $Matches.

As an example, you could do something like this:
$filePath = "C:\updatedFile.txt"
$prefix = "C:\PrintLog\GetUsageReportFromPrinterWebService\"
$lines = Get-Content $filePath |Where-Object {$_.Trim() -like "$($prefix)*"}
foreach($line in $lines)
{
$content = $line.Trim().Substring($prefix.Length)
$parts = $content -split ":"
$inputML = [xml]"$($parts[2])</input>"
$inputValue = [int]$inputML.input.value
if($inputValue -lt 20)
{
$printer = [System.Net.DNS]::GetHostByAddress($parts[0]).HostName
<#
Your sendMail call in here
#>
}
}
You remove the "C:\PrintLog\GetUsageReportFromPrinterWebService\" part with Substring(), split it into 3 pieces, and then parse to last part as an xml element, giving much easier access to the value attribute
The IP to Printer name part will only work if you have reverse DNS in place already

Related

Sending emails using multiple attachments using powershell script

I have the following variable stored
PS C:>$PathArray
jagbir.singh1990#gmail.com
mr.singh#gmail.com
PS C:>$PathArray2
805775-1.zip
805775-2.zip
$Arrayresult = for ( $i = 0; $i -lt $max; $i++)
{
Write-Verbose "$($PathArray[$i]),$($PathArray2[$i])"
[PSCustomObject]#{
PathArray = $PathArray
PathArray2 = $PathArray2
}
PS C:>$Arrayresult
PathArray PathArray2
--------- ----------
{jagbir.singh1990#gmail.com, mr.singh#gmail.com...} {805775-1.zip, 805775-2.zip...}
{jagbir.singh1990#gmail.com, mr.singh#gmail.com...} {805775-1.zip, 805775-2.zip...}
I want to send email1 with body text containing zip file1 name and email2 with body text containing zip file2 name
Ex:
From : jagbir.singh1990#gmail.com
Body : 805775-1.zip file transfer successful.
No Attachments required
code:
foreach($X in $Arrayresult){
Send-MailMessage -From $X.Patharray -To 'jagbir.singh1990#gmail.com' -Subject 'File Transfer completed successfully' -body 'File $X.PathArray2 transfer success' -smtpServer 'smtp.gmail.com' -Port 465
}
Write-Host "Email sent.."
How can I seperate each email for each zip file
email1 --> file1
email2 --> file2
I'm not sure why you would want to keep the email addresses and filenames in separate arrays and then combine them in an array of PSObjects for this.
Anyway, the first mistake is that you did not specify a value for variable $max, which should be the count for the smallest of the two arrays (I'm using more descriptive variable names for these arrays):
$max = [math]::Min($email.Count, $files.Count)
The second one is that you are adding the complete arrays in the properties of each PSCustomObject you create instead of one element of the input arrays.
Finally, I would suggest using a simpler loop (not creating a new PSObject array), use Splatting for the parameters of Send-MailMessage and add some error checking while sending:
Something like:
# array of email addresses to send TO
$email = 'jagbir.singh1990#gmail.com', 'mr.singh#gmail.com'
# array of filenames to use in the mail body
$files = '805775-1.zip', '805775-2.zip'
# determine the max number of iterations
$max = [math]::Min($email.Count, $files.Count)
# set up a hashtable for splatting the parameters to the Send-MailMessage cmdlet
$mailParams = #{
From = 'me#gmail.com'
Subject = 'File Transfer completed successfully'
SmtpServer = 'smtp.gmail.com'
Port = 465
# other parameters go here
}
for ($i = 0; $i -lt $max; $i++) {
# inside the loop we add/update the parameters 'To' and 'Body'
$mailParams['To'] = $email[$i]
$mailParams['Body'] = "File $($files[$i]) transfer success"
try {
Send-MailMessage #mailParams -ErrorAction Stop
Write-Host "Email sent.."
}
catch {
Write-Error "Email NOT sent.`r`n$($_.Exception.Message)"
}
}

Parsing Large Text Files Eventually Leading to Memory and Performance Issues

I'm attempting to work with large text files (500 MB - 2+ GB) that contain multi line events and sending them out VIA syslog. The script I have so far seems to work well for quite a while, but after a while it's causing ISE (64 bit) to not respond and use up all system memory.
I'm also curious if there's a way to improve the speed as the current script only sends to syslog at about 300 events per second.
Example Data
START--random stuff here
more random stuff on this new line
more stuff and things
START--some random things
additional random things
blah blah
START--data data more data
START--things
blah data
Code
Function SendSyslogEvent {
$Server = '1.1.1.1'
$Message = $global:Event
#0=EMERG 1=Alert 2=CRIT 3=ERR 4=WARNING 5=NOTICE 6=INFO 7=DEBUG
$Severity = '10'
#(16-23)=LOCAL0-LOCAL7
$Facility = '22'
$Hostname= 'ServerSyslogEvents'
# Create a UDP Client Object
$UDPCLient = New-Object System.Net.Sockets.UdpClient
$UDPCLient.Connect($Server, 514)
# Calculate the priority
$Priority = ([int]$Facility * 8) + [int]$Severity
#Time format the SW syslog understands
$Timestamp = Get-Date -Format "MMM dd HH:mm:ss"
# Assemble the full syslog formatted message
$FullSyslogMessage = "<{0}>{1} {2} {3}" -f $Priority, $Timestamp, $Hostname, $Message
# create an ASCII Encoding object
$Encoding = [System.Text.Encoding]::ASCII
# Convert into byte array representation
$ByteSyslogMessage = $Encoding.GetBytes($FullSyslogMessage)
# Send the Message
$UDPCLient.Send($ByteSyslogMessage, $ByteSyslogMessage.Length) | out-null
}
$LogFiles = Get-ChildItem -Path E:\Unzipped\
foreach ($File in $LogFiles){
$EventCount = 0
$global:Event = ''
switch -Regex -File $File.fullname {
'^START--' { #Regex to find events
if ($global:Event) {
# send previous events' lines to syslog
write-host "Send event to syslog........................."
$EventCount ++
SendSyslogEvent
}
# Current line is the start of a new event.
$global:Event = $_
}
default {
# Event-interior line, append it.
$global:Event += [Environment]::NewLine + $_
}
}
# Process last block.
if ($global:Event) {
# send last event's lines to syslog
write-host "Send last event to syslog-------------------------"
$EventCount ++
SendSyslogEvent
}
}
There are a couple of real-bad things in your script, but before we get to that let's have a look at how you can parameterize your syslog function.
Parameterize your functions
Scriptblocks and functions in powershell support optionally typed parameter declarations in the aptly named param-block.
For the purposes if this answer, let's focus exclusively on the only thing that ever changes when you invoke the current function, namely the message. If we turn that into a parameter, we'll end up with a function definition that looks more like this:
function Send-SyslogEvent {
param(
[string]$Message
)
$Server = '1.1.1.1'
$Severity = '10'
$Facility = '22'
# ... rest of the function here
}
(I took the liberty of renaming it to PowerShell's characteristic Verb-Noun command naming convention).
There's a small performance-benefit to using parameters rather than global variables, but the real benefit here is that you're going to end up with clean and correct code, which will save you a headache for the rest.
IDisposable's
.NET is a "managed" runtime, meaning that we don't really need to worry about resource-management (allocating and freeing memory for example), but there are a few cases where we have to manage resources that are external to the runtime - such as network sockets used by an UDPClient object for example :)
Types that depend on these kinds of external resources usually implement the IDisposable interface, and the golden rule here is:
Who-ever creates a new IDisposable object should also dispose of it as soon as possible, preferably at latest when exiting the scope in which it was created.
So, when you create a new instance of UDPClient inside Send-SyslogEvent, you should also ensure that you always call $UDPClient.Dispose() before returning from Send-SyslogEvent. We can do that with a set of try/finally blocks:
function Send-SyslogEvent {
param(
[string]$Message
)
$Server = '1.1.1.1'
$Severity = '10'
$Facility = '22'
$Hostname= 'ServerSyslogEvents'
try{
$UDPCLient = New-Object System.Net.Sockets.UdpClient
$UDPCLient.Connect($Server, 514)
$Priority = ([int]$Facility * 8) + [int]$Severity
$Timestamp = Get-Date -Format "MMM dd HH:mm:ss"
$FullSyslogMessage = "<{0}>{1} {2} {3}" -f $Priority, $Timestamp, $Hostname, $Message
$Encoding = [System.Text.Encoding]::ASCII
$ByteSyslogMessage = $Encoding.GetBytes($FullSyslogMessage)
$UDPCLient.Send($ByteSyslogMessage, $ByteSyslogMessage.Length) | out-null
}
finally {
# this is the important part
if($UDPCLient){
$UDPCLient.Dispose()
}
}
}
Failing to dispose of IDisposable objects is one of the surest way to leak memory and cause resource contention in the operating system you're running on, so this is definitely a must, especially for performance-sensitive or frequently invoked code.
Re-use instances!
Now, I showed above how you should handle disposal of the UDPClient, but another thing you can do is re-use the same client - you'll be connecting to the same syslog host every single time anyway!
function Send-SyslogEvent {
param(
[Parameter(Mandatory = $true)]
[string]$Message,
[Parameter(Mandatory = $false)]
[System.Net.Sockets.UdpClient]$Client
)
$Server = '1.1.1.1'
$Severity = '10'
$Facility = '22'
$Hostname= 'ServerSyslogEvents'
try{
# check if an already connected UDPClient object was passed
if($PSBoundParameters.ContainsKey('Client') -and $Client.Available){
$UDPClient = $Client
$borrowedClient = $true
}
else{
$UDPClient = New-Object System.Net.Sockets.UdpClient
$UDPClient.Connect($Server, 514)
}
$Priority = ([int]$Facility * 8) + [int]$Severity
$Timestamp = Get-Date -Format "MMM dd HH:mm:ss"
$FullSyslogMessage = "<{0}>{1} {2} {3}" -f $Priority, $Timestamp, $Hostname, $Message
$Encoding = [System.Text.Encoding]::ASCII
$ByteSyslogMessage = $Encoding.GetBytes($FullSyslogMessage)
$UDPCLient.Send($ByteSyslogMessage, $ByteSyslogMessage.Length) | out-null
}
finally {
# this is the important part
# if we "borrowed" the client from the caller we won't dispose of it
if($UDPCLient -and -not $borrowedClient){
$UDPCLient.Dispose()
}
}
}
This last modification will allow us to create the UDPClient once and re-use it over and over again:
# ...
$SyslogClient = New-Object System.Net.Sockets.UdpClient
$SyslogClient.Connect($SyslogServer, 514)
foreach($file in $LogFiles)
{
# ... assign the relevant output from the logs to $message, or pass $_ directly:
Send-SyslogEvent -Message $message -Client $SyslogClient
# ...
}
Use a StreamReader instead of a switch!
Finally, if you want to minimize allocations while slurping the files, for example use File.OpenText() to create a StreamReader to read the file line-by-line:
$SyslogClient = New-Object System.Net.Sockets.UdpClient
$SyslogClient.Connect($SyslogServer, 514)
foreach($File in $LogFiles)
{
try{
$reader = [System.IO.File]::OpenText($File.FullName)
$msg = ''
while($null -ne ($line = $reader.ReadLine()))
{
if($line.StartsWith('START--'))
{
if($msg){
Send-SyslogEvent -Message $msg -Client $SyslogClient
}
$msg = $line
}
else
{
$msg = $msg,$line -join [System.Environment]::NewLine
}
}
if($msg){
# last block
Send-SyslogEvent -Message $msg -Client $SyslogClient
}
}
finally{
# Same as with UDPClient, remember to dispose of the reader.
if($reader){
$reader.Dispose()
}
}
}
This is likely going to be faster than the switch, although I doubt you'll see much improvement to the memory foot-print - simply because identical strings are interned in .NET (they're basically cached in a big in-memory pool).
Inspecting types for IDisposable
You can test if an object implements IDisposable with the -is operator:
PS C:\> $reader -is [System.IDisposable]
True
Or using Type.GetInterfaces(), as suggested by the TheIncorrigible1
PS C:\> [System.Net.Sockets.UdpClient].GetInterfaces()
IsPublic IsSerial Name
-------- -------- ----
True False IDisposable
I hope the above helps!
Here's an example of a way to switch over a file one line at a time.
get-content file.log | foreach {
switch -regex ($_) {
'^START--' { "start line is $_"}
default { "line is $_" }
}
}
Actually, I don't think switch -file is a problem. It seems to be optimized not to use too much memory according to "ps powershell" in another window. I tried it with a one gig file.

email to check size / if it's complete

Running the following script:
$FileToCheck = Get-Item -Path $folder/test.zip -ErrorAction SilentlyContinue
$EmailSplat = #{
To = 'business#email.com'
CC = 'admin#email.com'
#SmtpServer = 'smtp.server.net'
From = 'my#email.com'
Priority = 'High'
}
$folder = "C:\test\"
# first condition: 'If the file does not exist, or was not created today, an e-mail should be sent that states "File not created" or similar.'
if ((-not $FileToCheck) -or ($FileToCheck.CreationTime -le (Get-Date).AddDays(-1))) {
$EmailSplat.Subject = 'File not Found or not created today'
$EmailSplat.building = 'This is the email building'
Send-MailMessage #EmailSplat
# second condition 'If the file exists and was created today, but has no content, no e-mail should be sent.'
} elseif (($FileToCheck) -and ($FileToCheck.Length -le 2)) {
#third condition and the default condition if it does not match the other conditions
} else {
$EmailSplat.Subject = 'Active Directory Accounts To Check'
$EmailSplat.building = Get-Content -Path/test.zip //maybe add the file??
Send-MailMessage #EmailSplat
}
Goal: Check if file .zip is complete, once is complete it sends an email to let business that file is good to go. I'm running the script, getting no errors but no alert emails either.
Build on: Add maybe a time that the email could be sent. For example, script will run every morning, at 6:00 the email gets sent to users to notify that the file is complete.
The $Folder variable needs to be on the line before the "$FileToCheck = Get-Item..." line since it uses that variable.
There is no such parameter "building" in the Send-MailMessage cmdlet. I think you are after Body, since you are trying to Get-Content...?
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/send-mailmessage?view=powershell-6
Another note is that Get-Content won't be able to read the contents of a zip file. You would need to unzip the file then read the file or add the file as an attachment.
This is an example of using Get-Content on an zip file that has only a text file in it:
PK
ï¸ÌN_S³ test.txtsdafasdfPK
ï¸ÌN_S³ test.txtPK 6 .
Add $ErrorActionPreference = "Stop" at the top of the script, so errors are shown.
Use the Attachments parameter to add files, building is not a valid parameter of Send-MailMessage
Get-Content is not needed, just add the path to the attachment:
$EmailSplat.Attachments = "Path/test.zip"
So something like this:
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$folder = "C:\test"
$fileToCheck = Get-Item -Path (Join-Path $folder test.zip) -ErrorAction SilentlyContinue
$emailOptions = #{
"To" = "business#email.com"
"CC" = "admin#email.com"
"SmtpServer" = "smtp.server.net"
"From" = "my#email.com"
"Priority" = "High"
}
# first condition: If the file does not exist, or was not created today, an e-mail should be sent that states "File not created" or similar.
if ((-not $fileToCheck) -or ($fileToCheck.CreationTime -le (Get-Date).AddDays(-1)))
{
$emailOptions.Subject = "File not Found or not created today"
Send-MailMessage #emailOptions
}
elseif ($fileToCheck -and ($fileToCheck.Length -le 2))
{
# second condition: If the file exists and was created today, but has no content, no e-mail should be sent.
}
else
{
# third condition and the default condition if it does not match the other conditions
$emailOptions.Subject = "Active Directory Accounts To Check"
$emailOptions.Attachments = $fileToCheck.FullName
Send-MailMessage #emailOptions
}

In EWS/Powershell How Do I Remove Email Addresses From $EmailMessage.ToRecicpients?

Here's what is happening. I have this handy powershell script I've written up to connect to EWS using Impersonation, read plain-text files containing email messages, and send them one at a time in a loop. At the end there is a little Inbox and Sent Items folder clean up. (Kudos to my friends at stackoverflow, as many of the issues I already encountered in writing this have been solved by looking up the answers right here.)
Everything works great except ToRecipients.Add, which works a little _too_well_.
$EmailMessage.ToRecipients.Add($DName)
When the loop iterates to read the next file, the $EmailMessage.ToRecipients array retains it's information after executing
$EmailMessage.SendAndSaveCopy($SentItems.Id).
I had somehow expected a SendAndSaveCopy to discard all information in $EmailMessage, but it does not (Send, Save, and then Discard, right?).
So when sending one file, there is no issue whatsoever. But when sending multiple files, each subsequent file just adds its email addresses on to the end of the $EmailMessage.ToRecipients array. So the second file is sent to the first file's recipients AND the second file's recipients. The third file goes to first, second, and third file recipients. The more files, the bigger the mess.
I've tried various things at the end of the loop, before it iterates, to clear out the ToRecipients, but have not had any luck. Powershell rejects any direct assignment of any kind. I expected there to be a ToRecipients.Clear or .Empty or .Remove or something like that, but I haven't found anything that works. These approaches fail:
Remove-Variable $EmailMessage
Remove-Variable $EmailMessage.ToRecipients
Remove-Variable $EmailMessage.ToRecipients[x] #rejects all counter values
Clear-Variable $EmailMessage
Clear-Variable $EmailMessage.ToRecipients
Clear-Variable $EmailMessage.ToRecipients[x] #rejects all counter values
$EmailMessage.ToRecipients = NULL
$EmailMessage.ToRecipients = ""
I can loop through and read every value, get a count of how many ToRecipients there are, print them out, and all sorts of other nifty things, but it seems to be read-only and I haven't found any way to clear them out before iterating.
I have thought of a couple "band-aid" approaches, which I haven't tried yet:
1. Put it in a function with local variables, and $EmailMessage will poof on the function exit (with any luck).
2. Just process one file, and at the end of the script right before exiting, check to see if any files remain to process, and call the script again using Invoke-Expression.
Really, this seems like overkill, and a work around to the original problem rather than a direct fix. I should just be able to clear out the email addresses before the loop iterates, right?
Here is the full code:
<#
Script Overview
1. Opens EWS services for shared mailbox using an appid
2. Reads files from c:\outbox\ containing email to send, one per file
3. Sends the mail message in each file found
4. Renames each file found
5. Moves sent items into c:\sentemail\
6. Deletes any emails in the Inbox or Sent Items folders that are older than 14 days ($purgebeforedate))
#>
#Variables
## Define UPN of the Account that has impersonation rights
$AccountWithImpersonationRights = "myappid"
$appidpasswd = "mysupersecretpassword"
##Define the SMTP Address of the mailbox to impersonate
$MailboxToImpersonate = "autoemail#mysite.mydomain"
##Define CAS URL (Client Access Server) - can be found in system registry if necessary
$CASURL = "https://my.site.and.domain/ews/exchange.asmx"
$ewsApiDownload = "http://www.microsoft.com/en-us/download/details.aspx?id=35371"
## Define Exchange web services DLL path (requires 2.0 for our environment)
$dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll"
##Define folders for incoming and archived email files
$GetFolder = "c:\outbox\" #folder where input email message files are in plaintext
$SaveFolder = "c:\sentemail\" #folder to move sent email files to after processed
$PurgeBeforeDate = (Get-Date (Get-Date).AddDays(-14) -format G) #format m/d/yyyy hh:mm:ss ap- ::Date is 14 days ago [AddDays(-14)]
##Define location of error log on the local windows server where this script runs
$errorLog = "c:\sentemail\MyAutoSentEmail.log"
# Post a note in the error log that the script has started
$date = Get-Date -format G #Default date and time output used in log file.
Add-Content $errorLog $date": Email Service script started."
Add-Content $errorLog $date": Old sent email delete cutoff - $PurgeBeforeDate"
## Load Exchange web services DLL
Import-Module $dllpath
#Exit script if importing EWS API fails.
if ($? -eq $false)
{
$date = Get-Date -format s #Default date and time output used in log file. (was G before)
Add-Content $errorLog $date": Faied to load EWS, ensure it is installed:"$ewsApiDownload
Add-Content $errorLog $date": EWS API expected location:" $dllpath
Add-Content $errorLog $date": Failed to import the EWS API. Script terminated."
exit
}
## Set Exchange Version (our site requires Exchange2010_SP2)
$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2
## Create Exchange Service Object
$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)
#Get valid Credentials using UPN for the ID that is used to impersonate mailbox
$service.Credentials = New-Object System.Net.NetworkCredential($AccountWithImpersonationRights, $appidpasswd);
## Set the URL of the CAS (Client Access Server)
$service.Url = New-Object Uri($CASURL)
##Login to Mailbox with Impersonation
#Write-Host 'Using ' $AccountWithImpersonationRights ' to Impersonate ' $MailboxToImpersonate
$service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,$MailboxToImpersonate );
#Connect to the Inbox and display basic statistics
$SentFolder = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::SentItems,$ImpersonatedMailboxName)
$SentItems = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$SentFolder)
if ($? -eq $false) #Exit script if binding to folder fails.
{
Add-Content $errorLog $date": Failed to bind to the specified mailbox folder. Script terminated."
exit
}
#For info only, can uncomment to get a list of how many items are in the Inbox and how many are unread
#Write-Host 'Total Item count for Inbox:' $Inbox.TotalCount
#Write-Host 'Total Items Unread:' $Inbox.UnreadCount
#get the filenames in the outgoing-email directory into $files so they may be read one at a time
$files = Get-ChildItem $GetFolder
if ($files.Count -gt 0)
{ #if there are files to process
#Create new email message to send out
$EmailMessage = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage -ArgumentList $service
$EmailMessage.Body = New-Object Microsoft.Exchange.WebServices.Data.MessageBody
$EmailMessage.Body.BodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::HTML
$EmailMessage.From = $MailboxToImpersonate
for ($i=0; $i -lt $files.Count; $i++) { #start for $i loop for each filename found
$emailfile = Get-Content $files[$i].FullName #read file into $emailfile
$To = "empty" #init $To to empty
$Subject = "empty" #init $Subject to empty
$Body = "empty" #init $Body to empty
$Bodytext = "" #init $Bodytext - place to accumulate body lines before adding to email message, gets html return on end of each line
$Bodyhtml = "" #init $Bodyhtml - body lines are placed here in parallel, no html carriage return added
foreach ($Data in $emailfile) { #put each line of the file into $Data and process it
if ( ($Data.StartsWith("to:")) -and ($Subject -eq "empty") )
{ #add recipients to the email message, all to: lines must be before the subject: line
$To = "found" #mark $to line(s) as having been found
Write-Host "TO: line found - " $Data
$Data = $Data.Substring(3) #get all chars after subject:
if ($Data.Contains(";"))
{ #if the email address passed in contains multiples separated by semicolons, split and add them
$DataNames = $Data -split ';' #split $Data into an array of semicolon separated substrings
foreach ($DName in $DataNames) { #validate each semicolon separated email address and add it
$EmailMessage.ToRecipients.Add($DName)
Write-Host $date": TO: address added: "$DName
} #end foreach $DName in $DataNames
} else { #else if no semicolon, just add what you have
$EmailMessage.ToRecipients.Add($Data)
Write-Host $date": TO: address added: "$DName
} #end if email address passed doesn't contain a semicolon
} #end add recipients to $To
if ( ($Data.StartsWith("subject:")) -and ($Body -eq "empty") )
{ #add subject line to the email message, must come before any body: line(s)
$Data = $Data.Substring(8) #get all chars after subject:
$EmailMessage.Subject = $Data
$Subject = "found" #mark $subject line as having been found
Write-Host "SUBJECT: line found - " $Data
} #end add recipients to $To
if ($Data.StartsWith("body:"))
{ #first line of body found
$Data = $Data.Substring(5) #get all chars after subject:
$Bodytext = $Data
$Bodyhtml = $Data
$Body = "found" #mark $body line as having been found
Write-Host "BODY: line found - " $Data
} #end add recipients to $To
elseif ($Body -eq "found")
{ #accumulate the remaining lines into the $bodytext string with '<br />' as carriage returns between lines
$Bodytext = -join($Bodytext, '<br />', $Data); #text lines get an html carriage return
$Bodyhtml = -join($Bodyhtml, $Data); #html lines don't get an html carriage return added, shouldnt need it
Write-Host "body data found - " $Data
} #end accumulate the remaining lines into the $bodytext string with '<br />' as carriage returns between lines
} #end foreach - done processing this text file
$CurrentDateAndTime = $(get-date -f yyyy-MM-dd-HH-mm-ss) #date format to add to the front of the filename when moving/renaming
$OldFileName = $files[$i].FullName
$NewFileName = [io.path]::GetFileName($OldFileName) #just the filename without the path for rename later
Write-Host "OldFileName - " $OldFileName
if ( ($To -eq "found") -and ($Subject -eq "found") -and ($Body -eq "found") )
{ #if all parts found, add body text to the new email message and send it off
if ( ( $Bodyhtml -Match "<" ) -and ( $Bodyhtml -Match ">" ) ) {
$EmailMessage.Body.Text = $Bodyhtml } else {
$EmailMessage.Body.Text = $Bodytext
} #end if html set body.text to html, otherwise set to bodytext with '<br />' crlf's added
$EmailMessage.SendAndSaveCopy($SentItems.Id)
Add-Content $files[$i].FullName "EMAIL-SENT-AT: $CurrentDateAndTime"
Add-Content $errorLog $date": Email File Sent Successfully:"$OldFileName
$NewFileName = -join($CurrentDateAndTime, "-$i-", $NewFileName)
} Else { #add error messages to output file, and select a name that includes NOTSENT so it is obvious
Add-Content $errorLog $date": NOTSENT-ERROR in file: "$OldFileName
if ( $To -eq "empty") {
Add-Content $files[$i].FullName "NOTSENT-ERROR: NO TO: LINE FOUND"
Add-Content $errorLog $date": NOTSENT-ERROR: NO TO: LINE FOUND"
}
if ( $Subject -eq "empty") {
Add-Content $files[$i].FullName "NOTSENT-ERROR: NO SUBJECT: LINE FOUND"
Add-Content $errorLog $date": NOTSENT-ERROR: NO SUBJECT: LINE FOUND"
}
if ( $Body -eq "empty") {
Add-Content $files[$i].FullName "NOTSENT-ERROR: NO BODY: LINE FOUND"
Add-Content $errorLog $date": NOTSENT-ERROR: NO BODY: LINE FOUND"
}
$EmailMessage.Delete #delete new composition if it is not set up properly
$NewFileName = -join($CurrentDateAndTime, "-$i-NOTSENT-", $NewFileName) #note in filename if email was not sent
} #end send-or-not-to-send if
Write-Host "NewFileName - " $NewFileName
#rename and move the file with the date and time it was sent (or tried to be sent)
$NewFileName = -join($SaveFolder, $NewFileName)
Move-Item $OldFileName $NewFileName
$EmailMessage.ToRecipients.Clear() ##BenH - clears ToRecipients before next file is processed so they don't accumulate
} #end for $i loop for each filename found
} #end if there are files to process
if ($? -eq $false)
{
Add-Content error.log $date": error sending message. Script terminated."
exit
}
#Delete items from the Sent Items folder that are too old
#Get the ID of the folder to move to, by searching up from the mailbox root folder
$RootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot)
$fvFolderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1)
$fvFolderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep
$SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName,"Sent Items")
$findFolderResults = $RootFolder.FindFolders($SfSearchFilter,$fvFolderView) #search from mailbox root folder for a folder named Sent Items
if ($? -eq $false)
{
#Write-Host 'Unable to locate destination folder:' $findFolderResults
Add-Content error.log $date": Unable to locate the Sent Items folder for cleanup. Script terminated."
exit
}
$sentitemsfolder = $findFolderResults.Folders[0] #save reference to the Sent Items folder in $sentitemsfolder
$SfSearchFilterI = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName,"Inbox")
$findFolderResults = $RootFolder.FindFolders($SfSearchFilterI,$fvFolderView) #search from mailbox root folder for a folder named Inbox
if ($? -eq $false)
{
#Write-Host 'Unable to locate destination folder:' $findFolderResults
Add-Content error.log $date": Unable to locate the Inbox folder for cleanup. Script terminated."
exit
}
$inboxfolder = $findFolderResults.Folders[0] #save reference to the Inbox folder in $inboxfolder
#search through the Sent Items and Inbox folders for items older than $purgebeforedate and soft delete them
$puItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(500, 0, [Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
$puItemView.PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly, [Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject, [Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived)
$puItemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow
#Sent Items scan for old items to delete
$puItems = $null #init FindItems results to null before executing find
do
{ #start do
$puItems = $service.FindItems($sentitemsfolder.Id,$puItemView) #find all items in Sent Items
if ($puItems.Items.Count -gt 0)
{ #if Sent Items folder not empty, inspect all items found
foreach($Item in $puItems.Items)
{
if ($item.datetimereceived -le $PurgeBeforeDate) #compare email date to purge cutoff date
{
#Write-Host "Delete Item with date time received" $item.DateTimeReceived
[void]$item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::SoftDelete)
#Note: exchange email internal date is in format of : 5/31/2016 8:02:19 AM (get-date -format G)
} #end compare email date to purge cutoff date
} #end foreach $Item in $puItems.items
} #end if Sent Items folder not empty
$puItemView.Offset += $puItems.Items.Count
} #end do
while($puItems.MoreAvailable -eq $true)
#Inbox scan for old items to delete
$puItems = $null #init FindItems results to null before executing find
do
{ #start do
$puItems = $service.FindItems($inboxfolder.Id,$puItemView) #find all items in Sent Items
if ($puItems.Items.Count -gt 0)
{ #if Sent Items folder not empty, inspect all items found
foreach($Item in $puItems.Items)
{
if ($item.datetimereceived -le $PurgeBeforeDate) #compare email date to purge cutoff date
{
#Write-Host "Delete Item with date time received" $item.DateTimeReceived
[void]$item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::SoftDelete)
#Note: exchange email internal date is in format of : 5/31/2016 8:02:19 AM (get-date -format G)
} #end compare email date to purge cutoff date
} #end foreach $Item in $puItems.items
} #end if Sent Items folder not empty
$puItemView.Offset += $puItems.Items.Count
} #end do
while($puItems.MoreAvailable -eq $true)
#script done
Add-Content $errorLog $date": Email Service script finished."
exit
##############################################################################
And here are some sample email input files, where lines start with to:, subject:, and the first line of the message body starts with body:
Sample File 1:
to:sam.lemon#lemonade.org
subject:ews mail test
body:testing line 1
testing line 2
testing line 3
testing line 4
done testing
Sample File 2:
to:bill.hickock#wild.west.org
subject:ews mail test
body:testing line 1
testing line 2
testing line 3
testing line 4
done testing
With these two sample files, the first one would be sent to sam.lemon#lemonade.org as expected. But the second one would go to both sam.lemon#lemonade.org and bill.hickock#wild.west.org, which it should not, since the second file is only for bill.hickock#wild.west.org.
Thanks in advance!
Zistrosk
#BenH - Here's what I coded and what came out in white on the Powershell ISE console. No errors or warnings, just these info messages, and the email addressees were not cleared or removed, same symptoms of the problem persisted. Does my coding look right?
$EmailMessage.ToRecipients.Clear
MemberType : Method
OverloadDefinitions : {void Clear()}
TypeNameOfValue : System.Management.Automation.PSMethod
Value : void Clear()
Name : Clear
IsInstance : True
$EmailMessage.ToRecipients.Remove
MemberType : Method
OverloadDefinitions : {bool
Remove(Microsoft.Exchange.WebServices.Data.EmailAddress
emailAddress)}
TypeNameOfValue : System.Management.Automation.PSMethod
Value : bool
Remove(Microsoft.Exchange.WebServices.Data.EmailAddress
emailAddress)
Name : Remove
IsInstance : True
Note: added the following line above, thanks to #BenH, which fixes the issue and clears the ToRecipients array before the loop iterates and the next file is read, preventing an unwanted accumulation of TO addresses:
$EmailMessage.ToRecipients.Clear() ##BenH - clears ToRecipients before next file is processed so they don't accumulate

Expressions are only allowed as the first element of a pipeline

I'm new at writing in powershell but this is what I'm trying to accomplish.
I want to compare the dates of the two excel files to determine if one is newer than the other.
I want to convert a file from csv to xls on a computer that doesn't have excel. Only if the statement above is true, the initial xls file was copied already.
I want to copy the newly converted xls file to another location
If the file is already open it will fail to copy so I want to send out an email alert on success or failure of this operation.
Here is the script that I'm having issues with. The error is "Expressions are only allowed as the first element of a pipeline." I know it's to do with the email operation but I'm at a loss as to how to write this out manually with all those variables included. There are probably more errors but I'm not seeing them now. Thanks for any help, I appreciate it!
$CSV = "C:filename.csv"
$LocalXLS = "C:\filename.xls"
$RemoteXLS = "D:\filename.xls"
$LocalDate = (Get-Item $LocalXLS).LASTWRITETIME
$RemoteDate = (Get-Item $RemoteXLS).LASTWRITETIME
$convert = "D:\CSV Converter\csvcnv.exe"
if ($LocalDate -eq $RemoteDate) {break}
else {
& $convert $CSV $LocalXLS
$FromAddress = "email#address.com"
$ToAddress = "email#address.com"
$MessageSubject = "vague subject"
$SendingServer = "mail.mail.com"
$SMTPMessage = New-Object System.Net.Mail.MailMessage $FromAddress, $ToAddress, $MessageSubject, $MessageBody
$SMTPClient = New-Object System.Net.Mail.SMTPClient $SendingServer
$SendEmailSuccess = $MessageBody = "The copy completed successfully!" | New-Object System.Net.Mail.SMTPClient mail.mail.com $SMTPMessage
$RenamedXLS = {$_.BaseName+(Get-Date -f yyyy-MM-dd)+$_.Extension}
Rename-Item -path $RemoteXLS -newname $RenamedXLS -force -erroraction silentlycontinue
If (!$error)
{ $SendEmailSuccess | copy-item $LocalXLS -destination $RemoteXLS -force }
Else
{$MessageBody = "The copy failed, please make sure the file is closed." | $SMTPClient.Send($SMTPMessage)}
}
You get this error when you are trying to execute an independent block of code from within a pipeline chain.
Just as a different example, imagine this code using jQuery:
$("div").not(".main").console.log(this)
Each dot (.) will chain the array into the next function. In the above function this breaks with console because it's not meant to have any values piped in. If we want to break from our chaining to execute some code (perhaps on objects in the chain - we can do so with each like this:
$("div").not(".main").each(function() {console.log(this)})
The solution is powershell is identical. If you want to run a script against each item in your chain individually, you can use ForEach-Object or it's alias (%).
Imagine you have the following function in Powershell:
$settings | ?{$_.Key -eq 'Environment' } | $_.Value = "Prod"
The last line cannot be executed because it is a script, but we can fix that with ForEach like this:
$settings | ?{$_.Key -eq 'Environment' } | %{ $_.Value = "Prod" }
This error basically happens when you use an expression on the receiving side of the pipeline when it cannot receive the objects from the pipeline.
You would get the error if you do something like this:
$a="test" | $a
or even this:
"test" | $a
I don't know why are trying to pipe everywhere. I would recommend you to learn basics about Powershell pipelining. You are approaching it wrong. Also, I think you can refer to the link below to see how to send mail, should be straight forward without the complications that you have added with the pipes : http://www.searchmarked.com/windows/how-to-send-an-email-using-a-windows-powershell-script.php