0

I'm using Powershell to get some emails from Office 365. This is how I set my token, and it works great for reading emails. Now I want to mark an email as read.

 if ($token -ne $null) 
{
    $body = @{
        client_id     = $clientID
        scope         = "https://graph.microsoft.com/.default"
        client_secret = $clientSecret
        grant_type    = "client_credentials"
    }

    $URL = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
    try { $tokenRequest = Invoke-WebRequest -Method Post -Uri $URL  -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing -ErrorAction Stop }
    catch { 
        Write-Host "Unable to obtain access token, aborting..." 
        Write-Host "Error Message: $($Error[0])"
        return 
        }

    $token = ($tokenRequest.Content | ConvertFrom-Json).access_token
    #Write-Host("Got Token=$token") 
} 

$authHeader1 = @{
   'Content-Type'='application\json'
   'Authorization'="Bearer $token"
}

This is my function to mark one specific email as read:

function MarkSingleEmailAsRead ($emailId, $headers)
{
    <# This is what we need to post as an example 
    POST https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages/markRead
    Content-Type: application/json

    {
      "messageIds": ["MC172851", "MC167983"]
    }
    #>

    $graphApiPostUrl = "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages/markRead"
    $body = @{
       "messageIds" = @($emailId) 
    }    

    Write-Host "Headers=" 
    $headers | ConvertTo-Json | Write-Host 
    Write-Host "Body=" 
    $body | ConvertTo-Json | Write-Host 


    try { 
        $results = Invoke-WebRequest -Method Post -Uri $graphApiPostUrl-ContentType "application/x-www-form-urlencoded" 
                      -Body $body -Headers $headers -UseBasicParsing -ErrorAction Stop 
        }
    catch { 
        Write-Host "Error Message: $($Error[0])"
        return 
        }

}

I'm able to retrieve a list of emails, but when I try to mark one as read, I'm getting this error:

Error Message: The remote server returned an error: (401) Unauthorized.

Specifically, can I or how can I change this line to specify the scope I need. I would like to use the same token for both the retrieval and the update (mark as read). I think I'm limited to accessing one specific email, and not entirely sure if there needs to be a change on the Azure side to give some update permissions as well.

scope         = "https://graph.microsoft.com/.default"

I have tried these two, but I'm not sure which scope to use, or exactly where it goes:

scope         = "https://graph.microsoft.com/AdministrativeUnit.ReadWrite.All"
scope         = "https://graph.microsoft.com/.default AdministrativeUnit.ReadWrite.All"
NealWalters
  • 17,197
  • 42
  • 141
  • 251
  • the scope "https://graph.microsoft.com/.default" is right, it enables all rights available to the service principal used. I do not see that you converted the body to json, you did but only for output on the screen, or do I miss something? you are sure that the identity used has sufficient permissions? – Toni Sep 29 '22 at 17:19
  • @Toni - So "graph.microsoft.com/.default" works for updates too? I was getting "Bad Request" for a while, and got past that, so I think my $body is correct; that's when I started getting the 401. So maybe I need to get my security upgraded on Azure then? Of course another team does that. – NealWalters Sep 29 '22 at 17:23
  • I always use this scope in combination with service principals - and yes I can create/update/delete etc. by specifying it. it simply enables all rights available for the identity without listing each. If you take a look at the docu the body has to be json: https://learn.microsoft.com/en-us/graph/api/serviceupdatemessage-markread?view=graph-rest-1.0&tabs=http and why are you not using invoke-restmethod? – Toni Sep 29 '22 at 17:28
  • @Toni - Thanks. I'm cobbling code to together from various blogs and samples. I can check out invoke-restmethod. Passing $body seems to be converted from the dictionary or hashtable to JSON, I use the same thing on the get-token and the read-emails. I would get 400 bad request not 401 unauthorized if my JSON was bad. I got many of those trying to get that part correct. So back to my fix, you think it is to get more permission from Azure then? – NealWalters Sep 29 '22 at 17:33
  • This guy seems to prefer WebRequest because he gets full response and statuses: https://www.truesec.com/hub/blog/invoke-webrequest-or-invoke-restmethod – NealWalters Sep 29 '22 at 17:37
  • from the permission side you need: ServiceMessageViewpoint.Write – Toni Sep 29 '22 at 17:38
  • @Toni Thanks, I was trying to learn how you found that, looks it's documented here: https://learn.microsoft.com/en-us/graph/api/serviceupdatemessage-markread?view=graph-rest-1.0&tabs=http. So you can find the method, then see what permissions it needs. It might take days before I get the permission with approvals and such. – NealWalters Sep 29 '22 at 18:40
  • yes but there is another way to check which permissions are needed. I use the mgGraph cmdlets (install-module microsoft.graph) and then you get the cmdlet find-mggraphcommand with this one you can verify which permissions are needed: https://learn.microsoft.com/en-us/powershell/microsoftgraph/find-mg-graph-command?view=graph-powershell-1.0 – Toni Sep 29 '22 at 19:26
  • @Toni Security has given me ReadWrite.All and ServiceMessageViewpoint.Write (one at a time; I have now asked them for both). Still getting 401 Not Authorized. Do they have to add the "variants"? See https://stackoverflow.com/questions/73978953/what-are-variants-in-azure-permissions – NealWalters Oct 07 '22 at 19:15
  • When using an "access token", make sure all permission in Azure are related the "Application" and that they are NOT placed in the "Delegated" category – NealWalters Oct 13 '22 at 18:11
  • @Toni Turns out, canNOT do ServiceMessageViewPoint.Write with an "application". I'm using an access token. learn.microsoft.com/en-us/graph/api/…. Next I'm going to try just changing the properties with an update: learn.microsoft.com/en-us/graph/api/… – NealWalters Oct 13 '22 at 19:59

1 Answers1

1

This is the code I used to update the IsRead property, a totally different approach. My program will be running in batch mode, scheduled by Windows Task Scheduler, so I have to use an access token, rather than a logon. Instead of calling the method specific to marking an email as "read", I'm using a method that allows me to change any property of the email.

function MarkSingleEmailAsRead ($emailAddress, $emailId)
{

    $params = @{
        isRead = "true"
    }

    # Write-Host "Confirm email=$emailAddress" 
    # Write-Host "Confirm emailId=$emailID" 

    Update-MgUserMessage -UserId $emailAddress -MessageId $emailId -BodyParameter $params

}

These are the security scopes I have (associated to my Application):

User.Read.All
Mail.Read
Mail.ReadWrite

There are two types of security in Azure: Delegated and Application. When running with an "Access Token" all the security needs to be associated with the section called "Application" and not the section called "Delegated".

From this page: https://learn.microsoft.com/en-us/graph/api/serviceupdatemessage-markread?view=graph-rest-1.0&tabs=http we learn that "serviceUpdateMessage: markRead" isn't available for "Application" type security. So using the $graphApiPostUrl = "https://graph.microsoft.com/v1.0/admin/serviceAnnouncement/messages/markRead" won't work for this batch program.

enter image description here

Next, to query only the emails that are "unread", see my post here: How to user -Filter with Get-MgUserMessage in PowerShell to get only unread messages (isRead=False)

NealWalters
  • 17,197
  • 42
  • 141
  • 251