PowerShell – Prompt/Confirm User’s Choice (A Simple Yes/No function)

Here’s a sample of a custom function inspired from a previous PowerShell Tip of the Week:

function Confirm-UserChoice
{
    [CmdletBinding()]
    [Alias()]
    [OutputType([bool])]
    Param
    (
        [Parameter(Mandatory=$true,
                   Position=0)]
        [Alias('Title')]
        [string] 
        $PromptTitle,
        [Parameter(Mandatory=$true,
                   Position=1)]
        [Alias('Message')] 
        [string]
        $PromptMessage,
        [Parameter(Mandatory=$false,
                   Position=2)]
        [Alias('YesDesc')]
        [string]
        $YesOptionDescription,
        [Parameter(Mandatory=$false,
                   Position=3)]
        [Alias('NoDesc')]
        [string]
        $NoOptionDescription
    )
    Begin
    {
        if ($PSBoundParameters['YesOptionDescription'])
        {
            $yes = New-Object -TypeName System.Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes',$YesOptionDescription
        }
        else
        {
            $yes = New-Object -TypeName System.Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'
        }
        if ($PSBoundParameters['NoOptionDescription'])
        {
            $no = New-Object -TypeName System.Management.Automation.Host.ChoiceDescription -ArgumentList '&No',$NoOptionDescription
        }
        else
        {
            $no = New-Object -TypeName System.Management.Automation.Host.ChoiceDescription -ArgumentList '&No'
        }
        $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes,$no)
        #$maybe = New-Object -TypeName System.Management.Automation.Host.ChoiceDescription -ArgumentList '&Maybe','Maybe we want to do provide more options'
        #$options = [System.Management.Automation.Host.ChoiceDescription[]]($yes,$no,$maybe)
    }
    Process
    {
        $result = $Host.UI.PromptForChoice($PromptTitle, $PromptMessage, $options, 0) # Index of default option. '0' in this case represents 'Yes' in the above example, '1' for 'No'.

    switch ($result)
        {
            0 {return $true}
            1 {return $false}
        }
    }
    End
    {
        Remove-Variable -Name yes,no,options,result
    }
}

Where the usage looks like this:

Confirm-UserChoice -PromptTitle 'Confirm Action' -PromptMessage 'Do you really want to do this?' -YesOptionDescription 'Yes, I do.' -NoOptionDescription 'No, I do not.'

Bonus example: a slightly, possibly bad design decision, that let’s you make a mini pop-up selection at once:

function Confirm-OnTheFlyChoice
{
    [CmdletBinding(DefaultParameterSetName='Default', 
                  PositionalBinding=$false,
                  ConfirmImpact='Medium')]
    [Alias('cotfc')]
    Param
    (
        [Parameter(Mandatory=$true, 
                   Position=0,
                   ParameterSetName='Default')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [Alias('Title')] 
        [string]
        $PromptTitle,
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   Position=1,
                   ParameterSetName='Default')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [Alias('Message')] 
        [string]
        $PromptMessage,
        [Parameter(Mandatory=$true,
                   Position=2,
                   HelpMessage='Menu options:',
                   ParameterSetName='Default')]
        [string[]]
        $Option
    )
    Begin
    {
        $OptionArray = New-Object -TypeName System.Collections.ArrayList
        Write-Verbose -Message 'Listing Parameters utilized:'
        $PSBoundParameters.GetEnumerator() | ForEach-Object -Process { Write-Verbose -Message "$($PSItem)" }
    } # END: Begin
    Process
    {
        for ($i = 0; $i -lt $Option.Count; $i++)
        { 
            $OptionArray.Add("&$($Option[$i])") | Out-Null
        }
            $options = [System.Management.Automation.Host.ChoiceDescription[]]($OptionArray)
	        $result = $Host.UI.PromptForChoice($PromptTitle, $PromptMessage, $options, 0)
    } # END: Process
    End
    {
        return $Option[$result]
        Remove-Variable -Name OptionArray,options,result
    } # END: End
}

