Effective permissions on remote share for domain users in Powershell - powershell

I searched and read some topics here but I didn't found what I am looking for.
Basically, I want to check the effective permissions for a specific user for several shares, I want a script such as :
$user = Read-Host "Enter username"
$shares = "\\serverABC\share2","\\serverABC\share1"
foreach ($share in $shares)
{
Cmdlet-EffectivePermissions $share
}
Output expected :
\\serverABC\share1
Full Control : No
Traverse folder / execute / file : YEs
List folder / read data : No
...
\\serverABC\share2"
Full Control : No
Traverse folder / execute / file : YEs
List folder / read data : No
...
In fact, I want to do in Powershell exactly the same way that effective permissions Tab.
Does it exist a built-in solution (without importing any modules, add-ins, ...) with .NET Method (GetUserEffectivePermissions) or with Get-ACL?

I'm not aware of a .NET/PowerShell way to do this natively. There is a PowerShell module here that should be able to do what you're looking for, though. After importing that, you should be able to modify your pseudo code to the following:
$user = Read-Host "Enter username"
$shares = "\\serverABC\share2","\\serverABC\share1"
foreach ($share in $shares) {
Get-EffectiveAccess -Path $share -Principal $user -ListAllRights
}
That returns PS objects instead of simple text. If the format isn't to your liking, you can use some of the utility commands to shape it however you like. Here are two examples of doing that:
First, a simple change to original that doesn't return the exact format you mentioned, but it's pretty close:
foreach ($share in $shares) {
$share
Get-EffectiveAccess -Path $share -Principal $user -ListAllRights | ForEach-Object {
"{0}: {1}" -f $_.Permission, $_.Allowed
}
""
}
Next, a more complicated change that formats the output exactly how you were asking (at least I think):
# Go through each FileSystemRights enum name and add them to a hash table if their value is
# a power of 2. This will also keep track of names that share a value, and later those can
# be combined to provide a friendly permission name
$Ht = #{}
foreach ($Name in [System.Enum]::GetNames([System.Security.AccessControl.FileSystemRights])) {
$Value = [System.Security.AccessControl.FileSystemRights]::$Name
if ($Value.value__ -band ($Value.value__ - 1)) {
# Not a power of 2, so ignore this
continue
}
if (-not $Ht.$Value) {
$Ht.$Value = #()
}
$Ht.$Value += $Name
}
# FullControl isn't a power of 2, but it's useful to test for access, so add it manually
$Ht.([System.Security.AccessControl.FileSystemRights]::FullControl) = "FullControl"
function YesNoTest {
param(
[System.Security.AccessControl.FileSystemRights] $EffectiveAccess,
[System.Security.AccessControl.FileSystemRights] $AccessToTest
)
if (($EffectiveAccess -band $AccessToTest) -eq $AccessToTest) {
"Yes"
}
else {
"No"
}
}
$shares | Get-EffectiveAccess -Principal $user | ForEach-Object {
$_.DisplayName
$EffectiveAccess = $_.EffectiveAccess
$Ht.GetEnumerator() | sort { $_.Key.value__ } -Descending | ForEach-Object {
"{0}: {1}" -f ($_.Value -join " / "), (YesNoTest $EffectiveAccess $_.Key)
}
""
}
Note that this won't be completely accurate if you run this against a remote system and the following conditions are met:
The security descriptor contains groups that are local to the remote system, i.e., non domain groups
The user(s) you're checking is a member of one of the local groups

Related

How do I change the permissions of a user for 600+ folders in Outlook via PowerShell?

