Send-Email.ps1

function Send-Email {
    <#
    .Synopsis
        Sends an email.
    .Description
        Sends an email, using exchange web services.
         
    .Example
        $office365Credential = Get-SecureSetting -Name Office365Cred -Type ([Management.Automation.PSCredential])
        Send-Email -To "someone@somewhere.com" -Subject "Hello World" -Body "<b>Hello World</b>" -BodyAsHtml -IsOffice365Account -Credential $office365Credential
    .Example
        $office365Credential = Get-SecureSetting -Name Office365Cred -Type ([Management.Automation.PSCredential])
        Send-Email -To "someone@somewhere.com" -Subject "Dinner:Free at Five?" -From "5PM" -To "6pm" -Location "Machiavelli's" -IsOffice365Account -Credential $office365Credential
    .Link
        New-WebServiceProxy
    #>
    
    [OutputType([Nullable],[Management.Automation.Job])]    
    param(            
    # The to address or addresses for the message
    [Parameter(Position=0,ValueFromPipelineByPropertyName=$true)]
    [System.String[]]
    $To,
    
    # The replyTo email address
    [Parameter(Position=0,ValueFromPipelineByPropertyName=$true)]
    [System.String[]]
    $ReplyTo,
    
    # The path to any attachments
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [Alias('PsPath')]    
    [string[]]
    $Attachment,

    # Cc Addresses for the message
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [System.String[]]
    $Cc,

    # The Bcc address for the message
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [System.String[]]
    $Bcc,
    
    # The response type. Used to send cancellations and acceptance.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    $ResponseType,

    # The item class. This parameter is set automatically in most cases, but can be used to send meeting cancellations or meeting accepts.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    $ItemClass,

    # The body of the message
    [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
    [System.String]
    $Body,

    # If set, the body will be treated as HTML
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [Alias('BAH')]
    [Switch]
    $BodyAsHtml,

    # If set, will set the message encoding
    [Alias('BE')]
    [System.Text.Encoding]
    $Encoding,

    # Delivery Notification Options. Automatically generates a response mail
    [Alias('DNO')]
    [System.Net.Mail.DeliveryNotificationOptions]
    $DeliveryNotificationOption,

    # The from parameter. If not set, from will automatically be set by exchange to be the logged-on user.
    # Users can only send mail as a user once they have send permissions
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [System.String]
    $From,

    # The subject line for the message
    [Parameter(Position=1,ValueFromPipelineByPropertyName=$true)]
    [Alias('sub')]
    [System.String]
    $Subject,

    # The message sensitivity
    [string]
    [ValidateSet("Normal","Personal","Private","Confidential")]
    $Sensitivity = "Normal",
        
    # The category. Any category can be used, but only the built-in categories are guaranteed to be color-coded in a client.
    # Built in categories have are named 'Color' Cateogry, for example, 'Blue Category' or 'Red Category'
    [System.String[]]
    $Category,
    
    # The end time of th meeting
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [DateTime]
    $Start,
    
    # The end time of the meeting.
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [DateTime]
    $End,
    
    # The location of the meeting
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [string]
    $Location,

    # The Calender Identifier. This is used to
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [string]
    $CalenderId,

    # If set, outputs the response object returned from the exchange web services
    [switch]
    $PassThru,
    
    # The exchange server
    [string]$ExchangeServer,
    
    # The credential used to connect
    [Management.Automation.PSCredential]
    $Credential,
    
    # If set, will treat the account as an Office365 account
    [Switch]$IsOffice365Account,
    
    # If set, will use the trio of web.config values for the connection:
    # - ExchangeServer, ExchangeUserName, ExchangePassword
    [Switch]
    $UseWebConfiguration,
    
    # If set, will create the email in a background job
    [Switch]
    $AsJob
    )
    
    begin {
function Connect-Exchange
{
    [CmdletBinding(DefaultParameterSetName='ExchangeServer')]
    param(        
    [Parameter(Mandatory=$true,Position=0,ParameterSetName='ExchangeServer')]
    [Parameter(Mandatory=$true,Position=0,ParameterSetName='Office365')]
    [Management.Automation.PSCredential]
    $Account,
    
    [Parameter(Mandatory=$true,Position=1,ParameterSetName='ExchangeServer')]
    [Parameter(Mandatory=$true,Position=1,ParameterSetName='UseDefaultCredential')]    
    [string]
    $ServerName,
        
    [Parameter(Mandatory=$true,Position=1,ParameterSetName='Office365')]
    [Switch]
    $IsOffice365Account,
    
    [Parameter(Mandatory=$true,Position=1,ParameterSetName='UseDefaultCredential')]
    [Switch]
    $UseDefaultCredential,
    
    # If set, will attempt to connect to a remote powershell session as well as exchange web services
    [Switch]
    $ForAdministration
    )
    
    
    process {
        if (-not $UseDefaultCredential) {
            if ($account.username -match "(.+)@(.+)") 
            {
                $username = $matches[1]
                $script:hostedExchangeEmail = $account.UserName
            }
            else
            {
                $username = $account.username
            }
        }
        
        if ($psCmdlet.ParameterSetName -eq 'Office365') {
            if ($script:ExchangeWebService -and $script:CachedCredential.Username -eq $script:CachedCredential) {
                return
            }    
            $ExchangeServer = "https://ps.outlook.com/"
            Write-Progress "Connecting to Office365" "$exchangeServer"
            $script:CachedCredential = $Account
        
            $newSessionParameters = @{
                ConnectionUri='https://ps.outlook.com/powershell'
                ConfigurationName='Microsoft.Exchange'
                Authentication='Basic'           
                Credential=$Account
                AllowRedirection=$true
                WarningAction = "silentlycontinue"
                SessionOption=(New-Object Management.Automation.Remoting.PSSessionOption -Property @{OpenTimeout="00:30:00"})
            }
            
            $Session = New-PSSession @newSessionParameters -WarningVariable warning 
            if (-not $Session) { return } 
            if ($warning  -and $warning.count -gt 0 )
            {
                $message = $warning[$warning.count-1].Message
                if ($message -match "(https?://[a-zA-Z0-9\-\.]+\.(com|org|net|mil|edu|COM|ORG|NET|MIL|EDU))" ) { 
                    $finalserver =  $matches[0] 
                }
            }
            
            $u = $userName.TrimStart("\")
            $samaccountname = Invoke-Command -Session $Session -ScriptBlock {param($u) get-mailbox -Identity $u} -ArgumentList $u |
                Select-Object -ExpandProperty SamAccountName

            $Account = New-Object System.Management.Automation.PSCredential $samaccountname, $Account.Password
            $FinalUri = $finalserver + '/ews/exchange.asmx'
            
            Remove-PSSession -Session $session
            $ExchangeServer = "https://ps.outlook.com/"            
        } else { 
            $ExchangeServer = $ServerName
        }
               
        if ($script:ExchangeWebService) { return } 
        Write-Progress "Connecting to Exchange" "$exchangeServer"
        $uri = "$ExchangeServer".TrimEnd('/') + '/ews/exchange.asmx'
        $wsdl = "$ExchangeServer".TrimEnd('/') + '/ews/services.wsdl'
        $script:ExchangeWebService  = $null
        $script:ExchangeWebServiceNamespace = $null
        if ($finaluri) { 
            $wsdl = "$finalUri".Replace('/ews/exchange.asmx', '/ews/services.wsdl')
        } 
                
        if ($Account) {
            $script:ExchangeWebService = New-WebServiceProxy -Uri $wsdl -Credential $Account 
        } else {
            $script:ExchangeWebService= New-WebServiceProxy -Uri $wsdl -UseDefaultCredential 
        }
        
        if (-not $script:ExchangeWebService) {             
            return 
        }             
        
        Write-Progress "Connected to Exchange!" "$exchangeServer"
        
        $script:ExchangeWebServiceNamespace = $exchangeWebService.GetType().Namespace

        $script:exchangeWebService.RequestServerVersionValue = New-object "$ExchangeWebServiceNamespace.RequestServerVersion"
        if (-not $exchangeVersion) {
            $exchangeVersion = ([int[]][Enum]::GetValues(("$ExchangeWebServiceNamespace.ExchangeVersionType" -as [Type]))) | 
                Sort-Object | 
                Select-Object -Last 1 
        }
        $script:exchangeWebService.RequestServerVersionValue.Version = 
            ("$ExchangeWebServiceNamespace.ExchangeVersionType" -as [Type])::$exchangeVersion
        
        if ($FinalUri) {
            $script:exchangeWebService.Url = $finalUri
        } else {
            $script:exchangeWebService.Url = $uri                   
        }
        
        # If an account was set, make sure the web service has the credential information
        if ($Account) {
            $script:exchangeWebService.Credentials = $Account.GetNetworkCredential()
        }        
        
        
        # Find the correct time zone and set it, otherwise everything comes from UTC
        $tzRequestType = New-Object "$ExchangeWebServiceNamespace.GetServerTimeZonesType" -Property @{
            ReturnFullTimeZoneData=$true;
            ReturnFullTimeZoneDataSpecified=$true
        }
        $tzRequestType.Ids = "$([Timezone]::CurrentTimeZone.StandardName)"        
        $response = $script:exchangeWebService.GetServerTimeZones($tzRequestType)        
        # If the time zone was found, change the web service's time zone context.
              
        if ($response.responsemessages.items[0].ResponseClass -eq 'Success'){
            $tzd =    $response.responsemessages.items[0].timezonedefinitions
            $script:MyTimeZone = $tzd.TimeZoneDefinition
            $tzContext = New-Object "$ExchangeWebServiceNamespace.TimeZoneContextType"
            $tzContext.TimeZoneDefinition = $tzd.TimeZoneDefinition[0]
            $script:exchangeWebService.TimeZoneContext = $tzContext
        }
        
        
        if ($ForAdministration -and -not $script:administrationSession) {        
            $newSessionParameters = @{
                ConnectionUri='https://$exchangeServer/powershell'
                ConfigurationName='Microsoft.Exchange'
                Authentication='Basic'           
                Credential=$Account
                AllowRedirection=$true
                WarningAction = "silentlycontinue"
                SessionOption=(New-Object Management.Automation.Remoting.PSSessionOption -Property @{OpenTimeout="00:30:00"})
            }
            
            $Session = New-PSSession @newSessionParameters -WarningVariable warning         
        }
        
        
        
        if ($PassThru) {
            New-Object PSObject | 
                Add-Member NoteProperty ExchangeWebService $script:exchangeWebService -PassThru |
                Add-Member NoteProperty ExchangeWebService $script:administrationSession -PassThru # |
        }
    }
} 
        
    }
    
    process {        
        if ($UseWebConfiguration) {
            $exchangeServer = Get-WebConfigurationSetting -Setting ExchangeServer
            $exchangeusername = Get-WebConfigurationSetting -Setting ExchangeUsername
            $exchangepassword = Get-WebConfigurationSetting -Setting ExchangePassword |
                ConvertTo-SecureString -AsPlainText -Force           
            $credential  = New-Object Management.Automation.PSCredential $exchangeusername, $exchangepassword
            
            $psboundparameters.credential = $credential  
            if ($exchangeServer -like "*ps.outlook.com*") {
                $isOffice365Account = $true
                $psboundparameters.isOffice365Account = $true                
            } else {
                $psboundparameters.exchangeserver = $exchangeServer
            }
            
        }
        
        if ($AsJob) {
            $myDefinition = [ScriptBLock]::Create("function Send-Email {
$(Get-Command Send-Email | Select-Object -ExpandProperty Definition)
}
"
)
            $null = $psBoundParameters.Remove('AsJob')            
            $myJob= [ScriptBLock]::Create("" + {
                param([Hashtable]$parameter) 
                
            } + $myDefinition + {
                
                Send-Email @parameter
            }) 
            
            Start-Job -ScriptBlock $myJob -ArgumentList $psBoundParameters 
            return
        }

        
    
        if ($Credential -and ($ExchangeServer -or $IsOffice365Account)) {
            $connectParams = @{Account=$credential}
            if (-not $IsOffice365Account) {
                $connectParams += @{ServerName=$ExchangeServer}
            } else {
                $connectParams += @{IsOffice365Account=$IsOffice365Account}
            }
            Connect-Exchange @connectParams
        }
        if (-not $script:ExchangeWebService) {
            throw "Must be connected to exchange to Send-Email"
            return                                                
        }
        
        
        # This begs for documentation.
        # A small piece of information on the web service request is the message disposition.
        # You can SendOnly (makes some sense), SendAndSaveCopy (a good default), and SaveOnly.
        # SaveOnly made no sense to me when I first saw it, and I had to discover the horrible quirks
        # of exchange in the process. When you attach things, you actually "send" things, and then attach things
        # to them, and then send again. This is one of the times you use SaveOnly, and where there is a short
        # paragraph around this deceptively simple line. We'll come back here later.
        $messageDisposition = "SendAndSaveCopy"
        if ($attachment) {
            $messageDisposition = "SaveOnly"
        }
        
        # Ok, the next note is on syntax. Since each connection to the web service has a slightly different type name,
        # the New-Object has to always create an item related to that ever-changing namespace. Many things
        # within Exchange Web Service also require you to set a lot of propertys, and so the
        # New-OBJECT "preface.Typename" -Property @{Stuff} style is very common in this code base
        $createItemType = New-Object "$script:ExchangeWebServiceNamespace.CreateItemType" -Property @{
            MessageDisposition = $messageDisposition
            MessageDispositionSpecified  = $true
            Items = New-Object "$script:ExchangeWebServiceNamespace.NonEmptyArrayOfAllItemsType"
        }
        
        # Ok, next bit of complication. Send-Email actually sends appointment requests. It knows to do so by
        # the presence or absence of a Start or End parameter.
        if ($Start -and $End -and 
            (-not $responseType -or $responseType -eq "Organizer")) {
            if ($ItemClass -notlike "*Meeting*") {
                $ItemClass = "IPM.Schedule.Meeting.Request"
            }
        }
        # Next along the line if the default item class. Strangely, the item class for a mail message is "IPM.NOTE"
        if (-not $ItemClass) { $ItemClass = "IPM.Note" } 

        # We create a slightly different base object, depending on the type of meeting.
        # Most times it's MessageType, but it it's a meeting request it's a CalendarItemType
        $messageType = switch ($ItemClass) {
            "IPM.Schedule.Meeting.Request" { "CalendarItemType" }
            "IPM.Schedule.Meeting.Resp.Pos" { "MessageType" }
            "IPM.Schedule.Meeting.Cancelled" { "MessageType" }
            "IPM.Note" { "MessageType"}
        }
                

        # Message body is the same either way, so create the message and the body in a nice
        # byzantine nested New-Object ... -Property @{}
        $message =New-Object "$script:ExchangeWebServiceNamespace.${messageType}" -Property @{
            Body = New-Object "$script:ExchangeWebServiceNamespace.BodyType" -Property @{
                BodyType1 = if ($BodyAsHtml) { "Html" } else { "Text" } 
                Value =  $Body
            }
        }
        
        # Because, if you've failed to get a message at this point, it should give up
        if (-not $message) { 
            return   
        }
        
        if ("AcceptItemType","DeclineItemType" -contains $message.GetType().Name) {            
            $message.ReferenceItemId = New-Object "$ExchangeWebServiceNamespace.ItemIDType" -Property @{
                Id = $calenderId
            }
        } else {
            $message.Subject = $subject
        }
        
        if ($message.GetType().Name -eq "MessageItemType") {
            $message.Sender= New-Object "$script:ExchangeWebServiceNamespace.SingleRecipientType" -Property @{
                Item = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType" -Property @{
                    EmailAddress = "$From"            
                }
            }                    
        }
        
        if ($Sensitivity) {
            $message.Sensitivity=  $Sensitivity
            $message.SensitivitySpecified = $true
        }
        
        if ($start -and $message.psobject.members["Start"]) {
            $message.Start = $start
            $message.StartSpecified = $true
        }
        if ($end -and $message.psobject.members["End"]) {
            $message.End= $end
            $message.EndSpecified = $true
        }
        if ($location) {
            $message.Location = $Location            
        }
        
        if ($to) {
            if ($message.GetType().Name -eq "CalendarItemType") {
                $message.RequiredAttendees = New-Object "$script:ExchangeWebServiceNamespace.AttendeeType[]" $to.Count
                for ($i = 0; $i -lt $to.Count; $i++) {
                    $t = $to[$i]
                    $attendee = New-Object "$script:ExchangeWebServiceNamespace.AttendeeType" -Property @{
                        MailBox = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType" -Property @{
                            EmailAddress = "$t"
                        }
                    }
                    $message.RequiredAttendees[$i] = $attendee
                }
            } else {
                $message.ToRecipients = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType[]" $to.Count
                for($i =0;$i -lt $to.Count;$i++){
                    $t= $to[$i]
                    $message.ToRecipients[$i] = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType" -Property @{ EmailAddress = "$t" }
                }
            } 
            
        } 
        
        if ($replyTo) {
            $message.ReplyTo = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType[]" $replyTo.Count
            for($i =0;$i -lt $replyTo.Count;$i++){
                $t= $replyTo[$i]
                $message.ReplyTo[$i] = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType" -Property @{ EmailAddress = "$t" }
            }
        }
        
        if ($cc) {
            if ($message.GetType().Name -eq "CalendarItemType") {
                $message.OptionalAttendees = New-Object "$script:ExchangeWebServiceNamespace.AttendeeType[]" $to.Count
                for ($i = 0; $i -lt $to.Count; $i++) {
                    $t = $cc[$i]
                    $attendee = New-Object "$script:ExchangeWebServiceNamespace.AttendeeType" -Property @{
                        MailBox = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType" -Property @{
                            EmailAddress = "$t"
                        }
                    }
                    $message.OptionalAttendees[$i] = $attendee
                }
            } else {
                $message.CcRecipients = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType[]" $cc.Count
                for($i =0;$i -lt $cc.Count;$i++){
                    $t= $cc[$i]
                    $message.CCRecipients[$i] = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType" -Property @{ EmailAddress = "$t" }
                }
            }
        }
                
        if ($bcc) {
            $message.BccRecipients = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType[]" $bcc.Count
            for($i =0;$i -lt $bcc.Count;$i++){
                $t= $bcc[$i]
                $message.BccRecipients[$i] = New-Object "$script:ExchangeWebServiceNamespace.EmailAddressType" -Property @{ EmailAddress = "$t" }
            }
        }
        
        if ($psBoundParameters.Category) {
            $message.Categories = $category
        }        
        
        if ($message.GetType().Name -eq "CalendarItemType") {
            $createItemType.SendMeetingInvitations = "SendToAllAndSaveCopy"
            $createItemType.SendMeetingInvitationsSpecified = $true
        } else {
            $createItemType.SavedItemFolderId = New-Object "$script:ExchangeWebServiceNamespace.TargetFolderIdType" -Property @{
                Item = New-Object "$script:ExchangeWebServiceNamespace.DistinguishedFolderIdType" -Property @{
                    Id  = "SentItems"
                }
            }
        
        }

        $createItemType.Items.Items = New-Object "$script:ExchangeWebServiceNamespace.ItemType[]" 1
        $createItemType.Items.Items[0] = $message

        
        $result = $exchangeWebService.CreateItem($createItemType) 
        
        
        
        $result.ResponseMessages.Items | 
            Where-Object {
                $_.ResponseClass -eq "Error"
            } |
            ForEach-Object {
                Write-Error -Message $_.MessageText -ErrorId "ExchangeWebServiceError.$($_.ResponseCode)"
            }   
            
        #If there are attachments, we need to create attachments.
        if ($attachment) {
            $Attachment = @($attachment)

            #Need to get the item id of the item that is just created.
            $itemid = $result.ResponseMessages.Items[0].Items.Items[0].ItemId           
            
            #Need to call CreateAttachement webservice for each attachment and associate it with the above id.
            $createAttachmentType =  New-Object "$script:ExchangeWebServiceNamespace.CreateAttachmentType"  
           
            $itemidtype = New-Object "$script:ExchangeWebServiceNamespace.ItemIdType"  -Property @{
                Id = $itemid.Id
                ChangeKey = $itemId.ChangeKey
            }
            
           
            $createAttachmentType.ParentItemId = $itemidtype
          
           
            $createAttachmentType.Attachments = New-Object "$script:ExchangeWebServiceNamespace.AttachmentType[]"  @($Attachment).count
           
            for($i=0 ; $i -lt @($Attachment).count; $i++) {           
                $fileattachment = New-Object "$script:ExchangeWebServiceNamespace.FileAttachmentType"   
                $resolvedAttachmentPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath(@($attachment)[$i])
                if (-not $resolvedAttachmentPath) { break }
                $fileattachment.Name = Split-Path -Leaf $resolvedAttachmentPath                
                $fileattachment.Content = [IO.File]::ReadAllBytes($resolvedAttachmentPath )
              
                $createAttachmentType.Attachments[$i] = $fileattachment                     
            }          

            
            $attachmentresult = $exchangeWebService.CreateAttachment($createAttachmentType)
            $attachmentResponseMessage =$attachmentResult.ResponseMessages.Items[0]
        
            $sendItemType = New-Object "$script:ExchangeWebServiceNamespace.SendItemType" -Property @{            
                SavedItemFolderId = New-Object "$script:ExchangeWebServiceNamespace.TargetFolderIdType" -Property @{
                    Item = New-Object "$script:ExchangeWebServiceNamespace.DistinguishedFolderIdType" -Property @{
                        Id  = "SentItems"
                    }
                }
                ItemIds = New-Object "$script:ExchangeWebServiceNamespace.BaseItemIdType[]" $attachmentResponseMessage.Attachments.Count
                SaveItemToFolder = $true
            }
            
            for ($c = 0 ;$c -lt $attachmentResponseMessage.Attachments.Count; $c++) {
                $itemIdType = New-Object "$script:ExchangeWebServiceNamespace.ItemIdType" -Property @{
                    ChangeKey = $attachmentResponseMessage.Attachments[$c].AttachmentId.RootItemChangeKey
                    Id = $attachmentResponseMessage.Attachments[$c].AttachmentId.RootItemId
                }
                $sendItemType.ItemIds[$c] = $itemIdType
            }
            
            $result = $script:ExchangeWebService.SendItem($sendItemType)
            
        }
          
            
        if ($passThru) { $result } 
    }
}