Where the usage would be:

Confirm-OnTheFlyChoice -PromptTitle 'I needed a fast menu' -PromptMessage 'What do you select?' -Option Bird,Cat,Dog,Fish

Note on a bug: the Confirm-OnTheFlyChoice would introduce an issue as-is if you have two values with the same starting letter.
Example:

-Option Fish,Ferret

Migrate From G Suite Into Office 365

This is a reference post on how to perform (as much as possible) of a mail migration from G Suite (formerly Google Apps) into Office 365 via PowerShell.
The Office 365 documentation for G Suite is great, where you might end up with sub-pages requiring a bunch of tabs or some circular page logic along the way… This is to hopefully condense it into a single page as a quick reference. You’ll also want to understand Office 365’s best practices.

G Suite to Office 365 will use IMAP which only migrates mail content. You will have a few limitations where you will need to determine a solution before leaving one service for another. The below bulleted list is from the above IMAP link, with emphasis added which may concern you (or other IT departments):

  • You can only migrate items in a user’s inbox or other mail folders. This type of migration doesn’t migrate contacts, calendar items, or tasks.
  • You can migrate a maximum of 500,000 items from a user’s mailbox (emails are migrated from newest to oldest).
  • The biggest email you can migrate is 35 MB.
  • If you limited the connections to your source email system, it’s a good idea to increase them to improve migration performance.
    Common connection limits include client/server total connections, per-user connections, and IP address connections on either the server or the firewall.

OK, figure out something to do for those, if you don’t want to upset your end users. Let’s continue (where this information is provided as is, at your own risk and no warranties implied!).

Step 1: Verify your own domain
This adds the domain name you want to absorb into your Office 365 tenant.

$o365admin = Get-Credential
$domain = ‘domain-name-to-move.com’
Import-Module -Name MSOnline
Connect-MSOLService -Credential $o365admin
New-MsolDomain -Name $domain -VerificationMethod DnsRecord
Get-MsolDomainVertificationDns -DomainName $domain -Mode DnsTxtRecord
<# And we hit the first step that requires something outside of PowerShell (maybe)! Add the Text value to your DNS infrastructure. Wait ~15 minutes for initial replication to occur or check it has replicated  on an external service. #>
Confirm-MsolDomain -DomainName $domain

 

Optional Step: Reduce email delays

Microsoft’s guide provides an optional suggestion of updating your MX record’s Time-To-Live (TTL) to 3600 (1 hour). This is noted to be performed before the migration request, but is listed after you start a migration batch in their current directions where you might have closed out your DNS management tool. I’ve moved it higher in the steps list if you want to make the modification at the same time as adding the TXT record in from the step above.

Step 2: Add Users to Office 365
Create a CSV with the following header information as outlined here:
User Name,First Name,Last Name,Display Name,Job Title,Department,Office Number,Office Phone,Mobile Phone,Fax,Address,City,State or Province,ZIP or Postal Code,Country or Region

