Outlook-Mail.ps1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope='Function', Target='New*')]
param()
function Get-GraphMailTips       {
    <#
      .synopsis
        Gets mail tips for one or more users (is their mailbox full, are auto-replies on etc)
    #>

    [cmdletbinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification="MailTip would be incorrect")]
    param(
        #mail addresses
        [Parameter(Mandatory=$true)]
        [string[]]$Address
    )

    $json = Convertto-Json @{EmailAddresses=$Address;
                  MailTipsOptions= "automaticReplies, mailboxFullStatus, customMailTip, deliveryRestriction, externalMemberCount, maxMessageSize, moderationStatus, recipientScope, recipientSuggestions, totalMemberCount"
     }

    Connect-MSGraph
    (Invoke-RestMethod -Method post -headers $Script:DefaultHeader -Uri "https://graph.microsoft.com/v1.0/me/getMailTips" -Body $json -ContentType "application/json").value
}

function Get-GraphMailFolderList {
    <#
      .Synopsis
        Get the user's Mailbox folders
      .Example
        Get-GraphMailFolderList -Name inbox
        Gets the current users inbox folder
    #>

    [cmdletbinding(DefaultParameterSetName="None")]
    param(
        #UserID as a guid or User Principal name. If not specified defaults to "me"
        [string]$UserID,
        #Select the first n folders.
        [validaterange(1,1000)]
        [int]$Top,
        #fields to select in the query - will add a validate set later
        [string[]]$Select  ,
        #String with orderby clause e.g. "name", "lastmodifiedDate desc"
        [string]$OrderBy,
        #filter the folders returned by a name
        [Parameter(Mandatory=$true, ParameterSetName='FilterByName')]
        [string]$Name,
        #A custom filter clause.
        [Parameter(Mandatory=$true, ParameterSetName='FilterByString')]
        [string]$Filter
    )

    Connect-MSGraph
    $webParams = @{Method = "Get"
                    Headers = $Script:DefaultHeader
    }
    #region set-up URI . If we got a user ID, use it other otherwise use the current user, add select, orderby, filter & top parameters as needed
    if ($UserID)  {$uri = "https://graph.microsoft.com/v1.0/users/$userID/mailFolders" }
    else          {$uri = "https://graph.microsoft.com/v1.0/me/mailFolders" }
    $JoinChar = "?"  #Will the next parameter be joined onto the URI with a "?"" or with "&" ?
    if ($Select)  {$uri = $uri + '?$select=' + ($Select -join ',') ;                                 $JoinChar = "&"}
    if ($Name)    {$uri = $uri + $JoinChar + ("`$filter=startswith(displayName,'{0}') " -f $Name ) ; $JoinChar = "&"}
    if ($Filter)  {$uri = $uri + $JoinChar + '$Filter='  +$Filter                                  ; $JoinChar = "&"}
    if ($OrderBy) {$uri = $uri + $JoinChar + '$orderby='  +$Filter                                 ; $JoinChar = "&"}
    if ($Top)     {$uri = $uri + $JoinChar + '$top=' + $top }
    #endregion

    #region get the data, cope with it being paged add a type to help formatting and return the result
    $folderList    = @()
    $result       = Invoke-RestMethod @webParams -Uri $uri
    $folderList   += $result.value
    while ($result.'@odata.nextLink') {
        $result          =Invoke-RestMethod @webParams -Uri  $result.'@odata.nextLink' ;
        $folderList += $result.value
    }

    foreach ($f in $folderList) {$f.pstypenames.add("GraphMailFolder")}
    return $folderList
    #endregion
}

