Sending email with Powershell from csv list - arrays

I have a *.csv file with a few columns and I want to use the UsersTo and UsersCc list of users to send messages.
The problem is with $CcAddress as I have multiple email address once I format them I get the following :
error:
Send-MailMessage : An invalid character was found in the mail header: ','.
At C:\Discovery Scan Process\RSU notifications\Notifications.ps1:43 char:2
Send-MailMessage -Encoding UTF32 -to $ToAddress -Cc $CcAddress -from $FromAddre ...
+ CategoryInfo : InvalidType: (:) [Send-MailMessage], FormatException
+ FullyQualifiedErrorId : FormatException,Microsoft.PowerShell.Commands.SendMailMessage
CSV looks like this:
UsersTo,UsersCc,Domain
unknowTest.Test#test.com,"testOpen#new.com,Pvo.ovi#new.com",test.test
unknowNew.test#test.com,"testOpen#new.com,testOpen#new.com",new.test
unknowprod.mx#test.com,"testOpen#new.com,Evo.x#new.com",prod.test
unknowX.SS#test.com,"testOpen#new.com,Spekor1.#new.com",uat.test
Code:
$notificaitonList = import-csv .\'listOfUsers - Copy.csv'
$domains = 'test.test','prod.test'
foreach ($domain in $domains){
$FromAddress = "some#address"
$ToAddress = ($notificaitonList | Where-Object {$_.domain -like $domain}).UsersTo | foreach {"`"$_`""}
$Ccstr = (($notificaitonList | Where-Object {$_.domain -like "$domain"}).UsersCc).split(",")
$Ccstr = $Ccstr | foreach {"`"$_`""}
[string[]]$CcAddress= $Ccstr.Split(",") | foreach {"`"$_`""}
[string] $MessageSubject = "MessageSubject "
[string] $emailbody = "emailbody"
$SendingServer = "server"
Send-MailMessage -Encoding UTF32 -to $ToAddress -Cc $CcAddress -from $FromAddress -subject $MessageSubject -smtpServer $SendingServer -body $emailbody
}

You can simply split at the comma and let powershell handle the rest. Here is my test that confirmed this.
$tempfile = New-TemporaryFile
#'
UsersTo,UsersCc,Domain
some#email.com,"second#email.com,random#otherdomain.com",test.test
'# | Out-File $tempfile -Encoding utf8
Import-CSV $tempfile | foreach {
$mailparams = #{
SMTPServer = 'myexchangeserver'
From = "noreply#email.com"
To = $_.usersto
CC = $_.userscc -split ','
Subject = 'test email'
Body = 'test body'
}
Send-MailMessage #mailparams
}

Related

PNPUtil retrieve each driver and add to array with PSObject

Using Powershell I retrieve all third party drivers installed using:
$PNPDrivers = PNPUtil /Enum-Drivers
The format of the information retrieved are as followed:
Microsoft PnP Utility
Published Name: oem19.inf
Original Name: apollolakesystem.inf
Provider Name: INTEL
Class Name: Systemenheter
Class GUID: {4d36e97d-e325-11ce-bfc1-08002be10318}
Driver Version: 07/18/1968 10.1.17.1
Signer Name: Microsoft Windows Hardware Compatibility Publisher
Published Name: oem20.inf
Original Name: avotonsystem.inf
Provider Name: INTEL
Class Name: Systemenheter
Class GUID: {4d36e97d-e325-11ce-bfc1-08002be10318}
Driver Version: 07/18/1968 10.1.3.1
Signer Name: Microsoft Windows Hardware Compatibility Publisher
I'm trying to make a foreach loop so that each driver are added to an array with PSObject so that I can fetch required information later on in the script.
I have tried so many combinations but never succeeded.
Right now I have tried a regex expression and group each row in order to specify each value but I don't get any data what so ever.
Below is the Regex expression I have used which seems to work?
After that I've tried to follow the information from this link: Parsing Text with PowerShell
Just changing the information to match my code:
$Pattern = "
^(?<PublishedName>Published Name:.*)
^(?<OriginalName>Original Name:.*)
^(?<ProviderName>Provider Name:.*)
^(?<ClassName>Class Name:.*)
^(?<ClassGUID>Class GUID:.*)
^(?<DriverVersion>Driver Version:.*)
^(?<SignerName>Signer Name:.*)
"
$PNPDrivers |
Select-String -Pattern $Pattern |
Foreach-Object {
# here we access the groups by name instead of by index
$PublishedName, $OriginalName, $ProviderName, $ClassName, $ClassGUID, $DriverVersion, $SignerName = $_.Matches[0].Groups['PublishedName', 'OriginalName', 'ProviderName', 'ClassName', 'ClassGUID', 'DriverVersion', 'SignerName'].Value
[PSCustomObject] #{
PublishedName = $PublishedName
OriginalName = $OriginalName
ProviderName = $ProviderName
ClassName = $ClassName
ClassGUID = $ClassGUID
DriverVersion = $DriverVersion
SignerName = $SignerName
}
}
But no data is printed out so I must be missing some but I don't manage to find what.
How can I do to retrieve the data and for each driver use it?
Second try
The code below gives me all values (I think) but it splits it to one row each.
I guess this is because it doesn't match all rexeg:s at the same time.
Can this be changed?
$Pattern = "(^(?<PublishedName>^Published Name:\s*([^\n\r]*)))|(^(?<OriginalName>^Original Name:\s*([^\n\r]*)))|(^(?<ProviderName>^Provider Name:\s*([^\n\r]*)))|(^(?<ClassName>^Class Name:\s*([^\n\r]*)))|(^(?<ClassGUID>^Class GUID:\s*([^\n\r]*)))|(^(?<DriverVersion>^Driver Version:\s*([^\n\r]*)))|(^(?<SignerName>^Signer Name:\s*([^\n\r]*)))"
$PNPDrivers |
Select-String -Pattern $Pattern -AllMatch |
Foreach-Object {
# here we access the groups by name instead of by index
$PublishedName, $OriginalName, $ProviderName, $ClassName, $ClassGUID, $DriverVersion, $SignerName = $_.Matches[0].Groups['PublishedName', 'OriginalName', 'ProviderName', 'ClassName', 'ClassGUID', 'DriverVersion', 'SignerName'].Value
[PSCustomObject] #{
PublishedName = $PublishedName -replace "Published Name: ",""
OriginalName = $OriginalName -replace "Original Name: ",""
ProviderName = $ProviderName -replace "Provider Name: ",""
ClassName = $ClassName -replace "Class Name: ",""
ClassGUID = $ClassGUID -replace "Class GUID: ",""
DriverVersion = $DriverVersion -replace "Driver Version: ",""
SignerName = $SignerName -replace "Signer Name: ",""
}
}
Third try
Almost there! So close!
You are the man postanote!
Since I am going to loop through a package of drivers in our SCCM enviroment working with [PSCustomObject] seems to be the right way to do it.
Although the problem that persists seems to be that some rows get "out of sync" and displays wrong value for the column.
Any thoughts on how to correct that?
Fourth try
Working code!!
$List = New-Object System.Collections.ArrayList
((PNPUtil /Enum-Drivers |
Select-Object -Skip 2) |
Select-String -Pattern 'Published Name:' -Context 0,7) |
ForEach {
if($PSItem.Context.PostContext[4] -like "*Class Version:*"){
$ClassVersion = $PSItem.Context.PostContext[4] -replace '.*:\s+'
$DriverVersion = $PSItem.Context.PostContext[5] -replace '.*:\s+'
$SignerName = $PSItem.Context.PostContext[6] -replace '.*:\s+'
}else{
$ClassVersion = "N/A"
$DriverVersion = $PSItem.Context.PostContext[4] -replace '.*:\s+'
$SignerName = $PSItem.Context.PostContext[5] -replace '.*:\s+'
}
$y = New-Object PSCustomObject
$y | Add-Member -Membertype NoteProperty -Name PublishedName -value (($PSitem | Select-String -Pattern 'Published Name:' ) -replace '.*:\s+')
$y | Add-Member -Membertype NoteProperty -Name OriginalName -value (($PSItem.Context.PostContext[0]) -replace '.*:\s+')
$y | Add-Member -Membertype NoteProperty -Name ProviderName -value (($PSItem.Context.PostContext[1]) -replace '.*:\s+')
$y | Add-Member -Membertype NoteProperty -Name ClassName -value (($PSItem.Context.PostContext[2]) -replace '.*:\s+')
$y | Add-Member -Membertype NoteProperty -Name ClassGUID -value (($PSItem.Context.PostContext[3]) -replace '.*:\s+')
$y | Add-Member -Membertype NoteProperty -Name ClassVersion -value $ClassVersion
$y | Add-Member -Membertype NoteProperty -Name DriverVersion -value $DriverVersion
$y | Add-Member -Membertype NoteProperty -Name SignerName -value $SignerName
$z = $List.Add($y)
}
Let's say you only what monitors then you can just do this, without using a PSCustomObject at all.
Clear-Host
(PNPUtil /Enum-Drivers /class Display |
Select-Object -Skip 2) |
Select-String -Pattern 'Monitors' -Context 3,4
# Results
<#
Published Name: oem...
Original Name: tp...
Provider Name: L...
> Class Name: Monitors
Class GUID: {4d...
Driver Version: 11...
Signer Name: Micr...
Published Name: oe...
Original Name: tp...
Provider Name: L...
> Class Name: Monitors
Class GUID: {4d36...
Driver Version: 06...
Signer Name: Micros...
#>
# The cherry-pick as needed by index, string, string value, etc..
Clear-Host
((PNPUtil /Enum-Drivers /class Display |
Select-Object -Skip 2) |
Select-String -Pattern 'Monitors' -Context 3,4)[0]
# Results
<#
Published Name: oe...
Original Name: tpl...
Provider Name: L...
> Class Name: Monitors
Class GUID: {4d36e...
Driver Version: 11/...
Signer Name: Microso...
#>
Clear-Host
(PNPUtil /Enum-Drivers /class Display |
Select-Object -Skip 2 |
Select-String -Pattern 'Monitors' -Context 3,4 |
ForEach-Object {$PSItem.Context.PreContext[0]})
# Results
<#
Published Name: oem...
Published Name: oem...
#>
Clear-Host
(PNPUtil /Enum-Drivers /class Display |
Select-Object -Skip 2 |
Select-String -Pattern 'Monitors' -Context 3,4 |
ForEach-Object {$PSItem.Context.PreContext[0] -replace '.*:\s+'})
# Results
<#
oem...
oem...
#>
The above response is from a similar to this one that I and others have already responded to.
Parse pnputil output to published name for specific class
Are you guys in the same course/class, or in the same company doing the same thing to see who can get there first? ;-}
If You want to get just one driver's info at a time, just change that filter string and cherry-pick the line you are after.
Yet, if you really, really, want to use [PSCustomObject], then you can still use the above and refactor to this.
Clear-Host
((PNPUtil /Enum-Drivers /class Display |
Select-Object -Skip 2) |
Select-String -Pattern 'Class Name:' -Context 3,4) |
ForEach {
[PSCustomObject]#{
PublishedName = $PSItem.Context.PreContext[0] -replace '.*:\s+'
OriginalName = $PSItem.Context.PreContext[1] -replace '.*:\s+'
ProviderName = $PSItem.Context.PreContext[2] -replace '.*:\s+'
ClassName = ($PSitem | Select-String -Pattern 'Class Name:') -replace '.*:\s+'
ClassGUID = $PSItem.Context.PostContext[0] -replace '.*:\s+'
DriverVersion = $PSItem.Context.PostContext[1] -replace '.*:\s+'
SignerName = $PSItem.Context.PostContext[2] -replace '.*:\s+'
}
}
# Results
<#
PublishedName : oe...
OriginalName : am...
ProviderName : A...
ClassName : Syst...
ClassGUID : {4d36...
DriverVersion : 02/...
SignerName : Microso...
...
PublishedName : oem...
OriginalName : w...
ProviderName : W...
ClassName : W...
ClassGUID : {849...
DriverVersion : 11/...
SignerName : Microsof...
#>
Or table format:
Clear-Host
((PNPUtil /Enum-Drivers /class Display |
Select-Object -Skip 2) |
Select-String -Pattern 'Class Name:' -Context 3,4) |
ForEach {
[PSCustomObject]#{
PublishedName = $PSItem.Context.PreContext[0] -replace '.*:\s+'
OriginalName = $PSItem.Context.PreContext[1] -replace '.*:\s+'
ProviderName = $PSItem.Context.PreContext[2] -replace '.*:\s+'
ClassName = ($PSitem | Select-String -Pattern 'Class Name:') -replace '.*:\s+'
ClassGUID = $PSItem.Context.PostContext[0] -replace '.*:\s+'
DriverVersion = $PSItem.Context.PostContext[1] -replace '.*:\s+'
SignerName = $PSItem.Context.PostContext[2] -replace '.*:\s+'
}
} |
Format-Table -AutoSize
# Results
<#
PublishedName OriginalName ProviderName ClassName ClassGUID DriverVersion SignerName
------------- ------------ ------------ --------- --------- ------------- ----------
...
#>
Update as per your comment...
sometimes, some drivers apparently also displays a Class Version just
below Class GUID. When it does that row gets out of sync. Is it
possible to, I don't know, using a IF-statement that if PostContext[3]
exists change the order for the PostContext?
... and my response to it.
I played with this use case a bit more can refactored my code to address these potential custom/dynamic fields that can happen per driver.
On my system, I had two customer driver properties:
Extension ID
and
Class Version.
Context-wise, they in the same place, so, I addressed them that way. So, the code pulls the data and uses a custom field to hold the value of either or none. So, this means you need to first discover any such items, and alter the code as needed. Note: I used a Switch block to handle that and IF/then to finalize based on the fields needed in the case.
Clear-Host
# Parse/format PnpUtil output based on JSON details, customize for dynamic fields
$PNPDrivers = PNPUtil /Enum-Drivers
$StringDataRegEx = '.*:\s+'
$RemoveClassNamesRegEx = 'Extension ID|Class Version'
$CSvHeaders = #(
'PublishedName',
'OriginalName',
'ProviderName',
'ClassName',
'ClassGUID',
'ClassVersion_or_ExtensionGUID',
'DriverVersion',
'SignerName'
)
$ClassVersionElements = #(
'Extension ID',
'Class Version',
'Signer Name'
)
$ClassVersionElements |
ForEach {
$ElementString = $PSItem
$ContextID = If ($ElementString -match $RemoveClassNamesRegEx)
{5}
Else {6}
$PNPDrivers |
Select-String -Pattern $ElementString -Context $ContextID |
ForEach {
$DriverData = $PSItem
switch ($ElementString)
{
'Extension ID'
{
[PSCustomObject]#{
PublishedName = $DriverData.Context.PreContext[0] -replace $StringDataRegEx
OriginalName = $DriverData.Context.PreContext[1] -replace $StringDataRegEx
ProviderName = $DriverData.Context.PreContext[2] -replace $StringDataRegEx
ClassName = $DriverData.Context.PreContext[3] -replace $StringDataRegEx
ClassGUID = $DriverData.Context.PreContext[4] -replace $StringDataRegEx
ClassVersion_or_ExtensionID = (
$DriverData |
Select-String -Pattern $ElementString
) -replace $StringDataRegEx
DriverVersion = $DriverData.Context.PostContext[0] -replace $StringDataRegEx
SignerName = $DriverData.Context.PostContext[1] -replace $StringDataRegEx
}
}
'Class Version'
{
[PSCustomObject]#{
PublishedName = $DriverData.Context.PreContext[0] -replace $StringDataRegEx
OriginalName = $DriverData.Context.PreContext[1] -replace $StringDataRegEx
ProviderName = $DriverData.Context.PreContext[2] -replace $StringDataRegEx
ClassName = $DriverData.Context.PreContext[3] -replace $StringDataRegEx
ClassGUID = $DriverData.Context.PreContext[4] -replace $StringDataRegEx
ClassVersion_or_ExtensionID = (
$DriverData |
Select-String -Pattern $ElementString
) -replace $StringDataRegEx
DriverVersion = $DriverData.Context.PostContext[0] -replace $StringDataRegEx
SignerName = $DriverData.Context.PostContext[1] -replace $StringDataRegEx
}
}
'Signer Name'
{
If ($DriverData -NotMatch $RemoveClassNamesRegEx)
{
[PSCustomObject]#{
PublishedName = $DriverData.Context.PreContext[0] -replace $StringDataRegEx
OriginalName = $DriverData.Context.PreContext[1] -replace $StringDataRegEx
ProviderName = $DriverData.Context.PreContext[2] -replace $StringDataRegEx
ClassName = $DriverData.Context.PreContext[3] -replace $StringDataRegEx
ClassGUID = $DriverData.Context.PreContext[4] -replace $StringDataRegEx
ClassVersion_or_ExtensionID = 'Property not valid for this driver'
DriverVersion = $DriverData.Context.PreContext[5] -replace $StringDataRegEx
SignerName = (
$DriverData |
Select-String -Pattern $ElementString
) -replace $StringDataRegEx
}
}
}
}
}
} |
Format-Table -AutoSize

Powershell Invoke-Sqlcmd : Conversion failed when converting date and/or time from character string

I am trying to import a datetime from PowerShell into a SQL Table. It's the only thing stored in that table.
When I run this code: (The value stored in the variable is: 11/19/2020 09:51:40.3244656)
$location = 'path'
Set-Location $location
$c = Get-ChildItem 'path' | Sort { $_.LastWriteTime } | select -last 1 | foreach { $a = $_; $b = Get-Acl $_.FullName; Add-Member -InputObject $b -Name "LastWriteTime" -MemberType NoteProperty -Value $a.LastWriteTime; $b }
$c.LastWriteTime
$date = $c.LastWriteTime
Function Get-Datetime([datetime]$Date = $date ) {
$DT = Get-Date -Date $Date
[string]$DateTime2 = $DT.Month.ToString() + '/'
[string]$DateTime2 += $DT.Day.ToString() + '/'
[string]$DateTime2 += $DT.Year.ToString() + ' '
[string]$DateTime2 += $DT.TimeOfDay.ToString()
return $DateTime2
}
$finaldate = Get-Datetime
#$dateFormatted = $date -format
Invoke-Sqlcmd -ServerInstance "Server" -Database "db" -query "Update [server].[schema].[table] Set [columnName] = '$finaldate'"
I get this error:
Powershell Invoke-Sqlcmd : Conversion failed when converting date and/or time from character string.
How can I get the Powershell command to update the table?
Thank you for your help.
#NekoMusume helped me get to the answer to this specific question. After adding this line, (modified from #NekoMusume's comment): Thanks #NekoMusume! (If you want to answer, I'll mark it for you.)
$finaldate = Get-Date $finaldate -Format "yyyy/MM/dd HH:mm"
The code worked. Final Working code
$location = 'path'
Set-Location $location
$c = Get-ChildItem 'path' | Sort { $_.LastWriteTime } | select -last 1 | foreach { $a = $_; $b = Get-Acl $_.FullName; Add-Member -InputObject $b -Name "LastWriteTime" -MemberType NoteProperty -Value $a.LastWriteTime; $b }
$c.LastWriteTime
$date = $c.LastWriteTime
Function Get-Datetime([datetime]$Date = $date ) {
$DT = Get-Date -Date $Date
[string]$DateTime2 = $DT.Month.ToString() + '/'
[string]$DateTime2 += $DT.Day.ToString() + '/'
[string]$DateTime2 += $DT.Year.ToString() + ' '
[string]$DateTime2 += $DT.TimeOfDay.ToString()
return $DateTime2
}
$finaldate = Get-Datetime
$finaldate = Get-Date $finaldate -Format "yyyy/MM/dd HH:mm"
#$dateFormatted = $date -format
Invoke-Sqlcmd -ServerInstance "Server" -Database "db" -query "Update [server].[schema].[table] Set [columnName] = '$finaldate'"

How can I get a output from a powershell function into a output file for example txt?

I have made a create function in windows powershell:
function create(){
param(
$searchBase = "OU=Customers,DC=test,DC=nl",
$NewOUs = #(Import-csv -Path $txt_csv.Text -Delimiter ";"),
[switch]$ProtectOU
)
$Protect = $true
If ($ProtectOU){$Protect = $true}
<# ------- CREATE OU ------- #>
foreach ($NewOU in $NewOUs) {
try {
New-ADOrganizationalUnit -Name $NewOU.company -Description $NewOU.description -Path $searchBase -ProtectedFromAccidentalDeletion $Protect
}
catch {
Write-Host "OU already exists"
}
}
$UserList = Import-Csv -Path $txt_csv.Text -Delimiter ";"
<# ------- CREATE USERS ------- #>
foreach ($User in $UserList) {
$OU = $User.path
$UPN = $User.UPN
$Password = $User.password
$Detailedname = $User.firstname + " " + $User.Lastname
$UserFirstname = $User.Firstname
$FirstLetterFirstname = $UserFirstname.substring(0,1)
$SAM = $User.UPN
$Company = $User.company
$Description = $User.description
$AccountExpirationDate = $User.accountexpirationdate
$params = #{ 'Name'=$Detailedname;
'SamAccountName'=$SAM;
'UserPrincipalName'=$UPN+'#test.nl';
'DisplayName'=$Detailedname;
'GivenName'=$UserFirstname;
'Surname'=$User.Lastname;
'AccountPassword'=(ConvertTo-SecureString $Password -AsPlainText -Force);
'Enabled'=$True;
'PasswordNeverExpires'=$True;
'Path'=$OU;
'Company'=$Company;
'Description'=$Description;
'AccountExpirationDate'=$AccountExpirationDate
'HomeDrive' = "H:"
'HomeDirectory' = "\\home\userdata$\$SAM"
'ProfilePath'="\\dc-test\User Profiles$\$SAM"
}
New-ADUser #params
}
I want this create function as a output file for example in a txt.
This is my wpf button object:
$button_add.Add_Click({create})
When I click on the button the output must generated a output file. I already tried a lot of solutions such ass:
Create | out-file but I don't get the information I want:
source: https://msdn.microsoft.com/en-us/powershell/reference/5.1/microsoft.powershell.utility/out-file
Is it possible to do this?
Kind regards
You would want to do the following:
"processing New-ADUser with $params" | Out-File log.txt -Append
try {
New-ADUser #params
"Created $UPN" | Out-File log.txt -Append
}
catch {
$_ | Out-File log.txt -Append
}
or you could just pass the output of New-ADUser to the Out-File:
New-ADUser #params -PassThru | Out-File log.txt -Append

Automatically import ICS file to outlook.com

I have an *.ics file and want to import it to my calendar on outlook.com. How can I do this with a powershell script?
I need to either delete and recreate the calendar I import to, or clear the calendar before import.
Thanks in advance.
Try
Step 1: Read the contents of the ics file
Step 2: Parse it
Step 3: Use Outlook Application Object in Powershell
Step 4: Get the Calendar folder
Step 5: use the properties of the calendar folder to add the parsed content in step 2
#Folder containing ICS files
$ICSpath="C:\Users\test\testasdasd"
$ICSlist = get-childitem $ICSPath
Foreach ($i in $ICSlist )
{
$file= $i. fullname
$data = #{}
$content = Get-Content $file -Encoding UTF8
$content |
foreach-Object {
if($_.Contains(':')){
$z=#{ $_.split( ':')[0] =( $_.split( ':')[1]).Trim()}
$data. Add( $z. Keys, $z. Values)
}
}
$outlook = new-object -com Outlook.Application
$calendar = $outlook.Session.GetDefaultFolder(9)
$appt = $calendar.Items.Add(1)
$Body=[regex]::match($content,'(?<=\DESCRIPTION:).+(?=\DTEND:)', "singleline").value .trim ()
$Body= $Body -replace "\r\n\s"
$Body = $Body.replace("\,",",").replace("\n"," ")
$Body= $Body -replace "\s\s"
$Start = ($data.getEnumerator() | ?{ $_.Name -eq "DTSTART"}).Value -replace "T"
$Start = [datetime]::ParseExact ($Start ,"yyyyMMddHHmmss" ,$null )
$End = ($data.getEnumerator() | ?{ $_.Name -eq "DTEND"}).Value -replace "T"
$End = [datetime]::ParseExact ($End ,"yyyyMMddHHmmss" ,$null )
$Subject = ($data.getEnumerator() | ?{ $_.Name -eq "SUMMARY"}).Value
$Location = ($data.getEnumerator() | ?{ $_.Name -eq "LOCATION"}).Value
$appt.Start = $Start
$appt.End = $End
$appt.Subject = $Subject
$appt.Categories = "Presentations" #Pick your own category!
$appt.BusyStatus = 0 # 0=Free
$appt.Location = $Location
$appt.Body = $Body
$appt.ReminderMinutesBeforeStart = 15 #Customize if you want
$appt.Save()
if ($appt.Saved)
{ write-host "Appointment saved."}
Else {write-host "Appointment NOT saved."}
}
Acknowledging "thescriptkeeper.wordpress.com" for the script

Powershell: Missing '=' operator after key in hash literal Error

The code:
$ServerListFile = "D:\Scripts\ServerList.txt"
$ServerList = Get-Content $ServerListFile -ErrorAction SilentlyContinue
$Result = #()
ForEach($computername in $ServerList)
{
$AVGProc = Get-WmiObject -computername $computername win32_processor |
Measure-Object -property LoadPercentage -Average | Select Average
$OS = gwmi -Class win32_operatingsystem -computername $computername |
Select-Object #{Name = "MemoryUsage"; Expression = {“{0:N2}” -f ((($_.TotalVisibleMemorySize - $_.FreePhysicalMemory)*100)/ $_.TotalVisibleMemorySize) }
Get-Eventlog -LogName Security -Newest 5000 | Where-Object {$_.EventID -eq "4624"} | Select-Object #{Name ='Username'; Expression = {$_.ReplacementStrings[1]}}
}
$result += [PSCustomObject] #{
ServerName = "$computername"
CPULoad = "$($AVGProc.Average)%"
MemLoad = "$($OS.MemoryUsage)%"
EventLog = "$username"
}
$Outputreport = "<HTML><TITLE> Server Health Report </TITLE>
<BODY background-color:peachpuff>
<font color =""#99000"" face=""Microsoft Tai le"">
<H2> Server Health Report </H2></font>
<Table border=1 cellpadding=0 cellspacing=0>
<TR bgcolor=gray align=center>
<TD><B>Server Name</B></TD>
<TD><B>Avrg.CPU Utilization</B></TD>
<TD><B>Memory Utilization</B></TD>
<TD><B>Username Event-4624</B></TD></TR>"
Foreach($Entry in $Result)
{
if((($Entry.CpuLoad) -or ($Entry.memload)) -ge 80 )
{
$Outputreport += "<TR bgcolor=red>"
}
else
{
$Outputreport += "<TR>"
}
$Outputreport += "<TD>$($Entry.Servername)</TD><TD align=center>$($Entry.CPULoad)</TD><TD align=center>$($Entry.MemLoad)</TD><TD align=center>$($Entry.EventLog)</TD></TR>"
}
$Outputreport += "</Table></BODY></HTML>"
}
$Outputreport | out-file D:\Scripts\Test.htm
Invoke-Expression D:\Scripts\Test.htm
Produces the following error:
Missing '=' operator after key in hash literal.
At D:\Scripts\AGAIN.PS1:13 char:13
+ Get-Eventlog <<<< -LogName Security -Newest 5000 | Where-Object {$_.EventID -eq "4624"} | Select-Object #{Name ='Username'; Expression = {$_.ReplacementStrings[1]}}
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : MissingEqualsInHashLiteral
missing a } here:
Select-Object #{Name = "MemoryUsage"; Expression = {"{0:N2}" -f ((($_.TotalVisibleMemorySize - $_.FreePhysicalMemory)*100)/ $_.TotalVisibleMemorySize) } }

Resources