<# Create the Office 365 accounts and assign a license for a mailbox to be created for copying from Gmail into Office 365. #>
Import-Csv -Path "C:\Office365Migration\NewAccounts.csv" | `
ForEach-Object -Process { New-MsolUser -DisplayName $PSItem.DisplayName -FirstName $PSitem.FirstName -LastName $PSitem.LastName -UserPrincipalName $PSitem.UserPrincipalName -UsageLocation $PSitem.UsageLocation -LicenseAssignment $PSitem.AccountSkuId
} | Export-Csv -Path ‘C:\Office365Migration\NewAccountResults.csv’

Step 3: Create a list of Gmail mailboxes
An abridged summary. Create a CSV with the following header columns:
EmailAddress,UserName,Password

EmailAddress is the Office 365 User Name (and mailbox) content should be copied into,
UserName is the sign-in name for the Gmail account,
Password is the mailbox’s Gmail password or an app password (which is recommended as it’s more secure) for Gmail. Save the file a CSV. Example C:\Office365Migration\GmailMailboxes.csv Keep this path handy for a step below.

Step 4: Connect Office 365 to Gmail (with extra info on what’s going on here and here)

$o365admin = Get-Credential
$Session = New-PSSession -ConfigurationName 'Microsoft.Exchange' -ConnectionUri 'https://outlook.office365.com/powershell-liveid/' -Credential $o365admin -Authentication 'Basic' -AllowRedirection
Import-PSSession -Session $Session

$gmail = ‘imap.gmail.com’
$port = 993 # Or 143
$security = ‘Ssl’ # Or ‘Tls’, or ‘None’
$endpointname = ‘GSuiteMigrations’
<#Test connectivity to the environment #>
Test-MigrationServerAvailability -IMAP -RemoteServer $gmail -Port $port -Security $security
<# Create the endpoint: #>
New-MigrationEndpoint -IMAP -Name $endpointname -RemoteServer $gmail -Port $port -Security $security -MaxConcurrentMigrations 50 -MaxConcurrentIncrementalSyncs 25 -Authentication 'Basic'
<# Verify the connector was created: #>
Get-MigrationEndpoint -Name $endpointname

 

Step 5: Create a migration batch and start migrating Gmail mailboxes (with extra info here)
You should probably test and migrate in a few “department champions” or test mailboxes first.
(Maybe move the IT team first.)
If you have a ton of mailboxes, you could migration in multiple “waves”.  Keep the same CSV headers, and split up the GmailMailboxes.csv into different groups if you’d like.

$batchname = ‘FirstWave’
<# Path to the CSV created in Step 3: #>
$path = ‘C:\Office365Migration\GmailMailboxes.csv’

New-MigrationBatch -Name $batchname -SourceEndpoint $endpointname -CSVData ([System.IO.File]::ReadAllBytes("$path")) -AutoStart
<# You can alternatively not copy over specific folders or apply some extra switch options #>
New-MigrationBatch -Name $batchname -SourceEndpoint $endpointname -CSVData ([System.IO.File]::ReadAllBytes("$path")) -AutoStart -ExcludeFolders ‘Junk’ -AllowIncrementalSyncs:$true
<# Check on status along the way: #>
Get-MigrationBatch -Name $batchname | Format-List
<# Complete a batch like so: #>
Complete-MigrationBatch -Name $batchname
<# Alternatively, instead of just sending to the single admin issued the completion (which is what happens when a single person runs the cmdlet, you could send it to a list or mail-enabled group: #>
$emailadmins = ‘email-admins-group@an-existing-office365-domain.com’
Complete-MigrationBatch -Name $batchname -NotificationEmails $emailadmins

 

Step 6: Update your DNS records to route Gmail directly to Office 365 (with more info here)

Okay, your mail should be copied over and you are ready to have the end users begin using their Office 365 mailbox and want to cut mail flow into your G Suite where it should only be delivered into Office 365.

[Wait, what? That’s not what I want to do!]
If you need to keep mailflow running in a different manner and didn’t read through the link at the top, you’ll need to look into additional options. TechNet has a ton more info for you to read depending on what you want to do… Otherwise, continue:

Update your MX record.
Your <domain-key> is your Office 365 domain name.
e.g. if you signed up with domain ‘contoso.com’, it would be: contoso-com.mail.protection.outlook.com

Name Type TTL Data
@ MX 3600 (1HR) <domain-key>.mail.protection.outlook.com.

 

Add (4) Required CNAME records.

Name Type TTL Data
autodiscover CNAME 3600 (1HR) autodiscover.outlook.com.
sip CNAME 3600 (1HR)  

sipdir.online.lync.com.

lyncdiscover CNAME 3600 (1HR) webdir.online.lync.com.
msoid CNAME 3600 (1HR) clientconfig.microsoftonline-p.net.

 

Update TXT record for SPF record to prevent spam.

Name Type TTL Data
@ TXT 3600 (1HR) v=spf1 include:spf.protection.outlook.com -all

 

Add SRV record for other Office 365 Services.

Name Type TTL Data
_sip._tls SRV 3600 (1HR)  

100 1 443 sipdir.online.lync.com.

_sipfederationtls._tcp SRV 3600 (1HR) 100 1 5061 sipfed.online.lync.com.

 

Mobile Device Management (MDM) requires (2) additional CNAME records.

Name Type TTL Data
enterpriseregistration CNAME 3600 (1HR) enterpriseregistration.windows.net.
enterpriseenrollment CNAME 3600 (1HR) enterpriseenrollment-s.manage.microsoft.com.

 

Step 7: Stop synchronization with Gmail

Once happy that mailflow is working as expected, you can remove the migration batch(es):

Remove-MigrationBatch -Identity $batchname -Confirm:$false

And alternatively the migration endpoint:

Remove-MigrationEndpoint -Identity $endpointname -Confirm:$false

 

PowerShell – Passing An Argument List Within A Script Block


$Options = @{'Variable'='test'}

Invoke-Command -ArgumentList $Options -ScriptBlock {
Param($Options)&{
Param(
[string]
$Variable
)
Write-Output -InputObject "ComputerName: $env:COMPUTERNAME"
Write-Output -InputObject "Variable: $Variable"
} @Options
} # End: Script block

# Sample Output:
# ComputerName: MY-LOCALHOST
# Variable: test

Thanks to this post:
http://stackoverflow.com/questions/28234509/powershell-splatting-the-argumentlist-on-invoke-command

PowerShell – Break vs. Continue Usage

For this example, we create two ArrayList objects and add a couple ints to them to run a loop to see if the value matches between the two and either continues a loop or breaks out of a loop:

$arraylist1 = New-Object -TypeName System.Collections.ArrayList
$arraylist1.Add(1) | Out-Null
$arraylist1.Add(2) | Out-Null
$arraylist1.Add(3) | Out-Null
$arraylist2 = New-Object -TypeName System.Collections.ArrayList
$arraylist2.Add(1) | Out-Null
$arraylist2.Add(2) | Out-Null
$arraylist2.Add(3) | Out-Null
Sample code for ‘continue’:
foreach ($item in $arraylist1)
{
    Write-Output -InputObject "item is: $item"
    foreach ($subitem in $arraylist2)
    {
        Write-Output -InputObject "subitem is: $subitem"
        if ($item -eq $subitem)
        {
            Write-Output -InputObject "$item matches $subitem! Continuing!"
            continue
        }
    }
}

…Which outputs:

item is: 1
subitem is: 1
1 matches 1! Continuing!
subitem is: 2
subitem is: 3
item is: 2
subitem is: 1
subitem is: 2
2 matches 2! Continuing!
subitem is: 3
item is: 3
subitem is: 1
subitem is: 2
subitem is: 3
3 matches 3! Continuing!
Sample code for ‘break’:
foreach ($item in $arraylist1)
{
    Write-Output -InputObject "item is: $item"
    foreach ($subitem in $arraylist2)
    {
        Write-Output -InputObject "subitem is: $subitem"
        if ($item -eq $subitem)
        {
            Write-Output -InputObject "$item matches $subitem! Breaking!"
            break
        }
    }
}

…which outputs:

item is: 1
subitem is: 1
1 matches 1! Breaking!
item is: 2
subitem is: 1
subitem is: 2
2 matches 2! Breaking!
item is: 3
subitem is: 1
subitem is: 2
subitem is: 3
3 matches 3! Breaking!

What can you use this for or when should I use what one? It depends on your use case. 🙂

C#, XAML, and System.Management.Automation Reference

Do you get an error attempting to create a XAML app referencing System.Management.Automation (which you need for PowerShell much like is described as in this link)? You also tried adding it in via the Add>Reference… option from within the Solution Explorer which only seems to have System.Management?

It displays something like this:
XAML - Using directive is unnecessary
How to fix:

Install the NuGet package and reference it in your project.

Alternatively:
1 – Save your XAML Project and close it.
2 – Using your favorite text editor, open the <MyXAMLApp>.csproj file and add the reference manually with:

<Reference Include=”System.Management.Automation” />

XAML - Editing XAMLApp.csproj
3 – Save and re-open your project. Boom! You can now create new instances:
PowerShell ps = PowerShell.Create();

PowerShell – Calculated Property Example & Export-Csv Formatting


Import-Module MSOnline
Connect-MSOLService -Credential (Get-Credential)
$groupid = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' # A GroupID with a known single Error.
$groupdetails = Get-MSOLGroup -ObjectID $groupid -ErrorAction 'Stop'
$groupdetails | `
Select-Object ObjectID,DisplayName,GroupType,`
@{ Label='ExpandedErrorCode';Expression={$PSItem.Errors[0].ErrorDetail.ObjectErrors.ErrorRecord.ErrorCode} },`
@{ Label='ExpandedErrorDescription';Expression={$PSItem.Errors[0].ErrorDetail.ObjectErrors.ErrorRecord.ErrorDescription} }
| `
Export-Csv -LiteralFile "$env:USERPROFILE\Desktop\MyGroupFile.csv" -NoTypeInformation
Remove-Variable groupid

Running $groupdetails | Get-Member shows Name of ‘Errors’ as a MemberType of: ‘Property’, with a Definition of: ‘System.Collections.Generic.List[Microsoft.Online.Administration.ValidationError] Errors {get;set;} ‘

PowerShell – Array vs. ArrayList Performance

Let’s take a look at performance when dealing with looping over an array variable; adding a new item and overall performance.
A standard array object in PowerShell is of a fixed size when created and cannot expand automatically. When you use the “+=” operator to append an entry, a new array object is created in memory with the additional overall previous array object. This leads to a large performance hit when dealing with a large number of items and increased memory consumption. For example:


$ArrayList = New-Object -TypeName 'System.Collections.ArrayList';
$Array = @();


Measure-Command
{
for($i = 0; $i -lt 10000; $i++)
{
$null = $ArrayList.Add("Adding item $i")
}
};
Measure-Command
{
for($i = 0; $i -lt 10000; $i++)
{
$Array += "Adding item $i"
}
};

Recommendation: use generics!

Modifying Multivalued Attributes With PowerShell

[int] $ADS_PROPERTY_CLEAR = 1;
[int] $ADS_PROPERTY_UPDATE = 2;
[int] $ADS_PROPERTY_APPEND = 3;
[int] $ADS_PROPERTY_DELETE = 4;

# Get the user's Distinguished Name (DN) however you prefer.
# Utilize the above option switch to adjust.
$ADuser = [ADSI]"LDAP://SERVER-NAME-HERE/$($theUserAccount.DistinguishedName)"          
$ADuser.PutEx($ADS_PROPERTY_DELETE, "proxyAddresses", @("smtp:$userNameForUpdate@contoso.com"))
$ADuser.PutEx($ADS_PROPERTY_APPEND, "proxyAddresses", @("SMTP:$userNameForUpdate@extranet.contoso.com"))

# Commit the changes to the AD User Object:
$ADuser.SetInfo()

Reference:
http://technet.microsoft.com/en-us/library/ee156515.aspx

PowerShell – Check For Matching Items In Seperate Arrays

$test1 = (1,2,3)
$test2 = (2,4,6)
$test3 = (1,3,5)
( ( $test1 + $test2 + $test3) | Group-Object |?{$_.Count -gt 1}).Values

Example outputs:

PS C:\> $test1 = (1,2,3)
$test2 = (2,4,6)
$test3 = (1,3,5)
( ( $test1 + $test2 + $test3) | Group-Object |?{$_.Count -gt 1}).Values
1
2
3
PS C:\>  

PowerShell Special Characters and Tokens (short-handing):
http://www.neolisk.com/techblog/powershell-specialcharactersandtokens