function Get-GraphMailItem       {
    <#
      .Synopsis
        Get items in a mail folder
      .Example
        >Get-GraphMailItem -top 5
        Gets the top 5 items in the current users Inbox
      .Example
        >Get-GraphMailItem -Mailfolder "sentitems" -top 5
        Gets the top 5 items in the current users sent items folder
      .Example
        >Get-GraphMailFolderList -Name sent | Get-GraphMailItem -top 5
        This has the same result as before but could find any folder
      .Example
        >Get-GraphMailItem -Search 'criminal'
        Searches the default folder (inbox) for 'Criminal' in any field
      .Example
        >Get-GraphMailItem -Search 'criminal' -Mailfolder ''
        Searches for 'Criminal' in any field but this time searches the whole mailbox
      .Example
        >Get-GraphMailItem -Search 'subject:criminal' -Mailfolder ''
        This time limits the search to just the subject line. from:, to: etc can be
        used in the same way as they can in a search in outlook.
      .Example
        >Get-GraphMailItem -filter "from/emailAddress/address eq 'alex@contoso.com'"
        Instead of a free text search this applies a filter on email address, looking at the inbox.
      .Example
        Get-GraphMailItem -Filter "(hasattachments eq true) and startswith(from/emailAddress/name, 'alex')"
        This shows a filter based on two conditions.
    #>

    [cmdletbinding(DefaultParameterSetName="None")]
    param(
        #UserID as a guid or User Principal name. If not specified defaults to "me"
        [string]$User ,
        #The ID of a folder, or one of the well known folder names 'archive', 'clutter', 'conflicts', 'conversationhistory', 'deleteditems', 'drafts', 'inbox', 'junkemail', 'localfailures', 'msgfolderroot', 'outbox', 'recoverableitemsdeletions', 'scheduled', 'searchfolders', 'sentitems', 'serverfailures', 'syncissues'
        [Parameter(ValueFromPipeline=$true)]
        $Mailfolder = "Inbox",
        #if specified the command will return child folders instead of messages
        [switch]$ChildFolders,
        #A term to do a free text search for in the mail box (see examples)
        [string]$Search,
        #If specified returns the top X items
        [int]$Top,
        #Sorting option, defaults to sorting by SentDateTime with newest first. Searches are not sorted.
        [string]$OrderBy ='SentdateTime desc',
        #Select particular mail fields , ignored if -ChildFolders is specified; defaults to From, Subject, SentDateTime, BodyPreview, and Weblink
        [ValidateSet('bccRecipients', 'body', 'bodyPreview', 'categories', 'ccRecipients', 'changeKey', 'conversationId', 'createdDateTime',
        'flag', 'from', 'hasAttachments', 'id', 'importance', 'inferenceClassification', 'internetMessageHeaders', 'internetMessageId',
        'isDeliveryReceiptRequested', 'isDraft', 'isRead', 'isReadReceiptRequested', 'lastModifiedDateTime', 'parentFolderId',
        'receivedDateTime', 'replyTo', 'sender', 'sentDateTime', 'subject', 'toRecipients', 'uniqueBody', 'webLink' )]
        [string[]]$Select = @('From', 'Subject', 'SentDatetime', 'BodyPreview', 'weblink'),
        #A Custom filter string; for example "importance eq high" - the examples have more cases
        [Parameter(Mandatory=$true, ParameterSetName='FilterByString')]
        [string]$Filter
    )
    begin   {
        Connect-MSGraph
    }
    process {
        if     ($Mailfolder.id) {$MailPath = 'mailfolders/' +  $Mailfolder.id}
        elseif ($Mailfolder)    {$MailPath = 'mailfolders/' + ($Mailfolder -replace '^/','')}
        else                    {$MailPath = ''}

        if ($User.id) {$User  = $User.id}
        if ($User)    {$uri   = "https://graph.microsoft.com/v1.0/users/$user/$MailPath" }
        else          {$uri   = "https://graph.microsoft.com/v1.0/me/$MailPath" }
        $webParams = @{Method = "Get"
                      Headers = $Script:DefaultHeader
        }

        if ($ChildFolders -and '' -ne $MailPath)    {
            $result = (Invoke-RestMethod @webParams -Uri "$uri/childfolders")
            $folderList = $result.value
            foreach ($f in $folderList) {$f.pstypenames.add("GraphMailFolder")}
            return $folderList
        }
        elseif ($ChildFolders) {
            Write-Warning -Message 'You need to specify a folder when requesting child folders.'
        }
        else {
            $webParams.Headers["Prefer"] ='outlook.body-content-type="text"'
            $uri =  $uri + '/messages?$select='  + ($Select -join ',')
            if     ($Top)    {$uri = $uri + '&$top='     + $Top              }
            if     ($Search) {$Uri = $uri + '&$search="' + $Search + '"'     }
            elseif ($Filter) {$Uri = $uri + '&$filter='  + $Filter + ''      }
            else             {$uri = $uri + '&$orderby=' + $OrderBy          }


            (Invoke-RestMethod @webParams -Uri $uri ).value |
                ForEach-Object {$_.pstypeNames.add("GraphMailMessage") ; $_ } |
                Add-Member -PassThru -MemberType ScriptProperty -Name "fromName"    -Value {$this.from.emailAddress.name} |
                Add-Member -PassThru -MemberType ScriptProperty -Name "fromAddress" -Value {$this.from.emailAddress.address} |
                Add-Member -PassThru -MemberType ScriptProperty -Name "bodyText"    -Value {$this.body.content}
        }
    }
}

