Wednesday, October 30, 2019

How to test SMTP using Opportunistic TLS with Powershell and grab the public certificate a SMTP server is using

Most email services these day employ Opportunistic TLS when trying to send Messages which means that wherever possible the Messages will be encrypted rather then the plain text legacy of SMTP.  This method was defined in RFC 3207 "SMTP Service Extension for Secure SMTP over Transport Layer Security" and  there's a quite a good explanation of Opportunistic TLS on Wikipedia .  This is used for both Server to Server (eg MTA to MTA) and Client to server (Eg a Message client like Outlook which acts as a MSA) the later being generally Authenticated.

Basically it allows you to have a normal plain text SMTP conversation that is then upgraded to TLS using the STARTTLS verb. Not all servers will support this verb so if its not supported then a message is just sent as Plain text. TLS relies on PKI certificates and the administrative issues that come around certificate management like expired certificates which is why I wrote this script. Essentially I wanted to see the Public Certificate that was in used by Recipient SMTP server and couldn't find any easy to use method to get it. Eg in a web browser you can always view a certificate to check its authenticity, but with SMTP there aren't a lot of good tools around for this, you can use Telnet to test in Plan text a SMTP server, but its not easy to retrieve the TLS public certificate from the server for inspection over Telnet (or using something like putty etc).

In PowerShell this is pretty easy back in 2006 I wrote a plain text SMTP test script and a variant to do alerting on verbs  so this is more just a modern version of this with the addition of using the System.Net.Security.SslStream class that supports creating the TLS connection and also allows you to easily export the recipient servers Public cert eg

        Write-Host("STARTTLS") -ForegroundColor Green
        $startTLSResponse = $streamReader.ReadLine();
        $ccCol = New-Object System.Security.Cryptography.X509Certificates.X509CertificateCollection
        $Cert = $sslStream.RemoteCertificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert);
        [System.IO.File]::WriteAllBytes($CertificateFilePath, $Cert);

I've created a small Powershell Script module that has two cmdlets the first is called Get-SMTPTLSCert which can be used to get the Public cert being used by the SMTP endpoint eg for Gmail you could use Get-SMTPTLSCert -ServerName -Sendingdomain -CertificateFilePath c:\temp\gmailpubCer.cer By default this uses the client submission port 587 SMTP-MSA (Port 25 is often blocked from most locations) so its testing client(Message Submission Agent) to server (rather then server to server between to SMTP Mesage Transfer Agents). The Sending domain is required becuase most SMTP servers don't allows a empty helo/ehlo statement. I've also included a cmldet "Invoke-TestSMTPTLS" that does a test of the SMTP server and does Authentication if necessary. eg usually to use Port 587 you need to authenticate on the SMTP server. So in this instance once you have used the STARTTLS verb to upgrade the Plain text conversation to TLS you can then use the AUTH LOGIN verb to submit the username and password as base64 strings to authenticate eg this is what the Auth looks like in script
            $command = "AUTH LOGIN" 
            write-host -foregroundcolor DarkGreen $command
            $AuthLoginResponse = $SSLstreamReader.ReadLine()
            write-host ($AuthLoginResponse)
            $Bytes = [System.Text.Encoding]::ASCII.GetBytes($Credentials.UserName)
            $Base64UserName =  [Convert]::ToBase64String($Bytes)              
            $UserNameResponse = $SSLstreamReader.ReadLine()
            write-host ($UserNameResponse)
            $Bytes = [System.Text.Encoding]::ASCII.GetBytes($Credentials.GetNetworkCredential().password.ToString())
            $Base64Password = [Convert]::ToBase64String($Bytes)     
            $PassWordResponse = $SSLstreamReader.ReadLine()
            write-host $PassWordResponse    

So the above code takes a PSCredential object and changes that into necessary SMTP verbs to authenticate.  So run against office365 this looks like

(The base64 values above decode to Useraname: and Password: )

This cmdlet doesn't actually send a Message it just invokes the envelope verbs and no Data verb is sent (where the MIME message would go). I've put a copy of the script on GitHub here

Thursday, October 17, 2019

Doing Mailbox Change discovery with an EWS PowerShell Script

