PHP: Mailing through Office 365 using the Graph API

The Graph API is a great way to get your application to send/receive mail through Office 365 in the background

A while ago I needed to update my PHP applications mail handing scripts, as Microsoft are disabling basic authentication and they connected using EWS with basic authentication. I took the opportunity to update them to use the Microsoft Graph API instead.

My systems generally run mail as a background process, e.g. sending/receiving mail to the helpdesk mailbox, so this article is written in that vein. If you wanted to access mail in an interactive way (so the user is sat in front of the browser at the time mail is accessed) you’d need to switch to Delegated rights, rather than Application, and the user would log in rather than the script logging in. I’ve not looked at this so not able to say much more about it.

First of all you’ll need to create an application in Azure AD – you’ll need to have the appropriate permissions in Azure as we need to give admin consent:

  1. Go to https://portal.azure.com and navigate to Azure Active Directory\App Registrations.
  2. Click on New Registration, enter your application name and select which directories you want to be able to access.
  3. Now you’ll see a screen listing some IDs – take down the Client ID and Directory (Tenant) ID.
  4. Go to the Certificates and Secrets page, and add a client secret – note this down too.
  5. Now go to the API Permissions page. Click on Add a Permission, Microsoft Graph, Application Permissions, then select User.ReadAll, Mail.ReadWrite and Mail.Send. Now click Add Permissions.
  6. You’ll now need to give admin approval for these permissions – so click onĀ Grant Admin consent.

Your application will now have permission to read and write all mailboxes, and send as any user. We can now restrict this to specific mailboxes with the following powershell – e.g. my helpdesk system is restricted to only look at the helpdesk mailboxes:

New-ApplicationAccessPolicy -AppId clientID -PolicyScopeGroupId mailbox@domain.com -AccessRight RestrictAccess

You can run that multiple times for multiple mailboxes, or specify a group instead of a single mailbox

You can test with:

Test-ApplicationAccessPolicy -AppId clientID -Identity mailbox@domain.com

This will show Granted if you test the mailbox you just gave permissions to, and Denied for anything else. It sometimes takes up to half an hour for permission changes to take effect.

I’ll go through using the script now, and put the script itself at the end of the page.

First of all you’ll need to include the script and create a new object:

require_once 'mailer_graph.php';
$graphMailer = new graphMailer('tenantid','clientid','secret');

You’ll need to pass the directory (tenant) ID, client ID and the client secret that you noted down earlier. If all is well the script will authenticate itself using oAuth and obtain a token.

Receiving messages – this will by default grab the top 10 messages in the mailbox. I’ve not put any filters in as I tend to grab the mail, process it (e.g. into the helpdesk tickets database) and then delete the corresponding email. I just cycle through this until there are 0 emails left.

$messages = $graphMailer->getMessages('helpdesk@contoso.com');
echo '<pre>';
print_r($messages);
echo '</pre>';

You can set filters by altering line 37 in the script, e.g.

$messageList = json_decode($this->sendGetRequest($this->baseURL . 'users/' . $mailbox . '/mailFolders/Inbox/Messages?$top=20'));

Details of filtering can be found at https://docs.microsoft.com/en-us/graph/api/user-list-messages?view=graph-rest-1.0&tabs=http

To send an email, you need to prepare an array:

$mailArgs =  array('subject' => 'Test message',
    'replyTo' => array('name' => 'Katy', 'address' => 'address@email.com'),
    'toRecipients' => array( array('name' => 'Neil', 'address' => 'address@email.com'),  array('name' => 'Someone', 'address' => 'address2@email.com') ),     // name is optional
    'ccRecipients' => array( array('name' => 'Neil', 'address' => 'address@email.com'),  array('name' => 'Someone', 'address' => 'address2@email.com') ),	 // name is optional, otherwise array of address=>email@address
    'importance' => 'normal',
    'conversationId' => '',   //optional, use if replying to an existing email to keep them chained properly in outlook
    'body' => '<html>Blah blah blah</html>',
    'images' => array(  array('Name' => 'blah.jpg', 'ContentType' => 'image/jpeg', 'Content' => 'results of file_get_contents(blah.jpg)', 'ContentID' => 'cid:blah') ),   //array of arrays so you can have multiple images. These are inline images. Everything else in attachments.
    'attachments' => array(  array('Name' => 'blah.pdf', 'ContentType' => 'application/pdf', 'Content' => 'results of file_get_contents(blah.pdf)') )
    )
                   