function New-MailAddress         {
    param (
        # The recipient's email address, e.g Alex@contoso.com
        [Parameter(Mandatory=$true,Position=0, ValueFromPipeline=$true)]
        [String]$Mail,
        #The displayname for the recipient
        $DisplayName
    )
    $recip = @{address=$Mail}
    if ($DisplayName) {$recip['name'] = $DisplayName}

    $recip
}

function New-Recipient           {
    <#
      .Synopsis
        Creats a new meeting attendee, with a mail address and the type of attendance.
    #>

    param(
        # The recipient's email address, e.g Alex@contoso.com
        [Parameter(Mandatory=$true,Position=0, ValueFromPipeline=$true)]
        $Mail,
        #The displayname for the recipient
        $DisplayName
    )
    @{ 'emailAddress' = (New-MailAddress -Mail:$mail -DisplayName:$DisplayName )}
}

function Send-GraphMailMessage   {
    <#
      .Synopsis
        Sends Mail using the Graph API from the current user's mailbox.
      .Example
        >Send-GraphMail -To "chris@contoso.com" -subject "You left your keys behind[nt]"
        Sends a mail with a subject but no body or attachments
      .Example
        >Send-GraphMail -To "chris@contoso.com" -body "Keys are with reception" -NoSave
        Sends a mail but thi time the subject will read "No subject" and the test will be in the body.
        -NoSave means that no copy of this message will be kept in sent items
      .Example
        >Send-GraphMail -To "chris@contoso.com" -Subject "Screen shot" -body "How does this look ?" -Attachments .\Logon.png -Receipt
        #This message has an attachement and requests a read receipt.
      .Example
        >$body"<h1>New dialog</h1><br /><img src='cid:Logon.png' -alt='Look at that'><br/>what do you think"
        >$link = Send-GraphMail -To "jhoneill@waitrose.com" -Subject "Login Sreen" -body $body -BodyType HTML -NoSave -Attachments .\Logon.png -SaveDraftOnly
        This creates an HTML body, the attached picture can be referenced in an <img> tag with cid:fileName.ext
        this time the mail is not sent but left in the user's drafts folder for review.
    #>

    [Cmdletbinding(DefaultParameterSetName='None')]
    param (
        #Recipient(s) on the "to" line, each is either created with New-MailRecipient (a hash table), or a string holding an address.
        [parameter(Mandatory=$true,Position=0)]
        $To ,
        #Recipient(s) on the "CC" line,
        $CC  ,
        #Recipient(s) on the "Bcc line" line,
        $BCC,
        #The subject of the message. A message must have a subject and/or body and/or attachments. If the subject is left blank it will be sent as "No Subject"
        [String]$Subject,
        #The content of the message; assumed to be plain text, but HTML can be specified with -BodyType
        [String]$Body    ,
        #The type of the body content. Possible values are Text and HTML.
        [ValidateSet("Text","HTML")]
        $BodyType = "Text",
        #The importance of the message: Low, Normal or High
        [ValidateSet('Low','Normal', 'High')]
        $Importance = 'Normal' ,
        #Path to file(s) to send as attachments
        $Attachments,
        #If Specified, requests a receipt.
        [switch]$Receipt,
        #If specified leaves the message in the drafts folder without sending it and returns a link to open the message.
        [parameter(ParameterSetName='SaveDraftOnly',Mandatory=$true)]
        [switch]$SaveDraftOnly,
        #If specified specifies that a copy of the mail should not be saved
        [parameter(ParameterSetName='NoSave',Mandatory=$true)]
        [switch]$NoSave
    )

    #Do we post a message, or do we create a draft ? We need to check attacment sizes to be sure...
    $asDraft = [bool]$SaveDraftOnly
    if ($Attachments) {
        $AttachmentItems = Get-item $Attachments -ErrorAction SilentlyContinue
        if (-not $AttachmentItems) {
            Write-Warning (($Attachments -join ", ") + "Gave no items. Message sending will continue")
        }
        else {
            if ($Attachments.Where({$_.length -gt 2.85mb}))  {
                #The Maximum size for a POST is 4MB.
                #Attachments are base 64 encoded so 3MB of attachements become 4MB. Don't try closer than 95% of that
                throw ("Attachment would exceed maximum size for a POST. Maximum file size is ~ 2,900,000 bytes")
                return
            }
            elseif (-not $asDraft -and ($Attachments | Measure-Object -Sum length).sum -gt 2.7mb) {
                #If all the attaments add up to more than 90% of the possible message size, we need to
                #create a a draft and add each on its own. BUT this method does not support "No save to sent items"
                if ($NoSave) {
                    throw ("The total size of attachments would result in an HTTP Post which greater than 4MB. Individual uploads are not possible when SaveToSentItems is disabled.")
                    return
                }
                Else {
                    Write-Verbose -Message "After BASE64 encoding attacments, message may exceed 4MB. Using Draft and sequential attachment method"
                    $asDraft= $true
                }
            }
            else { Write-Verbose -Message "$($Attachments).count attachment(s); small enough to send in a single operation"}
         }
    }
    elseif (-not $Subject -and -not $Body) {
        Write-Warning -Message "Nothing to send" ; return
    }
    elseif (-not $Subject) {$Subject = "No subject"}

    Connect-MSGraph
    $webParams = @{Headers = $Script:DefaultHeader}

    if ($asDraft) {$Uri = "https://graph.microsoft.com/v1.0/me/Messages"}
    else          {$Uri = "https://graph.microsoft.com/v1.0/me/sendmail"}

    #Build a hash table with the parts of the message, this will be coverted into JSON
    #BEWARE names are case sensitive. if you create $msgSettings.Body instead of $msgSettings.body
    #the capital B will cause a 400 bad request error.
    #My personal coding style is to use inital CAPS for parameters and inital lower case for variables (though Powershell doesn't care)
    #so the parameter is $Body and the hash table key name and JSON label is body.

    $msgSettings   =  @{   body = @{ contentType  = $BodyType;
                                         content  = $Body}
                                         subject  = $Subject
                                      importance  = $Importance
                                     toRecipients = @()
    }
    foreach ($recip in $To ) {
            if     ($recip  -is [string] ) { $msgSettings[ 'toRecipients'] += New-Recipient $recip}
            else                           { $msgSettings[ 'toRecipients'] += $recip}
    }
    if     ($CC) {
        $msgSettings['ccRecipients']      = @()
        foreach ($recip in $cc ) {
            if     ($recip  -is [string] ) { $msgSettings[ 'ccRecipients'] += New-Recipient $recip}
            else                           { $msgSettings[ 'ccRecipients'] += $recip}}
    }
    if     ($BCC) {
                $msgSettings['bccRecipients']      = @()
        foreach ($recip in $bcc ) {
            if     ($recip  -is [string] ) { $msgSettings['bccRecipients'] += New-Recipient $recip}
            else                           { $msgSettings['bccRecipients'] += $recip}}
    }
    if ($Receipt)                          { $msgSettings['isDeliveryReceiptRequested'] = $true }

    #If we are creating a draft, save it now; if sending-in-one be ready for attachments
    if ($asDraft) {
        Write-Progress -Activity "Sending Message" -CurrentOperation "Uploading draft"
        $json = ConvertTo-Json $msgSettings -Depth 5 #default depth isn't enough !
        try            {$msg  = Invoke-RestMethod @webParams -Method post  -uri $uri  -Body $json -ContentType "application/json" }
        catch          {throw "There was an error creating the draft message."; return }
        if (-not $msg) {throw "The draft message was not created as expected" ; return }
        else           {
            Write-Verbose -Message "Message created with id '$($msg.id)'"
            $uri = $uri + "/" + $msg.id
        }
    }
    elseif ($AttachmentItems) {
        $msgSettings["attachments"]= @()
    }

    foreach ($f in $AttachmentItems) {
        $Filesettings = @{
            '@odata.type' = '#microsoft.graph.fileAttachment';
            name          = $f.Name ;
            contentId     = $f.name ;
            contentBytes  =  [convert]::ToBase64String( [system.io.file]::readallbytes($f.FullName))

        }
        if ($asDraft) {
            Write-Progress -Activity "Sending Message" -CurrentOperation "Uploading $($f.Name)"
            try {
                $null = Invoke-RestMethod @webParams -Method post  -uri "$uri/attachments"  -Body (ConvertTo-Json $Filesettings) -ContentType "application/json" -ErrorAction Stop
            }
            catch {
                Write-warning -Message "Error occured uploading file $($f.name) - will attempt to delete the draft message"
                Invoke-RestMethod @webParams -Method Delete  -Uri "$uri"
                throw "Failure during attachment upload"
                return
            }
        }
        else {
            $msgSettings["attachments"] += $Filesettings
        }
    }

    if ($SaveDraftOnly) {
            Write-Progress -Activity "Sending Message" -Completed
            return $msg.webLink
    }
    elseif ($asDraft) {
            Write-Progress -Activity "Sending Message" -CurrentOperation "Sending Draft"
            try {
                $msg =  Invoke-WebRequest @webParams -Method post  -uri "$uri/send"
                write-verbose -Message ($msg.StatusCode + " " + $msg.StatusDescription)
                Write-Progress -Activity "Sending Message" -Completed
            }
            catch {throw "There was an error sending the draft message; it remains in the drafts folder"}
    }
    else {
        $mail = @{Message=$msgSettings}
        if ($NoSave) {
           $mail['saveToSentItems'] = $false
        }
        Write-Progress -Activity "Sending Message" -CurrentOperation "Uploading and sending"

        $json = ConvertTo-Json $mail -Depth 10
        Write-Verbose $json
        try            {$msg  = Invoke-RestMethod @webParams -Method post  -uri $uri  -Body $json -ContentType "application/json" }
        catch          {throw "There was an error sending message."; return }
        write-verbose  -Message ($msg.StatusCode + " " + $msg.StatusDescription)
        Write-Progress -Activity "Sending Message" -Completed
    }
}

