Remediation/ExchangeOnline/EXO-RECIPIENTS.ps1

# UNTESTED: this script has never been run on a real tenant. Read it, try it on a test tenant first, and use it at your own risk.
<#
.SYNOPSIS
    Recreates in the target the chosen lists and contacts, then gives them back their original addresses (procedure EXO-RECIPIENTS).
 
.DESCRIPTION
    RELEASE-DOMAIN releases dynamic lists, contacts and mail users in the source; this script recreates them and gives the addresses back.
 
    Data read: lists, dynamic lists and contacts of each source, matrices. Guests also show as mail users: they are set aside, they
    belong to the invitations.
 
    Decision: decisions\EXO-RECIPIENTS.csv, Recreate or Drop. The Note column completes the decision:
      list with no manager found: the address of an owner in the target;
      dynamic list : the rewritten filter, required, because a filter of the source does not aim at the right objects.
 
    Three steps, the same script runs again:
      1. before the cutover: read the addresses of the source into decisions\EXO-RECIPIENTS-ORIGIN.csv. This read must come before
         RELEASE-DOMAIN: the aliases of lists and contacts are not in the snapshot;
      2. before the cutover: creation with the prefix of the tenant, contacts first, because a list can contain them;
      3. after the domains are verified: adding the original addresses. While the domain is not accepted in the target, the action is postponed.
    Without the read, step 3 only takes the primary address of the list, and an alert says so.
 
    Limits: members are read in the source at run time; a member with no target account is reported and not added. A chosen mail user
    is recreated as a contact: it has no mailbox and no account to carry over.
#>

$c = Open-TcRemediation

$AddressFile = 'EXO-RECIPIENTS-ORIGIN'
$TargetDomains = @(Get-TcRemediationTable 'Target' 'acceptedDomains' | ForEach-Object { ([string]$_['Domain']).ToLowerInvariant() })
$TargetNames = @(Get-TcRemediationTable 'Target' 'groups' | ForEach-Object { $_['Name'] })
$TargetContacts = @(Get-TcRemediationTable 'Target' 'contacts' | ForEach-Object { ([string]$_['External address'] -replace '^smtp:', '').ToLowerInvariant() })

function Get-TargetRecipient {
    <# Recipient of the target, or $null. #>
    param([string]$Identity)
    try { return (Invoke-TcServiceCommand -Tenant 'Target' -Service Exo -Command 'Get-Recipient' -Parameters @{ Identity = $Identity }) } catch { return $null }
}

function Get-TargetAlias {
    param([string]$Prefix, [string]$Alias)
    return ('{0}-{1}' -f $Prefix.ToLowerInvariant(), ($Alias -replace '[^A-Za-z0-9\.\-_]', '')).ToLowerInvariant()
}

function Find-TargetAccountList {
    <# Possible target identities of a source account: Target UPN, then Temporary UPN for an account not cut over yet. #>
    param([string]$Tenant, [string]$Address)
    $b = Get-TcRemediationTable $Tenant 'mailboxes' | Where-Object { $_['Primary address'] -eq $Address -or $_['UPN'] -eq $Address -or $_['ObjectId'] -eq $Address } | Select-Object -First 1
    $key = if ($b) { $b['UPN'] } else { $Address }
    $l = @((Get-TcMember (Get-TcRemediationIdentity $Tenant) 'rows')) | Where-Object { $_['Source UPN'] -eq $key } | Select-Object -First 1
    if (-not $l -or $l['Decision'] -notin 'Create', 'Duplicate') { return @() }
    return @(@($l['Target UPN'], $l['Temporary UPN']) | Where-Object { $_ })
}

function Resolve-TargetAccount {
    <# Live recipient of the target for a source account; $null if the account is not there yet. #>
    param([string]$Tenant, [string]$Address)
    foreach ($i in Find-TargetAccountList $Tenant $Address) {
        $r = Get-TargetRecipient $i
        if ($r) { return $r }
    }
    return $null
}

