I am brand new to PowerShell (started this morning). I have successfully connected to my Office 365 and have been able to get lists of users from Office 365 and mailbox fields from the Exchange portion. What I can't figure out is how to combine them.
What I am looking for is the ability to export certain fields from the mailbox object but only for those mailboxes that belong to a non-blocked, licensed Office 365 users. We have a lot of users whose mailboxes have not been removed but they may no longer be licensed or they may be blocked.
Here are the two exports I have running now. They are complete exports. I tried to filter to the Office 265 users by isLicensed but I never got any results so I just downloaded everything and post processed them with Excel. But I need to run this on a regular basis...
Here's the code:
Get-Mailbox -ResultSize Unlimited | Select-Object DisplayName,Name,PrimarySMTPAddress,CustomAttribute2 | Export-CSV C:\temp\o365\mailboxes.csv
Get-MsolUser -all | Select-Object SignInName, DisplayName, Office, Department, Title, IsLicensed | export-csv c:\temp\o365\Users.csv
Any assistance would be appreciated.
Okay, so as I understand what you're trying to do... You want to get a list of all O365 users for whom the IsLicensed property is $true and the BlockCredential property is $false. Of those users, you then want to pull some data from their mailbox objects; DisplayName, Name, PrimarySMTPAddress, and CustomAttribute2.
There are a couple of ways that we can do this. The first is easier to throw together in the shell but takes longer to actually run. The second requires some set up but completes quickly.
First method
Since we know what our criteria is for what we want from Get-MsolUser, we'll use the pipeline to pull out what we want and toss it straight into Get-Mailbox.
Get-MsolUser -All | Where-Object {$_.IsLicensed -eq $true -and $_.BlockCredential -eq $false} |
Select-Object UserPrincipalName |
ForEach-Object {Get-Mailbox -Identity $_.UserPrincipalName | Select-Object DisplayName,Name,PrimarySMTPAddress,CustomAttribute2}
O365 PowerShell doesn't like giving us ways to filter our initial query, so we handle that in the second step, here...
Where-Object {$_.IsLicensed -eq $true -and $_.BlockCredential -eq $false}
This means, for each item passed in from Get-MsolUser -All, we only want those which have the properties Islicensed set to $true and BlockCredential set to $false.
Now, we only care about the results of Get-MsolUser in so far as determining what mailboxes to look up, so we'll grab a single property from each of the objects matching our prior filter.
Select-Object UserPrincipalName
If you only run everything up to this point, you'd get a list of UPNs in your shell for all of the accounts that we're now going to pipe into Get-Mailbox.
Moving on to our loop in this... If you haven't learned ForEach-Object yet, it's used to run a scriptblock (everything between the {}) against each item in the pipeline, one at a time.
Get-Mailbox -Identity $_.UserPrincipalName
Welcome to the pipeline operator ($_). Our previous Select-Object is feeding a collection of objects through the pipeline and this placeholder variable will hold each one as we work on them. Since these objects all have a UserPrincipalName property, we reference that for the value to pass to the Identity parameter of Get-Mailbox.
Sidebar
Here's a simple example of how this works..
PS> 1,2,3 | ForEach-Object {Write-Host $_}
1
2
3
Each item is passed along the pipeline, where we write them out one at a time. This is very similar to your standard foreach loop. You can learn more about their differences in this Scripting Guy post.
Moving on...
Select-Object DisplayName,Name,PrimarySMTPAddress,CustomAttribute2
We wrap it up with one last Select-Object for the information that you want. You can then look at the results in the shell or pipe into Export-Csv for working with in Excel.
Now... Since the pipeline works sequentially, there's some overhead to it. We're running a command, collecting the results, and then passing those results into the next command one at a time. When we get to our Get-Mailbox, we're actually running Get-Mailbox once for every UPN that we've collected. This takes about 2.5 minutes in my organization and we have less than 500 mailboxes. If you're working with larger numbers, the time to run this can grow very quickly.
Second method
Since a large amount of the processing overhead in the first method is with using the pipeline, we can eliminate most of it by handling our data collection as early and thoroughly as possible.
$Users = Get-MsolUser -All | Where-Object {$_.IsLicensed -eq $true -and $_.BlockCredential -eq $false} | Select-Object -ExpandProperty UserPrincipalName
$Mailboxes = Get-Mailbox | Select-Object UserPrincipalName,DisplayName,Name,PrimarySMTPAddress,CustomAttribute2
$Results = foreach ($User in $Users) {
$Mailboxes | Where-Object UserPrincipalName -eq $User
}
$Results | Export-Csv myFile.csv
The first 2 lines are pretty self-explanatory. We get all the user account info that we care about (just the UPNs) and then we grab all the mailbox properties that we care about.
foreach ($User in $Users)
Each entry in $Users will be stored in $User, where we'll then use it in the scriptblock that follows (in the {}).
$Mailboxes | Where-Object UserPrincipalName -eq $User
Each item in $Mailboxes is piped into Where-Object where we then check if the UserPrincipalName property is equal to the current value of $User. All of the matches are then stored in $Results, which can again be piped to Export-Csv for work in Excel.
While this method is harder to write out in the shell, and requires a little extra initial set up, it runs significantly faster; 22 seconds for my org, versus 2.5 minutes with the first method.
I should also point out that the use of UserPrincipalName with the mailbox dataset is simply to help ensure a solid match between those and the account dataset. If you don't want it in your final results, you can always pipe $Results into another Select-Object and specify only the properties that you care about.
Hope this helps!
Related
First time poster but site has helped me so much in the past.
We are an MSP and regularly get requests from clients to pull various details off a list of users they send us. Unfortunately though their lists rarely (if ever) contain any unique identifiers for AD such as samAccountName or even e-mail.
So typically I only get their first and last names, job titles etc. and use a slight variation on the below to try and get the required samAccountNames to work in batch modify scripts.
Get Samaccountname from display name into .csv
The problem comes (and caused a big headache recently) when I try to put that output back into a table to line up with the displaynames. As if the script can't find the displayname it just moves onto the next one in the list and puts the samAccountName directly below the last one it found. making it out of line with the displayname column I've put it beside.
My question is is there something I can add to the below script that when an error occurs it simply inputs null or similar into the samAccountName output csv so I could spot that easily in an excel sheet.
Similarly some users have multiple accounts like an admin and non-admin account with the same display name but different samAccountName so it pulls both of them, which is less of a problem but also if there was any way to have the script let me know when that happens? That would be super useful for future.
Import-Module activedirectory
$displayname = #()
$names = get-content "c:\temp\users.csv"
foreach ($name in $names) {
$displaynamedetails = Get-ADUser -filter { DisplayName -eq $name } -server "domain.local"| Select name,samAccountName
$displayname += $displaynamedetails
}
$displayname | Export-Csv "C:\temp\Samaccountname.csv"
So the problem lies in that you rely on Get-ADUser to provide you with user objects and when it doesn't you have gaps in your output. You instead need to create an object for every name/line in your "csv" regardless of whether Get-ADUser finds anything.
Get-Content 'c:\temp\users.csv' |
ForEach-Object {
$name = $_
$adUser = Get-ADUser -Filter "DisplayName -eq '$name'" -Server 'domain.local'
# Create object for every user in users.csv even if Get-ADUser returns nothing
[PSCustomObject]#{
DisplayName = $name # this will be populated with name from the csv file
SamAccountName = $adUser.SamAccountName # this will be empty if $adUser is empty
}
} | Export-Csv 'C:\temp\Samaccountname.csv'
New to powershell and scripting in general. Trying to improve automation in our onboarding process, we have to move multiple user accounts to multiple OUs every couple of months.
Import-Module ActiveDirectory
$dept1 = "OU=Path1,DC=SOMEWHERE,DC=OUTTHERE"
$dept2 = "OU=Path2,DC=SOMEWHERE,DC=OUTTHERE"
Import-Csv "C:\Scripts\Incoming.csv" | ForEach-Object {
$samAccountName = $_."samAccountName"
Get-ADUser -Identity $samAccountName -Filter {Department -eq "Dept1"} | Move-ADObject -TargetPath $dept1
Get-ADUser -Identity $samAccountName -Filter {Department -eq "Dept2"} | Move-ADObject -TargetPath $dept2
}
This actually moves ALL users with the department marked into the path I have set.. but I only want it to look at those users in the csv and then filter their departments from AD - not the CSV. I feel like I'm missing a step but I've searched everywhere and tried the get-help. I feel like I need to get the identity, then search/filter against something else but I'm not quite sure how. Would appreciate any help.
edit
Okay, if I run the following:
Get-ADUser -Filter {Department -eq "Dept1"} -Properties Department
It returns everyone that fits that property but how do I compare those to the $samAccountName and ONLY try to move those accounts or run the commands against the accounts on the list? When I ran the second half of the command it tried to move them all and failed.
Move-ADObject $samAccountName -Target $dept1
I feel dumb.
It's ok to feel dumb. You're not and everyone feels that way at times when trying to learn a new thing. You're also here asking for help, so you're ahead of the game compared to a lot of others.
#Lee_Daily's comment is correct that Get-ADUser doesn't support using both -Identity and -Filter in the same command. They're part of different parameter sets. You can tell from the syntax output of Get-Help Get-ADUser or the online docs. Both show 3 different sets of parameters and Identity and Filter are not in the same one. What's odd is that your original script should have thrown an error because you tried to use both in the same command. No need to worry about that now though.
Here's a typical way one might approach this task. First, you query the user's details including the department you want to make a decision on. Then, you write your condition and perform the appropriate action. Doing it this way means you're only querying AD once for each user in your CSV rather than twice like your original script which is good for script performance and load on your AD. The inside of your ForEach-Object loop might look something like this. Note the addition of -Properties department in Get-ADUser. We need to ask for it explicitly because department isn't returned in the default result set.
# all of this goes inside your existing ForEach-Object loop
$u = Get-ADUser -Identity $_.samAccountName -Properties department
if ($u.Department -eq 'Dept1') {
$u | Move-ADObject -TargetPath $dept1
} elseif ($u.Department -eq 'Dept2') {
$u | Move-ADObject -TargetPath $dept2
}
Now let's talk about some alternative ways you might approach this.
The first way sort of flips things around so you end up only calling Get-ADUser once total, but end up doing a lot more filtering/processing on the client side. It's not my favorite, but it sets things up to understand my preferred solution. In particular, the Get-ADUser call uses the -LDAPFilter parameter. LDAP filter syntax is a little strange if you've never seen it before and this particular example could use the more common -Filter syntax just as easily. But in the next example it would be much more difficult and learning LDAP filter syntax enables you to query AD from anything rather than just PowerShell.
# instead of immediately looping on the CSV we imported, save it to a variable
$incoming = Import-Csv "C:\Scripts\Incoming.csv"
# then we make a single AD query for all users in either Dept1 or Dept2
$users = Get-ADUser -LDAPFilter '(|(department=Dept1)(department=Dept2))' -Properties department
# now we filter the set of users from AD by department and whether they're in the CSV and do the moves as appropriate
$users | Where-Object { $_.department -eq 'Dept1' -and
$_.samAccountName -in $incoming.samAccountName } | Move-ADObject -TargetPath $dept1
$users | Where-Object { $_.department -eq 'Dept2' -and
$_.samAccountName -in $incoming.samAccountName } | Move-ADObject -TargetPath $dept2
The benefit of this method is the single AD round trip for users rather than one for each in the CSV. But there are a lot more local loops checking the samAccountNames in the results with the samAccountNames from the CSV which can get expensive cpu-wise if your CSV and/or AD is huge.
The final example tweaks the previous example by expanding our LDAP filter and making AD do more of the work. AD is ridiculously good at returning LDAP query results. It's been fine-tuned over decades to do exactly that. So we should take advantange of it whenever possible.
Essentially what we're going to do is create a giant 'OR' filter out of the samAccountNames from the CSV so that when we get our results, the only check we have to do is the check for department. The way I verbalize this query in my head is that we're asking AD to "Return all users where SamAccountName is A or B or C or D, etc and Department is Dept1 or Dept2. The general form of the filter will look like this (spaces included for readability).
(& <SAM FILTER> <DEPT FILTER> )
# Where <DEPT FILTER> is copied from the previous example and
# <SAM FILTER> is similar but for each entry in the CSV like this
(|(samAccountName=a)(samAccountName=b)(samAccountName=c)...)
So let's see it in action.
# save our CSV to a variable like before
$incoming = Import-Csv "C:\Scripts\Incoming.csv"
# build the SAM FILTER
$samInner = $incoming.samAccountName -join ')(samAccountName='
$samFilter = "(|(samAccountName=$samInner))"
# build the DEPT FILTER
$deptFilter = '(|(department=Dept1)(department=Dept2))'
# combine the two with an LDAP "AND"
$ldapFilter = "(&$($samFilter)$($deptFilter))"
# now make our single AD query using the final filter
$users = Get-ADUser -LDAPFilter $ldapFilter -Properties department
# and finally filter and move the users based on department
$users | Where-Object { $_.department -eq 'Dept1' } | Move-ADObject -TargetPath $dept1
$users | Where-Object { $_.department -eq 'Dept2' } | Move-ADObject -TargetPath $dept2
There are more efficient ways to build the LDAP filter string, but I wanted to keep things simple for readability. It's also a lot easier to read with better PowerShell syntax highlighting than StackOverflow's. But hopefully you get the gist.
The only limitation with using this method is when your incoming CSV file is huge. There's a maximum size that your LDAP filter can be. Though I'm not sure what it is and I've never personally reached it with roughly ~4000 users in the filter. But even if you have to split up your incoming CSV file into batches of a few thousand users, it's still likely to be more efficient than the other examples.
I am needing to parse through user information to find which computers a specific user has access to, and then filter that out to generate txt docs for each computer listing the allowed users for that machine. However, my script isn't returning expected results and is creating incomplete lists.
Get-Content c:\temp\computers.txt | ForEach-Object {
$computername = $_
Get-ADUser -Filter "LogonWorkstations -like '*$computername'" -Properties LogonWorkstations |
Format-Table SamAccountName, Enabled |
Out-File -FilePath c:\temp\Accounts\"$computername-$fileDate".txt
}
I am fairly certain the issue lies in my filtering, because some of the files are returning info, however only ones where the username matches the computer name in some regard. Rather than listing users whose "LogonWorkstation" includes said computer, which is what I am looking to do. (If I pull a user's "LogonWorkstation" separately, that information is correct.)
I believe the issue is that the logonworkstations property stores the list of computers as a string rather than a collection. Since the -Filter parameter has limited operators, you will need to use -like in order to introduce wildcards. Then you can use whatever method to build your computer name string to include surrounding asterisks.
Get-Content c:\temp\computers.txt |
ForEach-Object {
Get-ADUser -Filter "LogonWorkstations -like '*$_*'" -Properties LogonWorkstations |
Format-Table SamAccountName, Enabled |
Out-File -FilePath c:\temp\Accounts\"$_-$fileDate".txt
}
I'm new and don't know enough about powershell.
I've got a .csv that is nothing but "EMAILS" for the header and some 6000 emails under it. (email1#company, email2#company, etc.)
I need to find the state of a particular, custom property for each one.
Individually, I know that I can run
Get-ADUser -Filter {mail -eq 'email#company'} -properties customproperty
in order to find one particular user's state.
I have been hitting my head against a wall trying to make it work with import-csv and export-csv, but I keep getting red all over my console.
Can someone point me an to example where import-csv and export-csv are used properly, with a command run against the contents?
Here's what I would do.
First, fetch all users that have email addresses in AD, and save them into a hashtable. This will make lookups absurdly faster and place less overall load on your domain controller. If you've got 100,000 user accounts it may not be the best option, but I expect that it will be in general.
$ADUsers = #{};
Get-ADUser -Filter "mail -like '*'" -Properties mail, customproperty | ForEach-Object {
$ADUsers.Add($_.mail, $_.customproperty);
}
Now you import the CSV and do lookup using a calculated property with Select-Object, and export it back out.
Import-Csv -Path $InputFile | Select-Object -Property emails, #{n='customproperty';e={$ADUsers[$_.emails]}} | Export-Csv -Path $OutputFile -NoTypeInformation;
So without seeing the code of what you posted, where I think you will run problems is with how interaction of two things.
The first will be that when you use the Import-CSV cmdlet. You will receive an array of objects with a property for each column and not an array of strings.
This is ties into the second part of the issue which is the sensitivity of the AD module filter. But the short answer is don't use {} inside the filter because it will break if you use {mail -eq $ImportedCSV.EMAILS}.
mklement0 has a wonderful answer that goes into the details. But a simple rule of thumb is "double quote outer, single quote" inner.
So you could expand the EMAILS property either with Select-Object -ExpandProperty EMAILS to have an array which works inside {} or you could use "mail -eq '$ImportedCSV.EMAILS'".
Here is an example with both the expansion and using the "property -eq '$variable'" style filter:
Import-CSV C:\Example\Path.csv |
Select-Object -ExpandProperty EMAILS |
ForEach-Object {
Get-ADUser -Filter "mail -eq '$_'" -Properties customproperty
} |
Select-Object mail,customproperty |
Export-CSV C:\Example\OutputPath.csv -NoTypeInformation
Please use below code
$csvInput = Import-CSV C:\Example\test.csv
Foreach ($line in $csvinput) {
Get-ADUser -Filter {mail -eq $line.mail} -Properties mail, customproperty |
Select-Object mail,customproperty |
Export-CSV C:\Example\OutputPath.csv -NoTypeInformation
}
So I have a command to look at all the mailboxes in my environment and return the oldest item in any folder that is not the "Contacts" folder of that particular mailbox. The whole thing seems to work except for the calculated expression that I threw in at the end of the command:
#{Name="Address";Expression={Get-Mailbox | ForEach-Object {$_.PrimarySmtpAddress}
The problem is this seems to return every Smtp Address for each line/object instead of one Smtp Address per line/object.
Here's the entire command:
Get-Mailbox | ForEach-Object {Get-MailboxFolderStatistics -IncludeOldestandNewestItems -Identity $_.Alias | Where-Object {($_.OldestItemReceivedDate -ne $null) -and ($_.FolderPath -ne "/Contacts")} | Sort OldestItemReceivedDate | Select First 1 OldestItemReceivedDate, Identity, #{Name="Address";Expression={Get-Mailbox | ForEach-Object {$_.PrimarySmptAddress}}}}
Ideally this would return the date of the oldest item, the folder where it was found, and the primary SMTP Address but it doesn't seem to be pulling only the corresponding SMTP Address. It looks like it's pulling every Primary SMTP Address every iteration. I'm sure it's something with my command but I can't figure out where. Any help would be much appreciated.
The calculated expression has access to the current pipeline object. However you are not using that when creating your expression. You are just calling every mailbox for each user as you have seen. Use the current pipeline object with $_. Get-Mailbox is smart enough to match needed values by property name.
#{Name="Address";Expression={Get-Mailbox $_ | ForEach-Object {$_.PrimarySmtpAddress}}}}
However you might be able to go about this is a different way. You already called all mailboxes at the start of your pipeline. No sense calling it a second time again.
Get-Mailbox | Select-Object Identity, #{Name="Address";Expression={$_.PrimarySmtpAddress}}, #{Name="OldestItemReceivedDate";Expression={
Get-MailboxFolderStatistics -IncludeOldestandNewestItems -Identity $_.Alias | Where-Object {
($_.OldestItemReceivedDate -ne $null) -and ($_.FolderPath -ne "/Contacts")} |
Sort-Object OldestItemReceivedDate |
Select-Object -ExpandProperty OldestItemReceivedDate -Last 1
}}
Now we have 2 calculated properties and we only need to call Get-Mailbox once for each user. You have some spelling mistakes and logic errors that I tried to fix. You will know if it does what you want.