function Send-GraphMailForward   {
    <#
      .synopsis
        Forwards a mail message.
      .example
      >
      > $alex = New-Recipient Alex@contoso.com -DisplayName "Alex B."
      > Get-GraphMailItem -top 1 | Send-GraphMailForward -to $Alex -Comment "FYI :-)"
      Creates a recipient , and forwards the top mail in the users inbox to that recipent
    #>

    [Cmdletbinding(DefaultParameterSetName='None')]
    param (
        #Either a message ID or a Message object with an ID.
        [parameter(Mandatory=$true,Position=0,ValueFromPipeline)]
        $Message,
        #Recipient(s) on the "to" line, each is either created with New-MailRecipient (a hash table), or a string holding an address.
        [parameter(Mandatory=$true,Position=1)]
        $To ,
        #Comment to attach when forwarding the message.
        $Comment
    )
    $msgSettings   =  @{     toRecipients = @() }
    foreach ($recip in $To ) {
        if     ($recip  -is [string] ) { $msgSettings[ 'toRecipients'] += New-Recipient $recip}
        else                           { $msgSettings[ 'toRecipients'] += $recip}
    }
    if ($Comment)                      { $msgSettings[ 'comment'] = $Comment}
    if ($Message.id) {$uri = "https://graph.microsoft.com/v1.0/me/Messages/$($Message.id)/forward"}
    else             {$uri = "https://graph.microsoft.com/v1.0/me/Messages/$Message/forward"}

    $json = ConvertTo-Json $msgSettings -depth 10
    Write-Verbose $Json
    Invoke-RestMethod -Method post -Uri $uri -ContentType 'application/json' -Body $json -Headers $script:DefaultHeader
}