Mailbox Change discovery is the process of looking at any folders or items that are new or have been modified recently in a Mailbox. Its useful in a number of different ways including (but not limited to)

  • Looking at what objects a third party Addin is creating or modifying in your mailbox
  • Help to work out which FAI (Folder Associated Item) is being modified when changes are made to the configuration in Outlook or Outlook on the Web (this can be useful if you then want to automate those changes in your own scripts)
  • Fixing client issues caused by corrupt or bad items (eg if you've ever used MFCMapi to delete and Item that's causing a particular client function not to work correctly)
  • Getting an understanding of how the backend scaffolding of new features work in Outlook on the Web (eg looking at what the substrate Is doing in Office365) 
If you have ever looked recently at the Non_IPM Root folder of any Office365 Mailbox you can see by the large number of folders that are used by various different apps, substrate processes as well as for new client features there is a lot going on. So this script can also help give a bit of insight on what's happening in the background when you activate or use particular features (or potentially point you to the location in the Mailbox when your looking at problems that might be occurring with certain features)
I'll go through a specific use case later looking at the "contact favourite feature (which I struggled to even find the UI documentation for)" which is what prompted me to write this script.

What this script does

The script has three main functions

  1. Enumerates every folder in the Mailbox (both IPM and NON_IPM_Subtree) as well as Search Folders and looks at the created and modified date of each folder. If they where created or modified in the lookbacktime then it's adds them to the report
  2. It then does a Scan of the Items in each Folder (excluding Search Folders) and if it finds any items that where modified or created after the lookbacktime then it adds these to the report
  3. It then does a Scan of the FAI Items (Folder Associated Items) in the Folder and again if the items where modified or created after the lookbacktime then it adds these to the report
The Output of the Report then contains information about what folders, Items and FAI Items have either been created or modified in the Mailbox in the last x number of seconds.

An Example

The best way to demonstrate this is with an Example which was the reason I wrote the script, The Contact Favourite feature in Outlook on the Web gives you the ability to click the star next to a Contacts name in OWA which then creates a favourite Shortcut eg

So I wanted to know when you did this where does the favourite item get created, what information it was storing and what other changes where happening. So this is where the following script comes into handy to find this information out all I needed to do was favourite a contact and then run the script immediately after to look at the items which changed in the Mailbox in the last 60 seconds. Eg a run of the script after I made the above change yielded a report that looked like

So from the above report you can see that firstly a new Search Folder was created 
\FavoritePersonas\Glen Scales_7d09f835-0028-4bd7-bed9-59535127bbe1 New SearchFolder

under the \FavoritePersonas\ directory and also an object of type SDS.32d4b5e5-7d33-4e7f-b073-f8cffbbb47a1.OutlookFavoriteItem was created under the folder


The other thing that I included in the report was the EntryId of the Item found so in the above case i can take the EntryId for the outlookfavourite and open the Item in a Mapi editor like OutlookSpy or MFCMAPI eg

And you can then see all the MAPI properties on the Item (or delete/export etc)

That's it relatively simple to use eg

Invoke-MailboxChangeDiscovery -MailboxName mailbox@domain -secondstolookback 60

I've put this script up on GitHub here

Friday, October 04, 2019

Using the MSAL (Microsoft Authentication Library) in EWS with Office365

Last July Microsoft announced here they would be disabling basic authentication in EWS on October 13 2020 which is now a little over a year away. Given the amount of time that has passed since the announcement any line of business applications or third party applications that you use that had been using Basic authentication should have been modified or upgraded to support using oAuth. If this isn't the case the time to take action is now.

When you need to migrate a .NET app or script you have using EWS and basic Authentication you have two Authentication libraries you can choose from

  1. ADAL - Azure AD Authentication Library (uses the v1 Azure AD Endpoint)
  2. MSAL - Microsoft Authentication Library (uses the v2 Microsoft Identity Platform Endpoint)
the most common library you will come across in use is the ADAL libraries because its been around the longest, has good support across a number of languages and allows complex authentications scenarios with support for SAML etc. The MSAL is the latest and greatest in terms of its support for oAuth2 standards and is where Microsoft are investing their future development efforts. A good primer for understanding the difference in terms of the Tokens that both of these endpoint generate is to read

So which should you choose ? If your using PowerShell then the ADAL is the easiest to use and there are a lot of good examples for this like. However from a long term point of view using MSAL library can be a better choice as its going to offer more supportability (new features etc) going forward as long as you don't fall into one of  the restrictions described in

In this post I'm going to look at using the MSAL library with EWS to access Mailboxes in Exchange Online. 