$graphMailer->sendMail('helpdesk@contoso.com', $mailArgs);

Then call sendMail and pass the mailbox you wish to send from, and the array.

To delete a message, you’ll need to get the message ID (this is one of the fields returned when you run getMessages). You can just move the email to deleted items:

$graphMailer->deleteEmail('mailbox@domain.com', messageID);

or delete it permanently. This won’t be available in deleted items, or any of the recovery stages such as 2nd stage deleted items:

$graphMailer->deleteEmail('mailbox@domain.com', messageID, false);

With any luck, you’ll be sending and receiving mail!

 

The main script – mailer_graph.php:

<?php
/*
 *   Mail script - Katy Nicholson, April 2020
 *
 *   Retrieves and sends messages from an Exchange Online mailbox using MS Graph API
 *
 *   https://katynicholson.uk
 */
class graphMailer {

    var $tenantID;
    var $clientID;
    var $clientSecret;
    var $Token;
    var $baseURL;

    function __construct($sTenantID, $sClientID, $sClientSecret) {
            $this->tenantID = $sTenantID;
        	$this->clientID = $sClientID;
        	$this->clientSecret = $sClientSecret;
        $this->baseURL = 'https://graph.microsoft.com/v1.0/';
        $this->Token = $this->getToken();
    }

    function getToken() {
            $oauthRequest = 'client_id=' . $this->clientID . '&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default&client_secret=' . $this->clientSecret . '&grant_type=client_credentials';
        $reply = $this->sendPostRequest('https://login.microsoftonline.com/' . $this->tenantID . '/oauth2/v2.0/token', $oauthRequest);
            $reply = json_decode($reply['data']);
            return $reply->access_token;

    }

    function getMessages($mailbox) {
        if (!$this->Token) {
            throw new Exception('No token defined');
        }
        $messageList = json_decode($this->sendGetRequest($this->baseURL . 'users/' . $mailbox . '/mailFolders/Inbox/Messages'));
        if ($messageList->error) {
            throw new Exception($messageList->error->code . ' ' . $messageList->error->message);
        }
        $messageArray = array();

        foreach ($messageList->value as $mailItem) {
            $attachments = (json_decode($this->sendGetRequest($this->baseURL . 'users/' . $mailbox . '/messages/' . $mailItem->id . '/attachments')))->value;
            if (count($attachments) < 1) unset($attachments);
            foreach ($attachments as $attachment) {
                if ($attachment->{'@odata.type'} == '#microsoft.graph.referenceAttachment') {
                    $attachment->contentBytes = base64_encode('This is a link to a SharePoint online file, not yet supported');
                    $attachment->isInline = 0;
                }
            }
            $messageArray[] = array('id' => $mailItem->id,
                        'sentDateTime' => $mailItem->sentDateTime,
                        'subject' => $mailItem->subject,
                        'bodyPreview' => $mailItem->bodyPreview,
                        'importance' => $mailItem->importance,
                        'conversationId' => $mailItem->conversationId,
                        'isRead' => $mailItem->isRead,
                        'body' => $mailItem->body,
                        'sender' => $mailItem->sender,
                        'toRecipients' => $mailItem->toRecipients,
                        'ccRecipients' => $mailItem->ccRecipients,
                        'toRecipientsBasic' => $this->basicAddress($mailItem->toRecipients),
                        'ccRecipientsBasic' => $this->basicAddress($mailItem->ccRecipients),
                        'replyTo' => $mailItem->replyTo,
                        'attachments' => $attachments);

        }
        return $messageArray;
    }

    function deleteEmail($mailbox, $id, $moveToDeletedItems = true) {
        switch ($moveToDeletedItems) {
            case true:
                $this->sendPostRequest($this->baseURL . 'users/' . $mailbox . '/messages/' . $id . '/move', '{ "destinationId": "deleteditems" }', array('Content-type: application/json'));
                break;
            case false:
                $this->sendDeleteRequest($this->baseURL . 'users/' . $mailbox . '/messages/' . $id);
                break;
        }
    }