function Send-GraphMailReply     {
    <#
      .synopsis
        Replies to a mail message.
    #>

    [Cmdletbinding(DefaultParameterSetName='None')]
    param (
        #Either a message ID or a Message object with an ID.
        [parameter(Mandatory=$true,Position=0,ValueFromPipeline)]
        $Message,
        #Comment to attach when repling to the message - blank replies aren't allowed.
        [parameter(Mandatory=$true,Position=1)]
        $Comment,
        #If specified changes reply mode from reply [to sender] to Reply-to-all
        [Alias('All')]
        [switch]$ReplyAll
    )
    $msgSettings =  @{'comment' = $Comment }
    if ($Message.id) {$uri =  "https://graph.microsoft.com/v1.0/me/Messages/$($Message.id)/"}
    else             {$uri =  "https://graph.microsoft.com/v1.0/me/Messages/$Message"}
    if ($ReplyAll)   {$uri += '/replyAll' }
    else             {$uri += '/reply' }

    $json = ConvertTo-Json $msgSettings -depth 10
    Write-Verbose $Json
    Invoke-RestMethod -Method post -Uri $uri -ContentType 'application/json' -Body $json -Headers $script:DefaultHeader
}

<#
  GET https://graph.microsoft.com/beta/me/mailFolders/inbox/messagerules
  GET https://graph.microsoft.com/beta/me/outlook/masterCategories #Colours ...
  GET https://graph.microsoft.com/beta/me/findRooms #
  https://graph.microsoft.com/v1.0/me/messages('AAMkADA1M-zAAA=')/attachments('AAMkADA1M-CJKtzmnlcqVgqI=')/?$expand=microsoft.graph.itemattachment/item
  #>



<#
POST https://graph.microsoft.com/beta/me/messages/AAMkAGE1M88AADUv0uFAAA=/attachments
Content-type: application/json
Content-length: 319
 
{
    "@odata.type": "#microsoft.graph.referenceAttachment",
    "name": "Personal pictures",
    "sourceUrl": "https://contoso.com/personal/mario_contoso_net/Documents/Pics",
    "providerType": "oneDriveConsumer",
    "permission": "Edit",
    "isFolder": "True"
}
#>



#https://docs.microsoft.com/en-us/graph/api/resources/call?view=graph-rest-beta calls in teams
#https://docs.microsoft.com/en-us/graph/api/resources/onlinemeeting?view=graph-rest-beta on line meetings in teams