$Sources = @{}
$proposals = foreach ($t in $c.Tenants) {
    Write-TcRemediationTenant $t
    $i = Get-TcRemediationTenant $t
    $lists = @(Get-TcRemediationTable $t 'distributionGroups')
    $dynamicLists = @(Get-TcRemediationTable $t 'dynamicGroups')
    $contacts = @(Get-TcRemediationTable $t 'contacts')
    if (-not ($lists.Count + $contacts.Count) -and -not (Test-TcCollectionComplete $t 'contacts')) { Write-TcRemediationAlert 'Recipient collection incomplete: run Invoke-TcAssessment again'; continue }
    $guests = @(Get-TcRemediationTable $t 'users' | Where-Object { $_['Type'] -eq 'Guest' } | ForEach-Object { ([string]$_['Email']).ToLowerInvariant() })
    $chosenRows = @($contacts | Where-Object { $_['Type'] -eq 'Contact' -or ([string]$_['External address'] -replace '^smtp:', '').ToLowerInvariant() -notin $guests })
    Write-TcRemediationNote ("{0} list(s), {1} dynamic list(s), {2} contact(s) and mail user(s), {3} guest(s) set aside" -f $lists.Count, $dynamicLists.Count, $contacts.Count, ($contacts.Count - $chosenRows.Count))
    $Sources[$t] = @{ Lists = $lists; DynamicLists = $dynamicLists; Contacts = $chosenRows }

    foreach ($l in $lists) {
        $already = ('{0}-{1}' -f (Get-TcMember $i 'Prefix'), $l['Name']) -in $TargetNames
        [ordered]@{ Tenant = $t; Kind = 'List'; Name = $l['Name']; Address = $l['Address']; 'Detail' = ("{0} member(s)" -f $l['Members']); Proposal = $(if ($l['Name'] -match '^checkpoint_' -or $already) { 'Drop' } else { 'Recreate' }) }
    }
    foreach ($l in $dynamicLists) {
        [ordered]@{ Tenant = $t; Kind = 'Dynamic list'; Name = $l['Name']; Address = $l['Address']; 'Detail' = $l['Filter']; Proposal = '' }
    }
    foreach ($l in $chosenRows) {
        $address = [string]$l['External address'] -replace '^smtp:', ''
        $internal = (Get-TcAddressDomain $address) -in $TargetDomains
        $knownOne = $address.ToLowerInvariant() -in $TargetContacts
        [ordered]@{ Tenant = $t; Kind = $l['Type']; Name = $l['Name']; Address = $address; 'Detail' = $(if ($internal) { 'Address already in target' } elseif ($knownOne) { 'Contact already in target' } else { '' })
                    Proposal = $(if ($internal -or $knownOne) { 'Drop' } elseif ($l['Type'] -eq 'Contact') { 'Recreate' } else { '' }) }
    }
}

$decisions = @(Get-TcDecision -Name 'EXO-RECIPIENTS' -Proposal @($proposals) -Key 'Tenant', 'Kind', 'Address' -Choice 'Recreate', 'Drop')
$chosenPolicies = @($decisions | Where-Object { (Get-TcMember $_ 'Tenant') -in $c.Tenants -and (Get-TcMember $_ 'Decision') -eq 'Recreate' })

$Backup = @()
if (Test-TcDecisionFile $AddressFile) { $Backup = @(Read-TcDecisionRow $AddressFile) }