    function sendMail($mailbox, $messageArgs ) {
        if (!$this->Token) {
            throw new Exception('No token defined');
        }

        /*
        $messageArgs[   subject,
                replyTo{'name', 'address'},
                toRecipients[]{'name', 'address'},
                ccRecipients[]{'name', 'address'},
                importance,
                conversationId,
                body,
                images[],
                attachments[]
                ]

        */

        foreach ($messageArgs['toRecipients'] as $recipient) {
            if ($recipient['name']) {
                $messageArray['toRecipients'][] = array('emailAddress' => array('name' => $recipient['name'], 'address' => $recipient['address']));
            } else {
                $messageArray['toRecipients'][] = array('emailAddress' => array('address' => $recipient['address']));
            }
        }
        foreach ($messageArgs['ccRecipients'] as $recipient) {
            if ($recipient['name']) {
                $messageArray['ccRecipients'][] = array('emailAddress' => array('name' => $recipient['name'], 'address' => $recipient['address']));
            } else {
                $messageArray['ccRecipients'][] = array('emailAddress' => array('address' => $recipient['address']));
            }
        }
        $messageArray['subject'] = $messageArgs['subject'];
        $messageArray['importance'] = ($messageArgs['importance'] ? $messageArgs['importance'] : 'normal');
        if (isset($messageArgs['replyTo'])) $messageArray['replyTo'] = array(array('emailAddress' => array('name' => $messageArgs['replyTo']['name'], 'address' => $messageArgs['replyTo']['address'])));
        $messageArray['body'] = array('contentType' => 'HTML', 'content' => $messageArgs['body']);
        $messageJSON = json_encode($messageArray);
        $response = $this->sendPostRequest($this->baseURL . 'users/' . $mailbox . '/messages', $messageJSON, array('Content-type: application/json'));

        $response = json_decode($response['data']);
        $messageID = $response->id;

        foreach ($messageArgs['images'] as $image) {
            $messageJSON = json_encode(array('@odata.type' => '#microsoft.graph.fileAttachment', 'name' => $image['Name'], 'contentBytes' => base64_encode($image['Content']), 'contentType' => $image['ContentType'], 'isInline' => true, 'contentId' => $image['ContentID']));
            $response = $this->sendPostRequest($this->baseURL . 'users/' . $mailbox . '/messages/' . $messageID . '/attachments', $messageJSON, array('Content-type: application/json'));
        }

        foreach ($messageArgs['attachments'] as $attachment) {
            $messageJSON = json_encode(array('@odata.type' => '#microsoft.graph.fileAttachment', 'name' => $attachment['Name'], 'contentBytes' => base64_encode($attachment['Content']), 'contentType' => $attachment['ContentType'], 'isInline' => false));
            $response = $this->sendPostRequest($this->baseURL . 'users/' . $mailbox . '/messages/' . $messageID . '/attachments', $messageJSON, array('Content-type: application/json'));
        }
        //Send
        $response = $this->sendPostRequest($this->baseURL . 'users/' . $mailbox . '/messages/' . $messageID . '/send', '', array('Content-Length: 0'));
        if ($response['code'] == '202') return true;
        return false;

    }

    function basicAddress($addresses) {
        foreach ($addresses as $address) {
            $ret[] = $address->emailAddress->address;
        }
        return $ret;
    }

    function sendDeleteRequest($URL) {
        $ch = curl_init($URL);
            curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $this->Token, 'Content-Type: application/json'));
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            $response = curl_exec($ch);
            curl_close($ch);
        echo $response;
    }

    function sendPostRequest($URL, $Fields, $Headers = false) {
        $ch = curl_init($URL);
            curl_setopt($ch, CURLOPT_POST, 1);
            if ($Fields) curl_setopt($ch, CURLOPT_POSTFIELDS, $Fields);
        if ($Headers) {
            $Headers[] = 'Authorization: Bearer ' . $this->Token;
            curl_setopt($ch, CURLOPT_HTTPHEADER, $Headers);
        }
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            $response = curl_exec($ch);
        $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);
        return array('code' => $responseCode, 'data' => $response);
    }

    function sendGetRequest($URL) {
        $ch = curl_init($URL);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $this->Token, 'Content-Type: application/json'));
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            $response = curl_exec($ch);
            curl_close($ch);
        return $response;
    }
}

?>

 

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.