Email Password Expiry Reminder to Multiple Users - powershell

So, I we have a few users that their computers are not on the domain. One of the annoying things about that is windows will not notify them that their domain password is expired obviously. So I decided I was going to put together a little script using powershell in windows that checks AD to see when their password expires and then if it's about to expire in 3 days to send the user an email to notify them that they should change their password.
I have it set up right now to look at the users distinguished name to pull all the necessary information. but I can only do that for one person, I need to look at two user's distinguished names and send each of them an email when their password is about to expire. I tried creating another $DN variable that I could put the other Distinguished name into and put get-aduser -searchbase $DN, $DN2 but that didn't work for me. Probably was a dumb thing to try, but not sure the syntax needed to accomplish this. Below is my code.
$smtpServer="smtp.office365.com" # Office 365 official smtp server
$expireindays = 100 # number of days for password to expire
$from = # email from
#$logging = "$true" # Set to Disabled to Disable Logging
$logFile = "c:\Scripts\PasswordChangeNotification.csv" # ie. c:\Scripts\PasswordChangeNotification.csv
#$testing = "Disabled" # Set to Disabled to Email Users
$testRecipient =
$date = Get-Date -format ddMMyyyy
$DN = "Distinguished name here"
# Add EMAIL Function
Function EMAIL{
Param(
$emailSmtpServer = $smtpServer, #change to your SMTP server
$emailSmtpServerPort = 587,
$emailSmtpUser = "User"
$emailSmtpPass = "Password", #Password for Send from email account
$emailFrom = "email#domain.com", #Email account you want to send from
$emailTo,
$emailAttachment,
$emailSubject,
$emailBody
)
Process{
$emailMessage = New-Object System.Net.Mail.MailMessage( $emailFrom , $emailTo )
$emailMessage.Subject = $emailSubject
$emailMessage.IsBodyHtml = $true
$emailMessage.Priority = [System.Net.Mail.MailPriority]::High
$emailMessage.Body = $emailBody
$SMTPClient = New-Object System.Net.Mail.SmtpClient( $emailSmtpServer , $emailSmtpServerPort )
$SMTPClient.EnableSsl = $true
$SMTPClient.Credentials = New-Object System.Net.NetworkCredential( $emailSmtpUser , $emailSmtpPass );
$SMTPClient.Send( $emailMessage )
}
}
# Get Users From AD who are Enabled, Passwords Expire and are Not Currently Expired
Import-Module ActiveDirectory
$users = get-aduser -SearchBase $DN -filter * -properties Name, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress |where {$_.Enabled -eq "True"} | where { $_.PasswordNeverExpires -eq $false } | where { $_.passwordexpired -eq $false }
$DefaultmaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
# Process Each User for Password Expiry
foreach ($user in $users)
{
$Name = $user.Name
$emailaddress = $user.emailaddress
$passwordSetDate = $user.PasswordLastSet
$PasswordPol = (Get-AduserResultantPasswordPolicy $user)
# Check for Fine Grained Password
if (($PasswordPol) -ne $null)
{
$maxPasswordAge = ($PasswordPol).MaxPasswordAge
}
else
{
# No FGP set to Domain Default
$maxPasswordAge = $DefaultmaxPasswordAge
}
$expireson = $passwordsetdate + $maxPasswordAge
$today = (get-date)
$daystoexpire = (New-TimeSpan -Start $today -End $Expireson).Days
# Set Greeting based on Number of Days to Expiry.
# Check Number of Days to Expiry
$messageDays = $daystoexpire
if (($messageDays) -ge "1")
{
$messageDays = "in " + "$daystoexpire" + " days."
}
else
{
$messageDays = "today."
}
# Email Subject Set Here
$subject="Your password will expire $messageDays"
# Email Body Set Here, Note You can use HTML, including Images.
$body ="
<p>Dear $name,<br></P><br>
<p>Your domain password will expire $messageDays<br><br>
Please change your password before it expires.<br></P><br><br>
<p>Thanks, <br>
} # End Send Message
} # End User Processing
# End
I am just trying to get some insight on how I could modify my code to use two Distinguished names instead of just the one. I'm sure this isn't the best way to do this, but I'm not too good with coding yet. Hopefully this all makes sense, I appreciate the help!