foreach ($t in @($chosenPolicies | ForEach-Object { (Get-TcMember $_ 'Tenant') } | Select-Object -Unique)) {
    if (-not @($Backup | Where-Object Tenant -eq $t).Count) { Write-TcRemediationNote ("{0}: addresses still to read; until then, only the primary address of the lists is planned" -f $t) }
    Add-TcBlockAction -Tenant $t -Service Exo -Object 'Addresses read' -Data @{ Domains = @((Get-TcMember (Get-TcRemediationTenant $t) 'Domains')) } -Description ("Read the addresses of the objects kept in {0}, before RELEASE-DOMAIN" -f $t) `
        -Reason "Read only in the source; the result stays in decisions\ for the third step." `
        -Test { param($a) @($Backup | Where-Object Tenant -eq $a.tenant).Count -gt 0 } `
        -Block {
            param($a)
            $rows = [System.Collections.Generic.List[object]]::new()
            foreach ($d in $chosenPolicies | Where-Object Tenant -eq $a.tenant) {
                $command = switch ((Get-TcMember $d 'Kind')) { 'List' { 'Get-DistributionGroup' } 'Dynamic list' { 'Get-DynamicDistributionGroup' } 'Contact' { 'Get-MailContact' } default { 'Get-MailUser' } }
                try { $o = Invoke-TcServiceCommand -Tenant $a.tenant -Service Exo -Command $command -Parameters @{ Identity = (Get-TcMember $d 'Address') } } catch { Write-TcRemediationAlert ("{0} not found in the source: addresses not read" -f (Get-TcMember $d 'Address')); continue }
                foreach ($x in @((Get-TcMember $o 'EmailAddresses') | Where-Object { $_ -match '^smtp:' } | ForEach-Object { $_ -replace '^smtp:', '' })) {
                    if ((Get-TcAddressDomain $x) -in $a.data.Domains) { $rows.Add([pscustomobject][ordered]@{ Tenant = $a.tenant; Kind = (Get-TcMember $d 'Kind'); Name = (Get-TcMember $d 'Name'); Address = $x.ToLowerInvariant() }) }
                }
            }
            @(@($script:TcRemediationRun['Backup']) + @($rows)) | Export-TcDecisionRow $AddressFile
            $script:TcRemediationRun['Backup'] = @(Read-TcDecisionRow $AddressFile)
        }
}

foreach ($d in $chosenPolicies | Where-Object { (Get-TcMember $_ 'Kind') -notin 'List', 'Dynamic list' }) {
    $i = Get-TcRemediationTenant (Get-TcMember $d 'Tenant')
    $name = '{0}-{1}' -f (Get-TcMember $i 'Prefix'), (Get-TcMember $d 'Name')
    $alias = Get-TargetAlias (Get-TcMember $i 'Prefix') (Get-TcAddressPrefix (Get-TcMember $d 'Address'))
    Add-TcCommandAction -Tenant 'Target' -Service Exo -Command 'New-MailContact' -Parameters @{ Name = $name; ExternalEmailAddress = (Get-TcMember $d 'Address'); Alias = $alias } -Object $name -Description ("Create the contact {0} to {1}" -f $name, (Get-TcMember $d 'Address')) `
        -Test { param($a) [bool]((Get-TargetRecipient $a.parameters.Name) -or (Get-TargetRecipient $a.parameters.ExternalEmailAddress)) } `
        -Undo @{ command = 'Remove-MailContact'; parameters = @{ Identity = $name } }
}

foreach ($d in $chosenPolicies | Where-Object Kind -eq 'List') {
    $t = (Get-TcMember $d 'Tenant')
    $i = Get-TcRemediationTenant $t
    $l = (Get-TcMember $Sources[$t] 'Lists') | Where-Object { $_['Address'] -eq (Get-TcMember $d 'Address') } | Select-Object -First 1
    if (-not $l) { Write-TcRemediationAlert ("{0}: list missing from the snapshot" -f (Get-TcMember $d 'Address')); continue }
    $name = '{0}-{1}' -f (Get-TcMember $i 'Prefix'), (Get-TcMember $d 'Name')
    $managers = @([string]$l['Managers'] -split ';\s*' | Where-Object { $_ })
    $note = ((Get-TcText $d 'Note')).Trim()
    if (-not $note -and -not @($managers | Where-Object { @(Find-TargetAccountList $t $_).Count }).Count) { Write-TcRemediationAlert ("{0}: no manager found in the matrix, enter an owner from the target in the Note column. List not created." -f (Get-TcMember $d 'Address')); continue }
    Add-TcBlockAction -Tenant 'Target' -Service Exo -Object $name `
        -Data @{ Source = $t; Name = $name; Alias = (Get-TargetAlias (Get-TcMember $i 'Prefix') $l['Alias']); Address = (Get-TcMember $d 'Address'); Managers = $managers; Note = $note; Prefix = (Get-TcMember $i 'Prefix'); ExternalUsers = (ConvertTo-TcBoolean $l['External senders allowed']); Concealed = (ConvertTo-TcBoolean $l['Hidden']) } `
        -Description ("Create the list {0} ({1} member(s) in the source)" -f $name, $l['Members']) `
        -Test { param($a) [bool](Get-TargetRecipient $a.data.Name) } `
        -Undo @{ command = 'Remove-DistributionGroup'; parameters = @{ Identity = $name; BypassSecurityGroupManagerCheck = $true } } `
        -Block {
            param($a)
            $x = $a.data
            $memberRows = [System.Collections.Generic.List[string]]::new()
            $missing = [System.Collections.Generic.List[string]]::new()
            $sources = @(Invoke-TcServiceCommand -Tenant (Get-TcMember $x 'Source') -Service Exo -Command 'Get-DistributionGroupMember' -Parameters @{ Identity = (Get-TcMember $x 'Address'); ResultSize = 'Unlimited' })
            foreach ($m in $sources) {
                $address = (Get-TcText $m 'PrimarySmtpAddress')
                $r = if ((Get-TcText $m 'RecipientTypeDetails') -match 'Contact') { Get-TargetRecipient ('{0}-{1}' -f (Get-TcMember $x 'Prefix'), (Get-TcMember $m 'Name')) } else { Resolve-TargetAccount (Get-TcMember $x 'Source') $address }
                if ($r) { $memberRows.Add((Get-TcText $r 'PrimarySmtpAddress')) } else { $missing.Add($address) }
            }
            $owners = @(if ((Get-TcMember $x 'Note')) { (Get-TcMember $x 'Note') } else { foreach ($g in (Get-TcMember $x 'Managers')) { $r = Resolve-TargetAccount (Get-TcMember $x 'Source') $g; if ($r) { (Get-TcText $r 'PrimarySmtpAddress') } } })
            if (-not $owners.Count) { throw ("No owner found in the target for {0}: enter their address in the Note column" -f (Get-TcMember $x 'Name')) }
            $p = @{ Name = (Get-TcMember $x 'Name'); Alias = (Get-TcMember $x 'Alias'); Type = 'Distribution'; ManagedBy = $owners; RequireSenderAuthenticationEnabled = (-not (Get-TcMember $x 'ExternalUsers')) }
            if ($memberRows.Count) { $p.Members = @($memberRows) }
            Invoke-TcServiceCommand -Tenant 'Target' -Service Exo -Command 'New-DistributionGroup' -Parameters $p | Out-Null
            if ((Get-TcMember $x 'Concealed')) { Invoke-TcServiceCommand -Tenant 'Target' -Service Exo -Command 'Set-DistributionGroup' -Parameters @{ Identity = (Get-TcMember $x 'Name'); HiddenFromAddressListsEnabled = $true; BypassSecurityGroupManagerCheck = $true } | Out-Null }
            if ($missing.Count) { Write-TcRemediationAlert ("{0}: {1} member(s) with no account in the target, to add after their cutover: {2}" -f (Get-TcMember $x 'Name'), $missing.Count, ($missing -join ', ')) }
        }
}

