mail.psm1

function Search-MailFrom {
    <#
    .SYNOPSIS
        Search a mailbox by "from address"
    .DESCRIPTION
        Use a search string with a trailing wildcard(*) to filter the mailbox
    #>

    Param(
        [string]$Identity
        , # Cannot start with a wildcard, only end with *
        [string]$From
        , [datetime]$Start = (Get-date).Date
        , [datetime]$End = $Start.addDays(1).Date
        , [switch]$Delete
        , [string]$ResultTarget = $env:username
    )
    Search-Mailbox -identity $Identity -SearchQuery "received:$($Start.toString('yyyy-MM-dd'))..$($End.toString('yyyy-MM-dd')) AND from:`"$from`"" -TargetMailBox $ResultTarget -TargetFolder "Search" -DeleteContent:$Delete -Force:$Delete
}

function Search-MailSubject {
    <#
    .SYNOPSIS
        Search a mailbox by subject
    .DESCRIPTION
        Use a search string with a trailing wildcard(*) to filter the mailbox
    #>

    Param(
        [string]$Identity
        , # Cannot start with a wildcard, only end with *
        [string]$Subject
        , [datetime]$Start = (Get-date).Date
        , [datetime]$End = $Start.addDays(1).Date
        , [switch]$Delete
        , [string]$ResultTarget = $env:username
    )
    Search-Mailbox -identity $Identity -SearchQuery "received:$($Start.toString('yyyy-MM-dd'))..$($End.toString('yyyy-MM-dd')) AND subject:`"$Subject`"" -TargetMailBox $ResultTarget -TargetFolder "Search" -DeleteContent:$Delete
}

function Search-MailDate {
    <#
    .SYNOPSIS
        Search a users mailbox by day
    .EXAMPLE
        Search-MailDate -Identity <searchTarget>
 
        RunspaceId : xxxxxxxx-0000-1111-aaaa-1234abcd5678
        Identity : Domain/Name/Users/Group/searchTarget
        DisplayName : searchTarget
        TargetMailbox : Domain/Name/Users/Group/resultTarget
        TargetPSTFile :
        Success : True
        TargetFolder : \Search\searchTarget-19/10/2016 15:03:48
        ResultItemsCount : 29
        ResultItemsSize : 18.83 MB (19,742,786 bytes)
    #>

    Param(
        [string]$Identity
        , [datetime]$Start = (Get-date).Date
        , [datetime]$End = $Start.addDays(1).Date
        , [switch]$Delete
        , [string]$ResultTarget = $env:username
    )
    "received:$($Start.toString('yyyy-MM-dd'))..$($End.toString('yyyy-MM-dd'))"
    Search-Mailbox -identity $Identity -SearchQuery "received:$($Start.toString('yyyy-MM-dd'))..$($End.toString('yyyy-MM-dd'))" -TargetMailBox $ResultTarget -TargetFolder "Search" -DeleteContent:$Delete
}