As you have discovered, you can store the DN values in an array $DNs and process each element of the array. The two expressions inside of the parentheses differ by only the $DN variables that you supply. Using a Foreach loop slightly performs better than piping to ForEach-Object, but in your case it will be negligible.
$users = Foreach ($DN in $DNs) {
get-aduser -SearchBase $DN -filter {
Enabled -eq "True" -and
PasswordNeverExpires -eq "False" -and
passwordexpired -eq "False"
} -properties Name, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress)
There are added benefits of doing it this way:
Removal of the Where-Object: Get-ADUser has its own filter as a parameter that can dramatically increase performance over using where in certain queries. It should be faster for you here as the number of returned users from the Get-ADUser query increases.

Got it!!
I changed $DN to:
$DN = "Distinguished name","Distinguished name"
then changed my get-aduser code to:
$users= $DN | ForEach-Objects {get-aduser -SearchBase $PSItem -filter * .....
thanks,

Related

How would I send an email to myself if the user's manager field is empty in AD?

Hello I am new to PowerShell and I am trying to figure out how to send emails to user's manager. In the script below I am able to send the notification if in AD ,manager field is not empty, but when is black I get an error message. How do I make it where it sends an email to myself, if the user's manager is blank? For some reason when I run this code, I keep getting line 233 manager email is null.
code is located here:
https://stackoverflow.com/a/69731370/18947133
Import-Module ActiveDirectory
$mailprops = #{
from = "test#domainname.com
smtpserver = "server.domain.com"
bodayashtml =$true
}
$startDate = Get-Date
$enddate= $startdate.addDays(x)
$props = #{ filer = "Account expirationDate -gt '$startdate' - and AccountExpiratioDate -lt '$endDate'"
Properties ='Accountexpirationdate', 'Manager' }
$Users = Get-ADuser #props
$bodyScriptBlock = {
# Group all users by their Manager
$Users | Group-Object Manager | ForEach-Object {
# Get this Manager's email address
$managerEmail = (Get-ADUser $_.Name -Properties EmailAddress).EmailAddress
# Create a string[] with the user's Name and their Account's Expiration Date
$userList = foreach($user in $_.Group)
{
'- {0} expires on {1}' -f $user.Name, $user.AccountExpirationDate
}
# Execute the Body scriptblock passing this user's list
$body = & $bodyScriptBlock -UserList $userList
# Set the remaing values for Mail props
$mailProps.To = $managerEmail
$mailProps.Subject = "Account Expiration Notification for Managees" # ??? No idea what to put here
$mailProps.Body = $body
# Send the email with the list of users
Send-MailMessage #mailProps
Have you tried
if ($null -eq $ManagerEmail)
{$ManagerEmail = "yourEmail#domain.com"}

Script to add days to a parameter

I need assistance creating a PowerShell script to expire a users account and add an Out of Office Message to Exchange for 30 days after the expiration date. Below is what I have so far:
Param(
$mailbox = $(Read-Host "you must enter the first part of the email address"),
$StartTime = $(Read-Host "Please enter the start date and time of account expiration in MM/DD/YYYY HH:MM:SS"),
$Username = $(Read-Host "please enter the AS400 username"),
$EndTime = $StartTime.AddDays(30)
)
Set-ADAccountExpiration -Identity $Username -DateTime $StartTime;
Set-MailboxAutoReplyConfiguration -Identity $mailbox -AutoReplyState Scheduled -StartTime $StartTime -EndTime $EndTime -ExternalMessage "The Person you are trying to contact is no longer employed by the Company" -InternalMessage "The Person you are trying to contact is no longer employed by Company XYZ for further assistance please contact the your local Store."
Method invocation failed because [System.String] does not contain a method named 'AddDays'.
How do I create the methods or is there an easier way?
Read-Host returns a string. String objects don't have a method AddDays(). You need to make $StartTime a DateTime value.
Change this:
Param(
...
$StartTime = $(Read-Host "..."),
...
)
into this:
Param(
...
[DateTime]$StartTime = $(Read-Host "..."),
...
)
Note that this only works, because input strings in the format you're asking for can be cast to DateTime. Otherwise you'd need to actually parse the string.
I had some surprisingly similar code written for something a while back, I can't remember if this worked fully and I am unable to test right now, but you should at least be able to get an idea of how parsing the inputs and ensuring everything is valid is done here.
I've changed it around a bit to fit your use better, if the SamAccountName in AD is the same as the Email Alias then you just need to specify Mailbox and StartTime at minimum, it will "autodiscover" the AD account.
Any questions please let me know - and if there's an issue running i'll take a look tomorrow when i'm back in work.
Function Set-LeaverAccount {
Param(
[string]$MailBox = $null,
[string]$StartTime = $null,
[string]$UserName = $null,
[string]$ExternalMessage = "The person you are emailing has left the company, please contact admin#company.com for more details",
[string]$InternalMessage = $null,
[uint16]$OutOfOfficeDays = 30
)
#region Parse Mailbox
$MailBoxAlias = $MailBox
if (!(Get-Mailbox $MailBox -EA SilentlyContinue)){
do {
$Input = Read-Host "Email Alias"
$MailBox = Get-Mailbox $Input -EA SilentlyContinue
} while (!$MailBox)
Write-Host "Mailbox of $($MailBox.Name) Selected"
} else { $Mailbox = Get-Mailbox }
#endregion
#region Parse DateTime
if (![DateTime]::Parse($StartTime)){
do {
$Input = Read-Host "Date/Time of Expiration"
[DateTime]$StartTime = [datetime]::MaxValue
} while (![DateTime]::TryParse($Input,[ref]$StartTime))
Write-Host "Start Time Set To $($StartTime.ToString())"
}
$EndTime = $StartTime.AddDays($OutOfOfficeDays)
#endregion
#region Parse AD Username
if ($UserName -eq $null){
$UserName = $MailBoxAlias #If not set use alias as AD Name
}
if (!(Get-AdUser $UserName -EA SilentlyContinue)){
do {
$Input = Read-Host "AD Username"
$User = Get-AdUser $Input -EA SilentlyContinue
} while ($User.count -ne 1)
Write-Host "User $($User.Name) Selected"
} else {
$User = Get-AdUser $UserName
}
#endregion
if ($InternalMessage -eq $null) { $InternalMessage = $ExternalMessage }
Set-ADAccountExpiration -Identity $User -DateTime $StartTime
Set-MailboxAutoReplyConfiguration -Identity $Mailbox -AutoReplyState Scheduled -StartTime $StartTime -EndTime $EndTime -ExternalMessage $ExternalMessage -InternalMessage $InternalMessage
}
Set-LeaverAccount -MailBox "User01" -StartTime "12/02/16" -UserName "User_1" `
-ExternalMessage "The Person you are trying to contact is no longer employed by the Company" `
-InternalMessage "The Person you are trying to contact is no longer employed by Company XYZ for further assistance please contact the your local Store."

PowerShell Function to check group member not working

I found this function I'd like to use in a script I'm writing, but it keeps coming back $false when I can see an account is in a group and I can't figure out why?
function Check-IsGroupMember{
Param($user,$grp)
$strFilter = "(&(objectClass=Group)(name=" + $grp +"))"
$objDomain = New-Object System.DirectoryServices.DirectoryEntry
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = $objDomain
$objSearcher.PageSize = 1000
$objSearcher.Filter = $strFilter
$objSearcher.SearchScope = "Subtree"
$colResults = $objSearcher.FindOne()
$objItem = $colResults.Properties
([string]$objItem.member).contains($user)
}
Usage:
Check-IsGroupMember "name of user" "DomainAdmins"
$objItem.member contains the DistinguishedName value of each principal who is a member of the group.
Even though the proper name of a person might be John Doe, the common name of the user account object may still be Doe, John, John G. Doe or anything else. This means that Contains() check (which is just a simple substring search) is not guaranteed to work as you expect.
The only real way to check is to either run another search for the user to find his/her DistinguishedName.
Personally, I would go for the AD PowerShell module from RSAT, rather than using a DirectorySearcher:
function Test-GroupMembership
{
Param(
[string]$UserName,
[string]$GroupName
)
$User = Get-ADUser -Identity $UserName
$Group = Get-ADGroup -Identity $GroupName -Properties member
$Group.member -contains $User.DistinguishedName
}
If size limit is your problem, you can use the DirectoryServer to retrieve a ranged result of the member attribute:
function Test-GroupMembership
{
[CmdletBinding()]
Param(
[string]$UserName,
[string]$GroupName
)
# Fetch User
$User = Get-ADUser -Identity $UserName
# return on failure
if(-not $User){
Write-Error -Message ('User "{0}" not found' -f $GroupName)
return $false
}
# Use DirectorySearcher to retrieve ranged member attribute
$GroupSearcher = '' -as [adsisearcher]
$GroupSearcher.Filter = '(&(objectClass=group)(name={0}))' -f $GroupName
$GroupSearcher.SearchScope = 'Subtree'
$GroupSearcher.SearchRoot = '' -as [adsi]
# AD reponds with at least 1500 values per multi-value attribute since Windows Server 2003
$Start = 1
$Range = 1500
$GroupMembers = #()
$HasMoreMembers = $false
# Keep retrieving member values until we've got them all
do{
# Use range operator to "page" values
# Ref: https://msdn.microsoft.com/en-us/library/aa367017(v=vs.85).aspx
$RangedMember = 'member;range={0}-{1}' -f $Start,$($Start + $Range - 1)
$GroupSearcher.PropertiesToLoad.Add($RangedMember) | Out-Null
# Retrieve group
$Group = $GroupSearcher.FindOne()
# return on failure
if(-not $Group) {
Write-Error -Message ('Group "{0}" not found' -f $GroupName)
return $false
}
# If we've reached the end of the member list,
# AD will return a property where the upper range
# value is *, so it might not be the same property
# name we specified in PropertiesToLoad
$ReturnedMember = #($Group.Properties.PropertyNames) -like 'member;*'
# Add all members to the $GroupMembers variable
foreach($member in $Group.Properties."$ReturnedMember") {
# Test if user is in the member list
if($member -eq $User.DistinguishedName){
return $true
}
}
# If we've reached the end, exit the loop
if($ReturnedMember -eq $RangedPropertyName){
$HasMoreMembers = $true
}
} while ($HasMoreMembers)
# User wasn't found
return $false
}
To provide a bit of consistency in user experience, please use Approved Verbs for command names in PowerShell (eg. Test-* instead of Check-*)
[adsisearcher] is a type accelerator for the DirectorySearcher class

Powershell script for Soon-to-expire AD users

So basically, what I have here is a script that will scan a CSV that it imports, and for every entry in the spreadsheet, except for people in the RANDOM.DOMAIN, it will find the managers email address and send an automated email to the manager telling them user XYZ is about to expire soon, and they need to do something about it.
If the managers email is unavailable for some reason, then it defaults to sending the email to me.
This script works well.
The problem I am running into is, I want to make it so only one email is sent to each manager, despite multiple users (or entries) from the spreadsheet, list them as the manager.
I.e. if Joe Bloggs has a manager Aaron T and Jane Doe has the manager Aaron T, then Aaron T will get two emails, one email for each user.
MY QUESTION:
Is there an easy way to only get it to send one email per manager, even if that manager has multiple users reporting to them that are about to expire?
$datai = Import-Csv "Soon-to-expire User Accounts22.csv" | select 'Display Name',Manager,'Domain Name','Account Expiry Time'
Connect-QADService -Service another.DC | Out-Null
$expiringUsers = #{}
foreach ($i in $datai) {
$dn = $i.'Display Name'
$dn1 = $i.'Domain Name'
$man = $i.'Manager'
$aet = $i.'Account Expiry Time'
$subject = "Account about to expire: $dn"
$getmail = get-qaduser "$man" -LdapFilter '(mail=*)' | select mail
$emailAD = $getmail.mail
if ($man -eq "-" -or $man -like 'CN=*' -or $getmail -eq $null -or $man -eq "") {
$man = "Aaron T"
$getmail = get-qaduser "$man" -LdapFilter '(mail=*)' | select mail
$emailAD = $getmail.mail
}
if ($expiringUsers.Contains($emailAD)) {
$expiringUsers[$emailAD]["dn"] += $dn += "`n"
$expiringUsers[$emailAD]["aet"] += $aet += "`n"
$expiringUsers[$emailAD]["man"] += $man += "`n"
} else {
$expiringUsers[$emailAD] = #{
#"dn1" = $dn1
#"aet" = $aet
#"man" = $man
# "dn" = #( $dn )
}
}
}
$expiringUsers | fc #as suggested
foreach ($emailAD in $expiringUsers.Keys) {
$dn = $expiringUsers[$emailAD]["dn"]
$dn1 = $expiringUsers[$emailAD]["dn1"]
$man = $expiringUsers[$emailAD]["man"]
$aet = $expiringUsers[$emailAD]["aet"]
$subject = "Account/s About to Expire!"
$content = #"
Hi,
$dn `n
$dn1 `n
$man `n
$aet `n
$emailAD `n
Technology Services
"#
Send-MailMessage -from "aaron#website.com" `
-To $emailAD `
-Subject $subject `
-Body $content `
-Priority high `
-smtpServer "relay.server"
#using this as a test instead of sending mass emais all the time
Write-Host $content
}
UPDATED with the new script as requested.... still having issues.
Is there an easy way to only get it to send one email per manager, even if that manager has multiple users reporting to them that are about to expire?
For this you need to defer e-mail processing. Collect the users in a hashtable, e.g. by manager e-mail address:
...
$expiringUsers = #{}
foreach ($i in $datai) {
If ($i.'Domain Name' -notmatch "RANDOM.DOMAIN") {
...
if ($expiringUsers.Contains($emailAD)) {
$expiringUsers[$emailAD]["dn"] += $dn
} else {
$expiringUsers[$emailAD] = #{
"dn1" = $dn1
"aet" = $aet
"man" = $man
"dn" = #( $dn )
}
}
}
}
and move the actual e-mail processing outside the loop:
foreach ($emailAD in $expiringUsers.Keys) {
$dn1 = $expiringUsers[$emailAD]["dn1"]
$man = $expiringUsers[$emailAD]["man"]
$aet = $expiringUsers[$emailAD]["aet"]
$subject = "Account about to expire: $($expiringUsers[$emailAD]["dn"])"
$content = #"
Hi,
...
Technology Services
"#
Send-MailMessage -from "Test Script - Powershell <email#test.com>" `
-To "$emailAD" `
-Subject $subject `
-Body $content `
-Priority high `
-smtpServer servername
Write-Host "Mail Sent to $man"
}
Note that for simplicity reasons the above code only records the expiry date of the first user. If you want the expiry date of each user recorded separately, you'll have to take additonal steps, e.g.
$expiringUsers[$emailAD]["expiry"] += #{
"name" = $dn;
"date" = $aet;
}
instead of
$expiringUsers[$emailAD]["dn"] += $dn
So I finally decided to revisit this script, after many, many months.
I'm get a little better at PowerShell and while I'm sure this isn't the most effective way to do it, this is something that works for me.
I've also changed the input method; it pulls the information directly from AD, instead of using a CSV file that used to be generated from an application called 'AD Manager Plus' (Hate it).
Remember, using Quest CMDlets here because we don't have a 2008 environment. (so using Get-QADUser instead of Get-ADuser)
FYI, I have only posted the code here which sorts out the data into separate tables - you can decide how you want to utilize those results. For our environment, I have it build an nice HTML table and body, then send it to the appropriate manager to deal with.
#user data input
$data = get-qaduser -SizeLimit 0 -includedproperties accountexpires | where {$_.AccountExpires -ne $null -and $_.AccountExpires -le ((Get-Date).AddDays(45)) }
#get a list of managers, unique.
$uniqueMan = $data | select Manager -Unique
#foreach manager from $uniqueman
Foreach ($manager in $uniqueman) {
#create the array variable / clear it out for the next manager.
$myarray = #()
#foreach User found in in $data query
Foreach ($user in $data) {
#Search the $user's query for people with the same manager as that of the $uniqueman list.
If ($user.Manager -eq $manager.Manager) {
#do what with the result.
#add the person to an array
$myarray += New-Object psobject -Property #{
Name = $user.'Name'
UserName = $user.'SAMAccountName'
AccountExpires = $user.'AccountExpires'
Manager = $user.Manager
}
}
#for testing, to output the results to an HTML file.
#$myarray | ConvertTo-Html | Out-File ("C:\test\" + $manager.Manager + ".html")
}
}

How to clear value of $msg.to in ps1 script?

I have a script for automaticaly notifying users about AD password expiration. It needed for VPN users. But I can't find a way to solve a problem with $msg.to field. It can't accept, for example, "$msg.to = ''" and works only by $msg.to.add method. It makes not so good situation, when user, who was notified first - will recieve all next e-mails because they will be just added at the end of string, but not replacing all of data in $msg.to
There is a code:
Import-Module ActiveDirectory
#SMTP server name
$smtpServer = "mail.domain.local"
#Creating a Mail object
$msg = new-object Net.Mail.MailMessage
$msgr = new-object Net.Mail.MailMessage
#Creating SMTP server object
$smtp = new-object Net.Mail.SmtpClient($smtpServer)
#E-mail structure
Function EmailStructure($to,$expiryDate,$upn)
{
$msg.IsBodyHtml = $true
$msg.From = "notification#domain.com"
$msg.To.Add($to)
$msg.Subject = "Password expiration notice"
$msg.Body = "<html><body><font face='Arial'>This is an automatically generated message from Exchange service.<br><br><b>Please note that the password for your account <i><u>Domain\$upn</u></i> will expire on $expiryDate.</b><br><br>Please change your password immediately or at least before this date as you will be unable to access the service without contacting your administrator.</font></body></html>"
}
Function EmailStructureReport($to)
{
$msgr.IsBodyHtml = $true
$msgr.From = "notification#domain.com"
$msgr.To.Add($to)
$msgr.Subject = "Script running report"
$msgr.Body = "<html><body><font face='Arial'><pre><b>This is a daily report.<br><br>Script has successfully completed its work.<br>$NotificationCounter users have recieved notifications:<br><br>$ListOfAccounts<br><br></b></pre></font></body></html>"
}
#Set the target OU that will be searched for user accounts
$OU = "OU=Organisation,DC=domain,DC=local"
$ADAccounts = Get-ADUser -LDAPFilter "(objectClass=user)" -searchbase $OU -properties PasswordExpired, extensionAttribute15, PasswordNeverExpires, PasswordLastSet, Mail, Enabled | Where-object {$_.Enabled -eq $true -and $_.PasswordNeverExpires -eq $false}
$NotificationCounter = 0
$ListOfAccounts = ""
Foreach ($ADAccount in $ADAccounts)
{
$accountFGPP = Get-ADUserResultantPasswordPolicy $ADAccount
if ($accountFGPP -ne $null)
{
$maxPasswordAgeTimeSpan = $accountFGPP.MaxPasswordAge
}
else
{
$maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
}
#Fill in the user variables
$samAccountName = $ADAccount.samAccountName
$userEmailAddress = $ADAccount.ExtensionAttribute15
$userPrincipalName = $ADAccount.UserPrincipalName
if ($ADAccount.PasswordExpired)
{
Write-host "The password for account $samAccountName has expired!"
}
else
{
$ExpiryDate = $ADAccount.PasswordLastSet + $maxPasswordAgeTimeSpan
$TodaysDate = Get-Date
$DaysToExpire = $ExpiryDate - $TodaysDate
$DaysToExpireDD = $DaysToExpire.ToString() -Split ("\S{17}$")
Write-host "The password for account $samAccountName expires on: $ExpiryDate. Days left: $DaysToExpireDD"
if (($DaysToExpire.Days -eq 15) -or ($DaysToExpire.Days -eq 7) -or ($DaysToExpire.Days -le 3))
{
$expiryDate = $expiryDate.ToString("d",$ci)
#Generate e-mail structure and send message
if ($userEmailAddress)
{
EmailStructure $userEmailAddress $expiryDate $samAccountName
$smtp.Send($msg)
Write-Host "NOTIFICATION - $samAccountName :: e-mail was sent to $userEmailAddress"
$NotificationCounter = $NotificationCounter + 1
$ListOfAccounts = $ListOfAccounts + $samAccountName + " - $DaysToExpireDD days left.<br>"
}
}
}
}
Write-Host "SENDING REPORT TO IT DEPARTMENT"
EmailStructureReport("itdepartment#domain.com")
$smtp.Send($msgr)
How can I drop string in $msg.to after each sent e-mail?
If you want to reuse the same message but change the address and send several times to different addresses, use the clear method on the MailAddressCollection.
So your code will look something like this:
$msg.To.Clear()
$msg.To.Add($to)