foreach ($d in $chosenPolicies | Where-Object Kind -eq 'Dynamic list') {
    $i = Get-TcRemediationTenant (Get-TcMember $d 'Tenant')
    $name = '{0}-{1}' -f (Get-TcMember $i 'Prefix'), (Get-TcMember $d 'Name')
    $filterValue = ((Get-TcText $d 'Note')).Trim()
    if (-not $filterValue) { Write-TcRemediationAlert ("{0}: enter the rewritten filter in the Note column. A filter that is too broad would send to the whole organization. List not created." -f (Get-TcMember $d 'Address')); continue }
    Add-TcCommandAction -Tenant 'Target' -Service Exo -Command 'New-DynamicDistributionGroup' -Parameters @{ Name = $name; Alias = (Get-TargetAlias (Get-TcMember $i 'Prefix') (Get-TcAddressPrefix (Get-TcMember $d 'Address'))); RecipientFilter = $filterValue } -Object $name `
        -Description ("Create the dynamic list {0}" -f $name) `
        -Test { param($a) [bool](Get-TargetRecipient $a.parameters.Name) } `
        -Undo @{ command = 'Remove-DynamicDistributionGroup'; parameters = @{ Identity = $name } }
}

foreach ($d in $chosenPolicies) {
    $i = Get-TcRemediationTenant (Get-TcMember $d 'Tenant')
    $readItems = @($Backup | Where-Object { (Get-TcMember $_ 'Tenant') -eq (Get-TcMember $d 'Tenant') -and (Get-TcMember $_ 'Kind') -eq (Get-TcMember $d 'Kind') -and (Get-TcMember $_ 'Name') -eq (Get-TcMember $d 'Name') } | ForEach-Object { (Get-TcMember $_ 'Address') })
    if (-not @($Backup | Where-Object Tenant -eq (Get-TcMember $d 'Tenant')).Count -and (Get-TcMember $d 'Kind') -in 'List', 'Dynamic list' -and (Get-TcAddressDomain (Get-TcMember $d 'Address')) -in (Get-TcMember $i 'Domains')) { $readItems = @((Get-TcMember $d 'Address').ToLowerInvariant()) }
    $name = '{0}-{1}' -f (Get-TcMember $i 'Prefix'), (Get-TcMember $d 'Name')
    $command = switch ((Get-TcMember $d 'Kind')) { 'List' { 'Set-DistributionGroup' } 'Dynamic list' { 'Set-DynamicDistributionGroup' } default { 'Set-MailContact' } }
    foreach ($x in $readItems) {
        $addition = 'smtp:{0}' -f $x
        $addressAddition = @{ Identity = $name; EmailAddresses = @{ Add = $addition } }
        $returnValue = @{ Identity = $name; EmailAddresses = @{ Remove = $addition } }
        if ($command -eq 'Set-DistributionGroup') { $addressAddition.BypassSecurityGroupManagerCheck = $true; $returnValue.BypassSecurityGroupManagerCheck = $true }
        Add-TcCommandAction -Tenant 'Target' -Service Exo -Command $command -Parameters $addressAddition -Object $name -Description ("Give {0} back its original address {1}" -f $name, $x) `
            -Test {
                param($a)
                $address = [string]$a.parameters.EmailAddresses.Add -replace '^smtp:', ''
                $domain = Get-TcAddressDomain $address
                if (-not (Test-TcTargetDomain $domain)) { return Test-TcPostponed $a ("{0} is not accepted in the target: run again after the domains are verified in the target" -f $domain) }
                $o = Get-TargetRecipient $a.parameters.Identity
                if (-not $o) { return Test-TcPostponed $a ("{0} does not exist in the target yet" -f $a.parameters.Identity) }
                return @((Get-TcMember $o 'EmailAddresses') | Where-Object { ($_ -replace '^smtp:', '') -eq $address }).Count -gt 0
            } `
            -Undo @{ command = $command; parameters = $returnValue }
    }
}

Invoke-TcRemediationPlan -Verify {
    param($t)
    $i = Get-TcRemediationTenant $t
    foreach ($d in $chosenPolicies | Where-Object Tenant -eq $t) {
        $name = '{0}-{1}' -f (Get-TcMember $i 'Prefix'), (Get-TcMember $d 'Name')
        $o = Get-TargetRecipient $name
        if (-not $o -and (Get-TcMember $d 'Kind') -notin 'List', 'Dynamic list') { $o = Get-TargetRecipient (Get-TcMember $d 'Address') }
        if ($o) { New-TcRemediationResult OK ("{0} present: {1}" -f $name, (Get-TcMember $o 'PrimarySmtpAddress')) } else { New-TcRemediationResult Warning ("{0} missing from the target" -f $name) }
    }
}
Close-TcRemediation