$script:session
[bool]$script:import
function Import-MailServer {
    <#
    .SYNOPSIS
        Create the remote connection to the mail server as yourself.
    .DESCRIPTION
        Correctly open remote session to the exchange server which loads the exchange cmdlets.
 
        When you are finished remove the connection with Remove-MailServer
    .EXAMPLE
        Import-MailServer
        WARNING: The names of some imported commands from the module 'tmp_connection.hlk' include unapproved verbs that might
        make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the
        Verbose parameter. For a list of approved verbs, type Get-Verb.
 
        ModuleType Version Name ExportedCommands
        ---------- ------- ---- ----------------
        Script 1.0 tmp_connection.hlk {Add-ADPermission, Add-AvailabilityAddressSpace, Add-Conte...
        WARNING: Use 'Remove-Mailserver' to close the connection for other users to get on.
    #>

    Param(
        [string]
        $ComputerName = 'mailgate.birkdalehigh.co.uk',

        [PSCredential]
        $Credential,

        [ValidateSet('Script','Global')]
        $Scope = 'Global'
    )
    $test = Test-Connection $ComputerName -count 1 -ErrorAction Stop
    $host = [System.Net.Dns]::GetHostbyAddress($test.ProtocolAddress).HostName

    $script:session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "http://$host/PowerShell/" -Authentication Kerberos -Credential:$Credential
    if($Scope -eq 'Global'){
        Import-module (Import-PSSession $script:session) -Global
    } else {
        if(-not $script:import){
            Import-PSSession $script:session
            $script:import = $true
        }
    }
    Write-Warning "Use 'Remove-Mailserver' to close the connection for other users to get on."
}
function Get-MailServer {
    <#
    .SYNOPSIS
        Get the mailserver session
    .DESCRIPTION
        Use the existing session to import into to higher powershell scope to get access to all the exchange cmdlets
    .EXAMPLE
        Import-Mailserver; Import-PSSession (Get-MailServer)
    #>

    if ($script:session){
        return $script:session
    }
    else{
        Write-Error 'Cannot find open session, use Import-Mailserver'
    }
}
function Remove-MailServer {
    <#
    .SYNOPSIS
        Removes the imported remote connection to the mail server.
    .EXAMPLE
        Remove-MailServer
    #>

    Remove-PSSession $script:session
}

function Get-RecentFailedMessage {
    <#
    .SYNOPSIS
        Show failed messages sent to the exchange server in the last hour
    .DESCRIPTION
        Filter the message tracking log for failed messages not caught by Sophos i.e PmE12Transport.
 
        This filtering should be checked regularly as messages failing here will not show up anywhere else.
 
        This cmdlet wraps Get-MessageTrackingLog and requires the exchange cmdlets to be loaded. see `Import-MailServer`
    .EXAMPLE
        Get-RecentFailedMessage -FormatView
        Auto format the output to be easily read. Data not suitable for piping.
 
        Timestamp Sender Recipients MessageSubject
        --------- ------ ---------- --------------
        07/02/2017 11:17:42 Sent@example.com {demo@example.com} Re: Example
    .EXAMPLE
        Get-RecentFailedMessage -FormatView -IncludeSophos
        Auto format the output to be easily read. Includes the spam filter that normally has its own console.
 
        Timestamp SourceContext Sender Recipients MessageSubject
        --------- ------------- ------ ---------- --------------
        29/09/2017 11:33:40 PmE12Transport ABC@example.com {internal@example.com} Invoice
        29/09/2017 11:33:50 Sender Id Agent DEF@example.com {internal@example.com} RE: Quick Quest
 
        Data not suitable for piping.
    .EXAMPLE
        Get-RecentFailedMessage | select SourceContext,recipientstatus,sender,ClientIP
        Make a table of any column headers, do not use a format switch as if cannot be used down the pipeline.
 
        SourceContext RecipientStatus Sender ClientIP
        ------------- --------------- ------ ---------
        Sender Id Agent {550 5.7.1 Sender ID (PRA) Not Permitted} sent@example.com 127.0.0.1
    .EXAMPLE
        Get-RecentFailedMessage -IncludeSophos | measure
        Check the number of blocked messages in the last hour
 
        Count : 231
    .EXAMPLE
        Get-RecentFailedMessage -recipients userA@example.com
        Gets messages that have faild in the last hour for a specific user account.
 
        EventId Source Sender Recipients MessageSubject
        ------- ------ ------ ---------- --------------
        FAIL SMTP SpamSender@example.com {userA@example.com} Invoice
    .EXAMPLE
        Get-RecentFailedMessage | select -first 1 | format-list *
        Show all the property : values of one message object.
 
        PSComputerName : org-server.org.internal
        RunspaceId : 4e7dfba1-dd6a-4076-84f2-9ed78b888854
        PSShowComputerName : False
        Timestamp : 29/09/2017 11:38:05
        ClientIp : 222.127.163.110
        ClientHostname : ORG-SERVER
        ServerIp : 10.201.0.25
        ServerHostname :
        SourceContext : Sender Id Agent
        ConnectorId : ORG-SERVER\Default ORG-SERVER
        Source : SMTP
        EventId : FAIL
        InternalMessageId : 0
        MessageId : <2a228e10-9af9-83bc-aa16-7b526ca3e832@example.com>
        Recipients : {UserD@example.com}
        RecipientStatus : {550 5.7.1 Sender ID (PRA) Domain Does Not Exist}
        TotalBytes : 0
        RecipientCount : 1
        RelatedRecipientAddress :
        Reference :
        MessageSubject : Invoice
        Sender : Spammer@example.com
        ReturnPath : Spammer@example.com
        MessageInfo :
        MessageLatency :
        MessageLatencyType : None
        EventData :3
 
        Inspect every propery of a message. Usefull to find property names you might want to make a table off.
        i.e. Get-RecentFailedMessage | select ClientIp,RecipientCount,Sender,ReturnPath
    .NOTES
        SourceContext as PmE12Transport is the Sophos spam filter engine.
        SourceContext as Sender Id Agent is an exchange filter agent.
        RecipientStatus as "550 5.7.1 Sender ID (PRA) Not Permitted" is an Incorrect SPF record for sender domain and clientIP
        See Get-, Add-, Remove-BypassedSender
 
        Politely message the sending domain technical support their DNS is configured incorrectly.
    #>

    Param(
        # Past amound of hours to get results
        [parameter(position = 0)]
        [int]
        $PastHours = 1

        , # Filter for a specific recipient
        [parameter(position = 1)]
        [ValidatePattern('(?# User account includes domain suffix)^.*@.*$')]
        [Alias('Identity')]
        [string[]]
        $recipients

        , # Friendly view that cannot be consumed down the pipe
        [switch]
        $FormatView

        , # Include mail sent to the sophos agent
        [switch]
        $IncludeSophos
    )
    Process{
        $result = Get-MessageTrackingLog -Start (get-date).addHours(-$PastHours) -EventId "FAIL" -Recipients:$recipients
        if( -not $IncludeSophos){
            $filtered = $result | Where-Object sourceContext -ne 'PmE12Transport'
        }
        if($FormatView -and $IncludeSophos){
            Write-Output $result | format-table -AutoSize -property timestamp,sourceContext,sender,recipients,messagesubject
        } elseif ($FormatView){
            Write-Output $filtered | format-table -AutoSize -property timestamp,sender,recipients,messagesubject,RecipientStatus
        } elseif ($IncludeSophos){
            Write-Output $result
        } else {
            Write-Output $filtered
        }
    }
}

function Get-BypassedSender{
    <#
    .SYNOPSIS
        Show the list of domain names that bypass SPF record validation.
    .EXAMPLE
        Get-BypassedSender
        Show the current list of domains that bypass SPF record validation
 
        example.com
    #>


    Get-SenderIdConfig | select -expandproperty BypassedSenderDomains
}

function Add-BypassedSender{
    <#
    .SYNOPSIS
        Add domain names to the exclusion list
    .DESCRIPTION
        To add domains you need to append to the SenderIdConfig BypassedSenderDomains list.
        This command is a wrapper for that. This is because Set-SenderIdConfig with only the new domain, will replace the list.
    .NOTES
        In essence this wraps the oneliner:
        Set-SenderIdConfig -BypassedSenderDomains ( (Get-SenderIdConfig).BypassedSenderDomains += "example.com" )
    .EXAMPLE
        Add-BypassedSender example.com
        Returns no output for success.
    .EXAMPLE
        Add-BypassedSender '@test.com'
        Cannot validate argument on parameter 'Domain'. Add sender domain not email address.
    #>

    Param(
        [Parameter(Mandatory,
                   Position=0,
                   ValueFromPipeline)]
        [ValidateScript({
            if($psitem -match '@'){
                Throw 'Add sender domain not email address.'
            }
            return $true
        })]
        [string[]]
        $Domain
    )
    Set-SenderIDConfig -BypassedSenderDomains ($domain + (Get-BypassedSender))
}

function Remove-BypassedSender{
    <#
    .SYNOPSIS
        Remove a signle domain from the list.
    .EXAMPLE
        Remove-BypassedSender example.com
        No output for sucessful removal.
    .EXAMPLE
        Remove-BypassedSender potato
        Did not find domain(potato) to remove in list: ...,
    .EXAMPLE
        Get-BypassedSender | where {$_ -like '*.net'} | foreach { Remove-BypassedSender $_ }
        Remove domain that match this pattern.
    #>

    Param(
        [Parameter(Mandatory,
                   Position=0,
                   ValueFromPipeline)]
        [string]
        $Domain
    )
    begin{
        [System.Collections.ArrayList]$senders = Get-BypassedSender
        $before = $senders.count
    }
    Process{
        $senders.remove($Domain)
        if($before -gt $senders.count){
            Set-SenderIDConfig -BypassedSenderDomains $senders
        } else {
            Throw "Did not find domain($Domain) to remove in list: $($senders -join ', ')"
        }
    }
}

function Convert-DistributionGroupToSharedMailbox {
    [CmdletBinding()]
    <#
    .SYNOPSIS
        Recrete a group as a shared mailbox
    .DESCRIPTION
        A distribution gounp is a kind of address that exchanged cannot transform into a different type.
 
        This cmdlet takes a group, stores the address exchange uses to map the address lookup, removes the group and re-creates it as a shared mailbox. Then re-adds the address mapping.
    .EXAMPLE
        PS C:\> Get-DistributionGroup Finance | Convert-DistributionGroupToSharedMailbox
    .INPUTS
        Microsoft.Exchange.Data.Directory.Management.DistributionGroup
    .OUTPUTS
        Microsoft.Exchange.Data.Directory.Management.Mailbox
    .NOTES
        Order of operations.
        todo: test for exsiting distribution group
        todo: Check group membership to re-apply permissions on the mailbox
        Save LegacyExchangeDN attribute for outlook address book mappings
        Remove existing distribution group
        Create new shared mailbox in correct DN
        Add X500 of LegacyExchangeDN of distrubution group that was replaced
    #>

    Param(
        # Pipe Get-DistributionGroup to me.
        [Parameter(Mandatory)]
        [Microsoft.Exchange.Data.Directory.Management.DistributionGroup]
        $Group
    )
    Begin{
        Throw "Never tested. Not even once. Validate for yourself."
    }
    Process{
        $name = $Group.Name
        $Mapping = $Group.LegacyExchangeDN
        Remove-DistributionGroup -Identity $Name
        New-SharedMailbox -DisplayName $Name -Alias $Name | Set-Mailbox -EmailAddresses "X500:$Mapping"
    }
}
function New-SharedMailbox {
    <#
    .SYNOPSIS
        Create a Shared Mailbox with a default configuration
    .DESCRIPTION
 
    .EXAMPLE
        PS C:\> New-SharedMailbox -Name "AddressTitle" -DisplayName "Full Address Title" -Alias "address" -Users "personA", "PersonB"
        Name Alias
        ---- -----
        AddressTitle address
 
    .EXAMPLE
        PS C:\> New-SharedMailbox -Name reports -DisplayName Reports -Alias reports -Users OfficeAdmin,Receptionist
        Name Alias ServerName ProhibitSendQuota
        ---- ----- ---------- -----------------
        reports reports exchange01 unlimited
    .OUTPUTS
        Microsoft.Exchange.Data.Directory.Management.Mailbox
    .NOTES
        Order of operations;
        new shared mailbox in correct DN
        Assign mailbox send parameters
        Add persmissions for each user
            Check to add full send-from persmission from switch
        Add send on behalf permission
        Return mailbox
 
        Permissions:
        There's levels have been discovered in testing and not researched. Enterprise Administrators would be the top level that can do anything to exchange.
        The ability to Set-MailboxSentItemsConfiguration requires "Enterprise Administrators" group membership with the credentials used for import-mailserver.
        Creating the mailbox and assigning permissions requires only "Organization Management" membership.
    #>

    [CmdletBinding()]
    Param(
        # Will show as the name for the contact and mailbox
        [Parameter(Mandatory)]
        [string]
        $Name

        , # The name that appears in the Exchange Management Console under Recipient Configuration
        [string]
        $DisplayName

        , # Emaill address excluding @example.com
        [Parameter(Mandatory)]
        [alias('EmailAddress')]
        [ValidatePattern('.*[^@].*')]
        [string]
        $Alias

        , # Users that initially use the mailbox
        [string[]]
        $Users

        , # Path to shared mailbox user account, not an AD OU Path
        [string]
        [ValidatePattern('[^=;]')]
        $OrganizationalUnit = "bhs.internal/BHS/Mail Users"

        , # mailbox database storage location
        [string]
        $Database = "BHS Staff Mailbox Database"

        , # Users will get permission to send "<user> On behald of <account>", directly from <account> or not at all.
        [ValidateSet('None', 'Behalf', 'From')]
        [string]
        $Send = 'Behalf'
    )
    Begin{
    }
    Process{
        New-MailBox -Name $Name -DisplayName $DisplayName -Alias $alias -OrganizationalUnit $OrganizationalUnit -Database $Database -UserPrincipalName "$Alias@bhs.internal" -Shared
        $mailbox = Get-Mailbox -Identity $Name
        # By default sent items will only show in the senders account. This forces them into the shared sent items folder.
        Set-MailboxSentItemsConfiguration -Identity $mailbox.Identity -SendAsItemsCopiedTo 'SenderAndFrom' -SendOnBehalfOfItemsCopiedTo 'SenderAndFrom'
        $users | ForEach-Object {
            # Mailbox permissions allow the user to perform actions
            Add-MailBoxPermission $mailbox.Identity -AccessRights FullAccess -InheritanceType All -User $PSItem
            # AD permissions allow the users account#new (outlook) to find the mailbox
            Add-ADPermission $mailbox.Identity -ExtendedRights "Receive-As" -User $PSItem
            if($Send -eq 'From'){
                Add-ADPermission $mailbox.Identity -ExtendedRights "Send-As" -User $PSItem
            }
        }
        if($Send -eq 'Behalf'){
            # Send 'From' permissions will supersede this "On behalf" setting.
            # -GrantSendOnBehalfTo only replaces the list, you must append your own users list first.
            Set-Mailbox -Identity $mailbox.Identity -GrantSendOnBehalfTo $users > $null
            Write-Warning "The permission for users to send 'From' will supersede this permission, check their AD-Permissions for 'Send-As'"
        }
    }
    End{
        Write-Output $mailbox
    }
}

Export-ModuleMember -Function "Get-*", "Search-*", "New-*", "Add-*", "Remove-*"