I'm trying to give an assistant access to part of someone's O365 mailbox. She currently has foldervisible permissions on his inbox. And there is about 607 folders under the inbox that she needs access to without having anymore permissions to the inbox itself.
Below is the code I've tried to run. I've removed the domain name but otherwise the code is exact. I've run the code twice and gotten no errors and it runs for a quite a while. But once it's complete, there is no change in the permissions.
ForEach($f in (Get-EXOMailboxFolderStatistics -identity jjo | Where { $_.FolderPath.Contains("jjo:/Inbox/Case Files") -eq $True } ) ) {
$fname = "jjo:" + $f.FolderPath.Replace("/","\");
Add-MailboxFolderPermission $fname -User gka -AccessRights Owner
}
Try to narrow down what's working and not;
Get-EXOMailboxFolderStatistics -identity jjo -ErrorAction Stop | Where-Object { $_.FolderPath.Contains("jjo:/Inbox/Case Files") } | ForEach-Object {
Try {
$fname = "jjo:" + $_.FolderPath.Replace("/","\")
Write-Host "Amending: $fname ..."
Add-MailboxFolderPermission $fname -User gka -AccessRights Owner -ErrorAction Stop
Write-Host "Done"
}
Catch {
$_
}
}
Write-Host "Complete"
On the client side, Extended MAPI (C++ or Delphi) can be used modify the ACL table on the folder level. If using Redemption (I am its author) is an option (any language), you can use RDOFolder.ACL collection to modify the permissions. Something along the lines (VBA, off the op of my head):
ROLE_PUBLISH_EDITOR = &H4FB
set Session = CreateObject("Redemption.RDOSession")
Session.MAPIOBJECT = Application.Session.MAPIOBJECT
set AddressEntry = Session.AddressBook.GAL.ResolveName("The Other User Name")
set Folder = Session.GetDefaultFolder(olFolderInbox)
for each subFolder in Folder.Folders
set ACE = subFolder.ACL.Add(AddressEntry)
ACE.Rights = ROLE_PUBLISH_EDITOR
next

Powershell Global Variable usage as parameter to argument

$global:af_fp = "C:\Path\to\folder\"
Function function-name {
do things …
$global:af_fp = $global:af_fp + $variableFromDo_things + "_AF.csv"
}
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
Above is the generalized (and abbreviated) script contents for a powershell script.
Every time I run the script in this way, I get the following error:
Add-Content : Could not find a part of the path 'C:\Users\timeuser\Documents\'.
At C:\Users\timeuser\Documents\get_software.ps1:231 char:51
+ ... ware | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Users\timeuser\Documents\:String) [Add-Content], DirectoryNotFoundException
+ FullyQualifiedErrorId : GetContentWriterDirectoryNotFoundError,Microsoft.PowerShell.Commands.AddContentCommand
When I run
Get-Variable -Scope global
after running the script and seeing the error, the variable af_fp contains exactly the information I am seeking for the file name, however, the error shows the variable contents ending in ':String'.
To confuse me even more, if I comment out the lines containing '$global:...' and re-run the same script, IT ACTUALL RUNS AND SAVES THE FILE USING THE LINE
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
AS INTENDED. Of course, I had to run the script and watch it error first, then re-run the script with the global variable declaration and update commented out for it to actually work. I want to run the script ONCE and still get the same results.
FYI, I am a complete noob to powershell, but very familiar with the concept of variable scope.....but why is this global not working when initially created and updated, but then work the second time around, when, as far as I can tell, the CONTENT AND SCOPE of the global remains the same...…. any assistance to finding a solution to this small issue would be greatly appreciated; I have tried sooooo may different methods from inquiries through here and on Google...…..
EDIT: not sure why this will matter, because the script ran before as intended when I explicitly typed the parameter for -Path as 'C:\path\to\file'. The ONLY CHANGES MADE to the original, working script (below) were my inclusion of the global variable declaration, the update to the contents of the global variable (near the end of the function), and the attempt to use the global variable as the parameter to -Path, that is why I omitted the script:
'''
$global:af_fp = "C:\Users\timeuser\Documents\"
Function Get-Software {
[OutputType('System.Software.Inventory')]
[Cmdletbinding()]
Param(
[Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
[String[]]$Computername = $env:COMPUTERNAME
)
Begin {
}
Process {
ForEach ($Computer in $Computername) {
If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {
$Paths = #("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall")
ForEach ($Path in $Paths) {
Write-Verbose "Checking Path: $Path"
# Create an instance of the Registry Object and open the HKLM base key
Try {
$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine', $Computer, 'Registry64')
}
Catch {
Write-Error $_
Continue
}
# Drill down into the Uninstall key using the OpenSubKey Method
Try {
$regkey = $reg.OpenSubKey($Path)
# Retrieve an array of string that contain all the subkey names
$subkeys = $regkey.GetSubKeyNames()
# Open each Subkey and use GetValue Method to return the required values for each
ForEach ($key in $subkeys) {
Write-Verbose "Key: $Key"
$thisKey = $Path + "\\" + $key
Try {
$thisSubKey = $reg.OpenSubKey($thisKey)
# Prevent Objects with empty DisplayName
$DisplayName = $thisSubKey.getValue("DisplayName")
If ($DisplayName -AND $DisplayName -notmatch '^Update for|rollup|^Security Update|^Service Pack|^HotFix') {
$Date = $thisSubKey.GetValue('InstallDate')
If ($Date) {
Try {
$Date = [datetime]::ParseExact($Date, 'yyyyMMdd', $Null)
}
Catch {
Write-Warning "$($Computer): $_ <$($Date)>"
$Date = $Null
}
}
# Create New Object with empty Properties
$Publisher = Try {
$thisSubKey.GetValue('Publisher').Trim()
}
Catch {
$thisSubKey.GetValue('Publisher')
}
$Version = Try {
#Some weirdness with trailing [char]0 on some strings
$thisSubKey.GetValue('DisplayVersion').TrimEnd(([char[]](32, 0)))
}
Catch {
$thisSubKey.GetValue('DisplayVersion')
}
$UninstallString = Try {
$thisSubKey.GetValue('UninstallString').Trim()
}
Catch {
$thisSubKey.GetValue('UninstallString')
}
$InstallLocation = Try {
$thisSubKey.GetValue('InstallLocation').Trim()
}
Catch {
$thisSubKey.GetValue('InstallLocation')
}
$InstallSource = Try {
$thisSubKey.GetValue('InstallSource').Trim()
}
Catch {
$thisSubKey.GetValue('InstallSource')
}
$HelpLink = Try {
$thisSubKey.GetValue('HelpLink').Trim()
}
Catch {
$thisSubKey.GetValue('HelpLink')
}
$Object = [pscustomobject]#{
#Potential Candidate for AssetID in the TIME system
AssetID = $Computer
#String that contains word or word combinations for the product field of CPE WFN; may also contain the valid values necessary for update, edition, language, sw_edition, target_hw/sw fields as well.
cpeprodinfo = $DisplayName
cpeversion = $Version
InstallDate = $Date
cpevendor = $Publisher
UninstallString = $UninstallString
InstallLocation = $InstallLocation
InstallSource = $InstallSource
HelpLink = $thisSubKey.GetValue('HelpLink')
EstimatedSizeMB = [decimal]([math]::Round(($thisSubKey.GetValue('EstimatedSize') * 1024) / 1MB, 2))
}
$Object.pstypenames.insert(0, 'System.Software.Inventory')
Write-Output $Object
}
}
Catch {
Write-Warning "$Key : $_"
}
}
}
Catch { }
$reg.Close()
}
}
Else {
Write-Error "$($Computer): unable to reach remote system!"
}
$global:af_fp = $global:af_fp + $Computer + "_AF.csv"
}
}
}
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
'''
IGNORE FORMATTING PLEASE- HAD TROUBLE MAKING INDENTS CORRECTLY FROM COPY-PASTE AND RESTRICTIONS ON SITE FOR CODE BLOCKS.....
NOTE: the ONLY changes I made, that I am asking about, are the global declaration, the global variable update in the function, and the attempt to use the global variable for the -Path parameter....script otherwise runs and will even run WITH THE LAST LINE AS IS if I ran it and errored the first time.....not sure how the addition script will help in any way, shape, or form!
With a little effort, Nasir's solution worked! HOWEVER, I ran across a sample file that had a way of adding to a parameter that inspired me to make a change to my ORIGINAL, that also worked: remove global variable from script entirely and add this code the very end:
$file_suffix = '_AF.csv'
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $env:COMPUTERNAME$file_suffix
In this way, I was able to accomplish exactly what I was setting out to do! Thanks Nasir for your response as well! I was able to also make that work as intended!
Global variables are generally frowned upon, since they often lead to poor scripts, with hard to debug issues.
It seems like your function returns some stuff, which you need to write to a file, the name of which is also generated by the same function. You can try something like this:
function function-name {
param($PathPrefix)
#do things
[pscustomobject]#{"DoThings_data" = $somevariablefromDoThings; "Filename" = "$($PathPrefix)$($variableFromDo_Things)_AF.csv"}
}
function-name -PathPrefix "C:\Path\to\folder\" | Foreach-Object { $_.DoThings_data | Export-Csv -Path $_.Filename -NoTypeInformation }
Or just have your function write the CSV data out and then return the data if you need to further process it outside the function.
Edit: this is just me extrapolating from partial code you have provided. To Lee_Dailey's point, yes, please provide more details.

Powershell - Checking variable for value in foreach - If no value then log other output

I'm having issue with my foreach method. I am checking in the registry whether a good amount of programs are installed. How would I write it to say something is not installed one time versus it saying something's not installed for each key it checks? Now, If I place a ElseIf it executes "PowerBroker not installed." about 16 times. This is due to it checking every key and writing it out for each key it does not find a match to the displayname. How do I go about it checking the key and only writing it out one time if it's not installed?? Thanks!
$UninstallKeys = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
foreach($Key in $UninstallKeys){
if($Key.GetValue("DisplayName") -Match "BeyondTrust"){
$PBW = $Key.GetValue("DisplayName")
$PBWV = $Key.GetValue("DisplayVersion")
if ($PBW) {
$PBW = $PBW, $PBWV
}
else {
$PBW = "PowerBroker not installed."
$installsmissing = "True"
}
}
Give this script a whirl. If I've understood the requirement correctly it should give you what you need.
$displayName = "BeyondTrust"
$uninstallKeys = Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
# Filter the keys down by their display name property
$specificUninstallKeys = $uninstallKeys |
Where-Object {
$_.GetValue("DisplayName") -eq $displayName
}
# Did we find any keys of that name?
if ($specificUninstallKeys) {
Write-Output "Keys found: $($specificUninstallKeys.Length)"
}
else {
Write-Output "Sorry pal, no keys by that name here!"
}
# There may be more than one; hence the loop-y requirement here.
foreach ($specificUninstallKey in $specificUninstallKey) {
Write-Output $displayName
Write-Output $specificUninstallKey.GetValue("DisplayVersion")
}

Missing AD module and can't get it, need something similar or something to simulate it

So I'm trying to output a complete KB list for all computers on a server (which works on one computer) but it doesn't recognize Get-ADcomputer as a cmdlet. When checking various sources, it appears that the AD module isn't included. As I'm doing this on a work computer/server I'm hesitant to download anything or anything of that nature.
Is there any way I can achieve the following without using the AD module or someway I might be missing how to import the module (if it exists, which I don't think it does on this system)?
# 1. Define credentials
$cred = Get-Credential
# 2. Define a scriptblock
$sb = {
$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$HistoryCount = $Searcher.GetTotalHistoryCount()
$Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {
$Title = $null
if ($_.Title -match "\(KB\d{6,7}\)") {
# Split returns an array of strings
$Title = ($_.Title -split '.*\((?<KB>KB\d{6,7})\)')[1]
} else {
$Title = $_.Title
}
$Result = $null
switch ($_.ResultCode) {
0 { $Result = 'NotStarted'}
1 { $Result = 'InProgress' }
2 { $Result = 'Succeeded' }
3 { $Result = 'SucceededWithErrors' }
4 { $Result = 'Failed' }
5 { $Result = 'Aborted' }
default { $Result = $_ }
}
New-Object -TypeName PSObject -Property #{
InstalledOn = Get-Date -Date $_.Date;
Title = $Title;
Name = $_.Title;
Status = $Result
}
} | Sort-Object -Descending:$false -Property InstalledOn | Where {
$_.Title -notmatch "^Definition\sUpdate"
}
}
#Get all servers in your AD (if less than 10000)
Get-ADComputer -ResultPageSize 10000 -SearchScope Subtree -Filter {
(OperatingSystem -like "Windows*Server*")
} | ForEach-Object {
# Get the computername from the AD object
$computer = $_.Name
# Create a hash table for splatting
$HT = #{
ComputerName = $computer ;
ScriptBlock = $sb ;
Credential = $cred;
ErrorAction = "Stop";
}
# Execute the code on remote computers
try {
Invoke-Command #HT
} catch {
Write-Warning -Message "Failed to execute on $computer because $($_.Exception.Message)"
}
} | Format-Table PSComputerName,Title,Status,InstalledOn,Name -AutoSize
You've got 3 options:
First is to just install the RSAT feature for AD which will include the AD module. This is probably the best option unless there is something specific preventing it. If you're running your script from a client operating systems you need to install the RSAT first, though.
Option 2 (which should only be used if adding the Windows feature is somehow an issue) is to download and use the Quest AD tools, which give very similar functionality, but it looks like Dell is doing their best to hide these now so that may be difficult to locate...
Option 3 is to use the .NET ADSI classes to access AD directly, which will work without any additional downloads on any system capable of running PowerShell. If you'd like to go this route you should check out the documentation for the interface Here and for the System.DirectoryServices namespace Here.
Edit
Just noticed the last part of your question, what do you mean by "a complete KB list"? Not just Windows updates or things updated manually or whatever? What else would be in a list of Windows updates that was not a Windows update?
You have not mentioned the OSes you are using but in general if you have a server 2008 R2 or above, all you have to do it activate the RSAT feature AD PowerShell Module and you will have the cmdlet you are looking for.
On a client machine, you 'have to' install RSAT, and then activate the features. You can take a look at the technet article for more info: https://technet.microsoft.com/en-us/library/ee449483(v=ws.10).aspx
If you don't want to use that option, then you will have to use .NET ADSI classes. There are tons of examples on how to do this, it basically boils down to a couple of lines really. Technet has examples on this as well: https://technet.microsoft.com/en-us/library/ff730967.aspx

Powershell to Validate Email addresses

I'm trying to get Powershell to validate email addresses using Regex and put email addresses into good and bad csv files. I can get it to skip one line and write to file, but cannot get it to target the email addresses and validate them, then write lines to good and bad files. I can do it in C# and JavaScript, but have never done it in Powershell. I know this can be done, but not sure how.
Here is what I have so far:
Function IsValidEmail {
Param ([string] $In)
# Returns true if In is in valid e-mail format.
[system.Text.RegularExpressions.Regex]::IsMatch($In,
"^([\w-\.]+)#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|
(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$");
}
## Now we need to check the original file for invalid and valid emails.**
$list = Get-Content C:\Emails\OriginalEmails\emailAddresses.csv
# This way we also use the foreach loop.
##======= Test to see if the file exists ===========
if (!(Test-Path "C:\Emails\ValidEmails\ValidEmails.csv")) {
New-Item -path C:\Emails\ValidEmails -name ValidEmails.csv -type
"file" # -value "my new text"
Write-Host "Created new file and text content added"
}
else {
## Add-Content -path C:\Share\sample.txt -value "new text content"
Write-Host "File already exists and new text content added"
}
if (!(Test-Path "C:\Emails\InValidEmails\InValidEmails.csv")) {
New-Item -path C:\Emails\InValidEmails -name InValidEmails.csv -type
"file" # -value "my new text"
Write-Host "Created new file and text content added"
}
else {
# Add-Content -path C:\Emails\ValidEmails -value "new text content"
Write-Host "File already exists and new text content added"
}
#$Addresses = Import-Csv "C:\Data\Addresses.csv" -Header
Name, Address, PhoneNumber | Select -Skip 1
$EmailAddressImp = Import-Csv
"C:\Emails\OriginalEmails\emailAddresses.csv" -Header
FirstName, LastName, Email, Address, City, State, ZipCode | Select
FirstName, LastName, Email, Address, City, State, ZipCode -Skip 1
I'm validating the third column "Email" in the original csv file and trying to write out the whole row to file (good file, bad file). Not sure how to buffer either doing this.
ForEach ($emailAddress in $list) {
if (IsValidEmail($emailAddress)) {
"Valid: {0}" -f $emailAddress
Out-File -Append C:\Emails\ValidEmails\ValidEmails.csv -Encoding UTF8
$EmailAddressImp | Export-Csv "C:\Emails\ValidEmails\ValidEmails.csv"
-NoTypeInformation
}
else {
"Invalid: {0}" -f $emailAddress
Out-File -Append C:\Emails\InValidEmails\InValidEmails.csv -
Encoding UTF8
$EmailAddressImp | Export-Csv
"C:\Emails\InValidEmails\InValidEmails.csv" -NoTypeInformation
}
}
I'm trying to get Powershell to validate email addresses using Regex
Don't!
I would recommend against this. Accurately validating email addresses using regular expressions can be much more difficult than you might think.
Let's have a look at your regex pattern:
^([\w-\.]+)#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$
In it's current form it incorrectly validates .#domain.tld.
On the other hand, it doesn't validate unicode-encoded internationalized domain names, like user#☎.com (yes, that's a valid email address)
Instead of trying to find or construct a perfect email validation regex pattern, I would use the MailAddress class for validation instead:
function IsValidEmail {
param([string]$EmailAddress)
try {
$null = [mailaddress]$EmailAddress
return $true
}
catch {
return $false
}
}
If the input string is a valid email address, the cast to [mailaddress] will succeed and the function return $true - if not, the cast will result in an exception, and it returns $false.
When exporting the data, I'd consider collecting all the results at once in memory and then writing it to file once, at the end.
If you're using PowerShell version 2 or 3, you can do the same with two passes of Where-Object:
$EmailAddresses = Import-Csv "C:\Emails\OriginalEmails\emailAddresses.csv" -Header FirstName, LastName, Email, Address, City, State, ZipCode | Select -Skip 1
$valid = $list |Where-Object {IsValidEmail $_.Email}
$invalid = $list |Where-Object {-not(IsValidEmail $_.Email)}
If you're using PowerShell version 4.0 or newer, I'd suggest using the .Where() extension method in Split mode:
$EmailAddresses = Import-Csv "C:\Emails\OriginalEmails\emailAddresses.csv" -Header FirstName, LastName, Email, Address, City, State, ZipCode | Select -Skip 1
$valid,$invalid = $list.Where({IsValidEmail $_.Email}, 'Split')
before exporting to file:
if($valid.Count -gt 0){
$valid |Export-Csv "C:\Emails\ValidEmails\ValidEmails.csv" -NoTypeInformation
}
if($invalid.Count -gt 0){
$invalid |Export-Csv "C:\Emails\ValidEmails\InvalidEmails.csv" -NoTypeInformation
}
You can just use the -match operator, instead of calling into the [Regex] class. Here's a simple example, without any wrapper function:
$EmailRegex = '^([\w-\.]+)#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$'
$EmailList = #('a#a.com', 'b#b.co', 'm.a#example.il')
foreach ($Email in $EmailList) {
$DidItMatch = $Email -match $EmailRegex
if ($DidItMatch) {
# It matched! Do something.
}
else {
# It didn't match
}
}
FYI, when you use the -match operator, if it returns boolean $true, then PowerShell automatically populates a built-in (aka. "automatic") variable called $matches. To avoid unexpected behavior, you might want to reset this variable to $null during each iteration, or just wrap it in a function as you did in your original example. This will keep the variable scoped to the function level, as long as you don't declare it in one of the parent scopes.
Once you've validated the e-mail address, you can append it to your existing CSV file, using:
Export-Csv -Append -FilePath filepath.csv -InputObject $Email
For efficiency with the available filesystem resources, you'll probably want to buffer a few e-mail addresses in memory, before appending them to your target CSV file.
# Initialize a couple array buffers
$ValidEmails = #()
$InvalidEmails = #()
if ($ValidEmails.Count -gt 50) {
# Run the CSV export here
}
if ($Invalid.Count -gt $50) {
# Run the CSV export here
}
If you need further help, can you please edit your question and clarify what isn't working for you?
Each of the current top 2 answers here has one significant deficiency:
#Trevor's answer would do just fine, until you supply it this:
John Doe <johndoe#somewhere.com>
#Mathias' answer preaches about accommodating exceptional (yet valid) addresses such as those with non-ASCII or no TLD suffix. The following addresses all validate successfully with the [mailaddress] casting:
olly#somewhere | olly#somewhere. | olly#somewhere...com etc
If, like me, you will not be entertaining these edge cases into your email databases, then a combination of both ideas might prove more useful, like so:
function IsValidEmail {
param([string]$Email)
$Regex = '^([\w-\.]+)#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$'
try {
$obj = [mailaddress]$Email
if($obj.Address -match $Regex){
return $True
}
return $False
}
catch {
return $False
}
}
Perhaps there is a performance overhead with creating $obj for every email address on a possibly long mailing list. But I guess that's another matter.
You can use the mailaddress type to ensure it meets RFC, but you will likely still want to make sure the domain is valid:
Resolve-DnsName -Name ('vertigoray#example.com' -as [mailaddress]).Host -Type 'MX'
Works well as a validation script for a function parameter:
function Assert-FromEmail {
param(
[Parameter(Mandatory = $true)]
[ValidateScript({ Resolve-DnsName -Name $_.Host -Type 'MX' })]
[mailaddress]
$From
)
Write-Output $From
}
Output examples of that function on success:
PS > Assert-FromEmail -From vertigoray#example.com
DisplayName User Host Address
----------- ---- ---- -------
vertigoray example.com vertigoray#example.com
Output examples of that function on failure:
PS > Assert-FromEmail -From vertigoray#example..com
Assert-FromEmail : Cannot validate argument on parameter 'From'. The " Resolve-DnsName -Name $_.Host -Type 'MX' "validation script for the argument with value "vertigoray#example..com" did not return a result of True. Determine why the validation script failed, and then try the command again.
At line:1 char:24
+ Assert-FromEmail -From vertigoray#example..com
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Assert-FromEmail], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Assert-FromEmail
Here is one to try I wrote up and tested and has not failed me in any environment to date. Not, saying it won't in someone else's, but for me, it's been 100%.
$SomeEmailAddresses = #'
From:JoeBob#yahoo.com,Tom TheCat tcat#snailmail.net,jerry#snailmail.net
To:TulaJane#hotmail.com;JF#gmail.com;tiger#outlook.com;
Doug Tompson DTompson#icloud.com
MailTo:BobsYourUncle#protonmail.com;
johnny.bravo#yahoo.co.uk
'#
(((Select-String -InputObject $SomeEmailAddresses `
-Pattern '\w+#\w+\.\w+|\w+\.\w+#\w+\.\w+\.\w+' `
-AllMatches).Matches).Value)
Rsults
JoeBob#yahoo.com
tcat#snailmail.net
jerry#snailmail.net
TulaJane#hotmail.com
JF#gmail.com
tiger#outlook.com
DTompson#icloud.com
BobsYourUncle#protonmail.com
johnny.bravo#yahoo.co.uk
#postanote
This common email formatting fails
$SomeEmailAddresses = #'
First A. Last first.a.last#gmail.com.
'#
(((Select-String -InputObject $SomeEmailAddresses -Pattern '\w+#\w+\.\w+|\w+\.\w+#\w+\.\w+\.\w+'
-AllMatches).Matches).Value)
Here is the code I use.
The regex does not support the following because the major email players do not support.
Domains as IP addresses.
Space and special characters "(),:;<>#[] inside a quoted string in local-part.
Comments within parentheses in local-part.
$email = "^(?(?=^(?:([a-zA-Z0-9_!#$%&'+-/=?^{|}~]+|[a-zA-Z0-9_!#$%&'*+\-\/=?^{|}~].[a-zA-Z0-9_!#$%&'+-/=?^{|}~][\.a-zA-Z0-9_!#$%&'*+\-\/=?^{|}~]))#[a-zA-Z0-9.-]{1,63}$)[a-zA-Z0-9_.!#$%&'*+-/=?^`{|}~]{1,63}#[a-zA-Z0-9-]+(?:.[a-zA-Z0-9-]{2,})+)$"
$email -match $regexPattern