One of the biggest differences when it comes to coding between the libraries with ADAL you specify the resource your going to use eg "" and with the MASL you specific the scopes you are going to use. With EWS its relatively simple in that  there are only two scopes (EWS doesn't allow you to constrain your access to different mailbox item types) which you would first need to allow in your Application registration which can be found in the Supported Legacy API's section of the application registration(make sure you scroll right to the bottom)

Delegated Permissions

Application Permissions (where you going to use AppOnly Tokens)

.default Scope

For v.1 apps you can get all the static scopes configured in an application using the .default scope so for ews that would look something like . When your using App Only tokens this becomes important.

App registration and Consent

One of the advantages of the MSAL library is dynamic consent which for EWS because in practice your only going to be using one scope it doesn't have much use. However if your also going to be using other workloads you maybe able to take advantage of that feature. For the app registration you need to use the v2 Endpoint registration process (which is the default now in the Azure portal) see This also makes it easy to handle the consent within a tenant.

Getting down to coding

In the ADAL there was only one single class called the AuthenticationContext which you used to request tokens. In the MSAL you have the PublicClientApplication (which you use for standard user authentication) and ConfidentialClientApp which gets used for AppOnly tokens and On-Behalf-Of flow.


With the v2 Endpoint you have the option of allowing
  1. common
  2. organizations
  3. consumers
  4. Tenant specific (Guid or Name)
For EWS you generally always want to use the Tenant specific endpoint which means its best to either dynamically get the TenantId for the tenant your targeting or hard code it . eg you can get the TenantId need with 3 lines of C#

string domainName = "";
HttpClient Client = new HttpClient();
var TenantId = ((dynamic)JsonConvert.DeserializeObject(Client.GetAsync("" + domainName + "/v2.0/.well-known/openid-configuration")
In PowerShell you can do it with

$TenantId = (Invoke-WebRequest | ConvertFrom-Json).token_endpoint.Split('/')[3]

Delegate Authentication in EWS with MSAL and the EWS Managed API

This generally is the most common way of using EWS where your authenticating as a standard User and then accessing a Mailbox. If its a shared Mailbox then access will need to be have granted via Add-MailboxFolderPermission or you using EWS Impersonation

This is the simplest C# example of an Auth using the MSAL library in a Console app to logon (the currently logged on user).

 string MailboxName = "";
 string scope = "";
 string redirectUri = "msal9d5d77a6-fe09-473e-8931-958f15f1a96b://auth";
 string domainName = "";

 HttpClient Client = new HttpClient();
 var TenantId = ((dynamic)JsonConvert.DeserializeObject(Client.GetAsync("" + domainName + "/v2.0/.well-known/openid-configuration")

 PublicClientApplicationBuilder pcaConfig = PublicClientApplicationBuilder.Create("9d5d77a6-fe09-473e-8931-958f15f1a96b")      
 var TokenResult = pcaConfig.Build().AcquireTokenInteractive(new[] { scope })

 ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2016);
 service.Url = new Uri("");
 service.Credentials = new OAuthCredentials(TokenResult.AccessToken);
 service.HttpHeaders.Add("X-AnchorMailbox", MailboxName);

 Folder Inbox = Folder.Bind(service, WellKnownFolderName.Inbox);

AppOnly Tokens

This is where your Application is authenticating using a App Secret or SSL certificate, after this your App will get full access to all Mailboxes in a tenant (it important to not that the scoping feature doesn't work with the EWS so you need to be using the Graph or Outlook api).

 string clientId = "9d5d77a6-fe09-473e-8931-958f15f1a96b";
 string clientSecret = "xxxx";
 string mailboxName = "";
 string redirectUri = "msal9d5d77a6-fe09-473e-8931-958f15f1a96b://auth";
 string domainName = "";
 string scope = "";

 HttpClient Client = new HttpClient();
 var TenantId = ((dynamic)JsonConvert.DeserializeObject(Client.GetAsync("" + domainName + "/v2.0/.well-known/openid-configuration")

 IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(clientId)

 var TokenResult = app.AcquireTokenForClient(new[] { scope }).ExecuteAsync().Result;
 ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2016);
 service.Url = new Uri("");
 service.Credentials = new OAuthCredentials(TokenResult.AccessToken);
 service.HttpHeaders.Add("X-AnchorMailbox", mailboxName);
 service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, mailboxName);
 Folder Inbox = Folder.Bind(service, new FolderId(WellKnownFolderName.Inbox, mailboxName));

Token Refresh

One of the big things missing in the EWS Managed API is a callback before each request that checks for an expired Access Token. Because tokens are only valid for 1 hour if you have a long running process like a migration/export or data analysis then you need to make sure that you have some provision in your code to track the expiry of the access token and the refresh the token when needed.

Doing this in PowerShell

If your using PowerShell you can use the same code as above as long as import the MSAL library dll into your session

Some simple auth examples for this would be

 Delegate Authentication 

$MailboxName = "";
$ClientId = "9d5d77a6-fe09-473e-8931-958f15f1a96b"
$scope = "";
$redirectUri = "msal9d5d77a6-fe09-473e-8931-958f15f1a96b://auth";
$domainName = "";
$Scopes = New-Object System.Collections.Generic.List[string]
$TenantId = (Invoke-WebRequest | ConvertFrom-Json).token_endpoint.Split('/')[3]
$pcaConfig = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId).WithTenantId($TenantId).WithRedirectUri($redirectUri)
$TokenResult = $pcaConfig.Build().AcquireTokenInteractive($Scopes).WithPrompt([Microsoft.Identity.Client.Prompt]::Never).WithLoginHint($MailboxName).ExecuteAsync().Result;

AppOnly Token

$ClientId = "9d5d77a6-fe09-473e-8931-958f15f1a96b"
$MailboxName = ""
$RedirectUri = "msal9d5d77a6-fe09-473e-8931-958f15f1a96b://auth"
$ClientSecret = "xxx";
$Scope = ""
$TenantId = (Invoke-WebRequest | ConvertFrom-Json).token_endpoint.Split('/')[3]
$app =  [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create($ClientId).WithClientSecret($ClientSecret).WithTenantId($TenantId).WithRedirectUri($RedirectUri).Build()
$Scopes = New-Object System.Collections.Generic.List[string]
$TokenResult = $app.AcquireTokenForClient($Scopes).ExecuteAsync().Result;

Friday, September 20, 2019

Using a System-assigned managed identity in an Azure VM with an Azure Key Vault to secure an AppOnly Certificate in a Microsoft Graph or EWS PowerShell Script

One common and long standing security issue around automation is the physical storage of the credentials your script needs to get, whatever task your trying to automate done. The worse thing you can do from a security point of view is store them as plain text in the script (and there are still plenty of people out there doing this) a better option is to do some encryption (making sure you use the Windows Data Protection API) eg . Azure also offers some better options with ability to secure the credentials and certificates in RunBooks, so it is just a few clicks in the Gui and some simple code to secure your credentials when using a RunBooks.

In this post I’m going to look at the issues around storing and accessing SSL certificates associated with App only token authentication that you might be looking to use in Automation scripts. This is  more for when you can’t take advantage of Azure Runbooks and need the flexibility of a VM.

In Exchange (and Exchange Online) EWS Impersonation has been around for quite some time, it offers the ability to have a set of credentials that can impersonate any Mailbox owner. With the Microsoft Graph you have App Only tokens which offers a similar type of access with the additional functionality to limit the scope greatly to certain mailboxes and Item types . With App Only tokens you don’t have a username/password combination but a SSL certificate or application secret (the later should be avoided in production). So instead of the concern around the physical security of the username and password combination, your concern now is around the security of the underlying certificate.

One of the most important points is the time to start thinking about the security of the certificate is before you generate it. Eg just having a developer or Ops person generate it on their workstation leaving copies of the certificate file anywhere is the equivalent of the postit note on the monitor. This is where the Azure KeyVault (or AWS KMS) can be used to secure both the creation of the certificate and provide the ongoing storage and importantly Access Control and Auditing. So from the point of the creation of the AppOnly cert you should be able to have an audit trail of who created it and who accessed it. The other advantage of having the cert in a KeyVault is that it also makes it easy for you to have a short expiry on the certificate and automate the renewal process which in tern makes your auth process more secure.

Once the authentication Certificate is stored in the KeyVault then the weakest link can be the authentication method you then use to access the KeyVault. Eg a user account can be granted rights to the KeyVault (which then make that user account the weakest link) or you could use another Application secret or SSL Certificate to secure access to the data plain of the KeyVault. At some point all of these become self defeating from a security point of view as your still storing a credential (especially if you then store that as plain text) and for a persistent threat actor this still leaves you exposed.

One way of getting rid of the storage of credentials is the use of Managed identities where (“in my mind at least”) your trusting the infrastructure where the code is running. The simplest example would be say you create an Azure compute function and give that function a Managed identity, you can then grant that access to the KeyVault and your function can now access the certificate from the KeyVault and then authenticate to Azure and access every Mailbox in your tenant. So now you have placed the trust in the code in the function and the underlying security of the function (eg can the code be exploited, could somebody hack the deployment method being used and replace the code with their own Etc). With a System-assigned managed identity in an Azure VM your doing the same thing but this time the point of trust is the Azure Virtual Machine. So now its down to the physical security measures around the Azure VM which becomes the weakest link. Still not infallible but your security options around securing VM are many, so with good planning and practice you should be able get a balance between flexibility and security.
So lets look at a simple implementation of using a System-assigned managed identity in a Azure VM in a Powershell Script to get a SSL Certificate from a KeyVault and then access the Microsoft Graph using an AppOnly token generated using that certificate.

  1. This first thing you need is an Azure Key Vault where you have enabled auditing
  2. You need to create an application registration in Azure AD for your app that will be using the SSL cert from the Keystore to generate a AppOnly token and then use Application permissions for the task you want it to perform.
  3. Make sure you then consent to the above application registration so it can be used in your tenant (The portal now make this very straight forward)
  4. You need an Azure VM where you have enabled System-assigned managed identity
  5. One your Azure VM has a System-assigned managed identity you should be able to grant that secret-permissions so it can access the SSL certificate we are going to store in the KeyVault eg 
  6. Next step is create the Self Signed in the Azure Key Vault using the Azure Portal
  7. At this step you should now be able to access the Self Signed certificate from the Key Vault on your VM with some simple PowerShell code. All you will need is the URL to Key Secret Identifier eg

Them some code like

$KeyVaultURL = ""
$SptokenResult = Invoke-WebRequest -Uri '' -Headers @{Metadata="true"}
$Sptoken = ConvertFrom-Json $SptokenResult.Content
$headers = @{
'Content-Type'  = 'application\json'
'Authorization' = 'Bearer ' + $Sptoken.access_token    

$Response = (Invoke-WebRequest -Uri $KeyVaultURL  -Headers $headers) 
$certResponse = ConvertFrom-Json $Response.Content
With the above example you’ll see the following hard-coded URI

This isn’t something you need to change as is

The endpoint is available at a well-known non-routable IP address ( that can be accessed only from within the VM

So the above script uses this local Metadata Service to acquire the access token to access the Azure KeyVault (as the System-assigned managed identity). One you have the certificate raw data from the KeyVault you can then load it into a Typed Certificate Object eg

$base64Value = $certResponse.value
$Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
The last step is the certificate must be uploaded to the Application registration created in step 2 or added to the application manifest either manually or pro-grammatically. eg the following is an example to produces the required manifest format described here

    $SptokenResult = Invoke-WebRequest -Uri '' -Headers @{Metadata="true"}
    $Sptoken = ConvertFrom-Json $SptokenResult.Content
    $KeyVaultURL = ""
    $headers = @{
        'Content-Type'  = 'application\json'
        'Authorization' = 'Bearer ' + $Sptoken.access_token    
    $Response = (Invoke-WebRequest -Uri $KeyVaultURL  -Headers $headers) 
    $certResponse = ConvertFrom-Json $Response.Content
    $base64Value = $certResponse.value
    $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
    $bin = $Certificate.GetCertHash()
    $base64Thumbprint = [System.Convert]::ToBase64String($bin)
    $keyid = [System.Guid]::NewGuid().ToString()
    $jsonObj = @{ customKeyIdentifier = $base64Thumbprint; keyId = $keyid; type = "AsymmetricX509Cert"; usage = "Verify"; value = $base64Value }
    $keyCredentials = ConvertTo-Json @($jsonObj) | Out-File "c:\temp\tmp.key"

This puts the certificate data into a file temporarily which isn’t great you can actually use the Graph API to create an app registration and add the cert data directly which means the cert data never needs to be export/import into a file.

Authenticating with the SSL Certifcate you retrieved from the KeyVault

Once you have the Certificate loaded you can then use the ADAL library to perform the authentication and get the AppOnly access token you can either use in the Microsoft Graph or EWS eg

$ClientId = "12d09d34-c3a3-49fc-bdf7-e059801801ae"
$MailboxName = ""  
Import-Module .\Microsoft.IdentityModel.Clients.ActiveDirectory.dll -Force
$TenantId = (Invoke-WebRequest -Uri ('' + $MailboxName.Split('@')[1] + '/.well-known/openid-configuration') | ConvertFrom-Json).authorization_endpoint.Split('/')[3]

The ClientId is the ClientId from the application registration in step 2 the following does the Authentication using the configuration information from the above and then makes a simple Graph
request that uses the App Only Access Token that is returned.
$Context = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext("" + $TenantId)
$clientCredential = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate($ClientId,$Certificate)
$token = ($Context.AcquireTokenAsync("", $clientCredential).Result)
$Header = @{
   'Content-Type'  = 'application\json'
   'Authorization' = $token.CreateAuthorizationHeader()
$UserResult = (Invoke-RestMethod -Headers $Header -Uri ("`$filter=mail eq '" + $MailboxName + "'&`$Select=displayName,businessPhones,mobilePhone,mail,jobTitle,companyName") -Method Get -ContentType "Application/json").value
return $UserResult

A Gist of the full script can be found here

Wednesday, August 07, 2019

Email Header IpAddress GeoIP report Addin for Outlook and Outlook on the Web in Office365

Something that can be useful from time to time when looking at email delivery issues or email threats is to be able to see the Geographical regions that an email has traversed in its delivery. Usually this information gets stored in the Email Header in the received headers but also depending on the client and services being used the Source IpAddress of the client and other intermediaries may get written in other properties.

Because I needed something last week to do this and couldn't find any other addins to do this I created a pretty simple Outlook addin that

  • Gets the headers from a Message using the REST API in Office365
  • Uses a RegEx to get all the IPAddresses from that header
  • Uses a Set in JavaScript to then de duplicate these IPAddresses
  • Then I used one of the many free GeoIP web services out there to query each of the returned IPAddresses from the Regex matches and finally display the result in a table but to Outlook
For example here is what it returns where run against a normal gray email that was forward to my Office365 Mailbox from Gmail

For a quick diagnostic this information is pretty useful as it tells you where the email traversed and the Org information generally will tell you the cloud providers being used. Doing this on email in my junk email folder that where of a nefarious nature showed that the these emails had transitied through  countries that are the usual suspects in this type of activity.

I've hosted the code up on GitHub here so the addin can be added straight from the Repo if you want to try it using (MyAddins - Custom Addins) and

This uses REST to get the header so will only work on Office365 but the same thing could also be done using EWS for OnPrem servers from Exchange 2013 onward.

Wednesday, July 24, 2019

How to enable Dark mode in Outlook on Web in Office365 with EWS and PowerShell

Last year at Ignite Microsoft announced Dark mode for Outlook On the Web, while this seem to excite a lot of people I never really caught the buzz. However after taking the plunge after being notification bugged by Outlook this week I've found it to be a nice addition especially if your eyes aren't 100%.

When you enable Dark mode using the slider in Outlook on the Web

This changes/creates a setting called "isDarkModeTheme" in the OWA.UserOptions User Configuration Object which is held in the FAI collection (Folder Associated Items) in the Non_IPM_Root of the Mailbox. If you want to enable this setting for a user (or users) programmatically or just want to take stock of who is using this then you can use EWS to Read and Set the value in the OWA.UserOptions User Configuration Object in a Mailbox. (if you want to do this in the Microsoft Graph you will need to cry into your beer at the moment because the Microsoft Graph still doesn't support either user configuration objects or accessing FAI Items 😭😭😭). 

The code to enable dark mode is pretty easy first you need the FolderId for the Non_IPM_Root folder of the Mailbox you want to work with, then bind to the UserConfiguration object which will return the Dictionary from the underlying PR_ROAMING_DICTIONARY property. If Dark mode hasn't been enabled yet then the property shouldn't yet be in the Dictionary but if its is it will either be set to True of False depending on wether its enabled or not. So to Change this all we need is some simple code like the following 

        $folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root,$MailboxName)   
        $UsrConfig = [Microsoft.Exchange.WebServices.Data.UserConfiguration]::Bind($service, "OWA.UserOptions", $folderid, [Microsoft.Exchange.WebServices.Data.UserConfigurationProperties]::All)
        if ($UsrConfig.Dictionary) {
                    $UsrConfig.Dictionary["isDarkModeTheme"] = $false
                    $UsrConfig.Dictionary["isDarkModeTheme"] = $true                    

I've put together a simple script that wraps the above and Oauth authentication and provides two cmdlets for getting and setting Darkmode for Outlook on the Web for a mailbox. Eg 

To Get the Current Dark Mode setting use

 Get-DarkModeSetting -MailboxName

To Enable Dark Mode use

Set-DarkModeSetting -MailboxName (will return Get-DarkModeSetting after the update)

To disable Dark Mode use

Set-DarkModeSetting -MailboxName -Disable

I've put the script up on GitHub