Sending Emails from Microsoft Dynamics AX 2012 using the Microsoft Graph API with Modern Authentication

We send various documents to customers directly from AX batch jobs using print management. We have found that emails sent directly from Office 365 have a much higher chance of not getting filtered by end user’s Email servers and clients as spam. With Microsoft wanting to disable SMTP Basic Authentication I had the desired to use Microsoft’s Graph API with Modern Authentication for these automated emails. I have the emails working in our test environment by altering the SysMailer X++ class in AX. I figured I would share some X++ jobs I wrote along the way to help others. The jobs should compile and run on any AX AOS given they do not use any third party SDK’s.

First is a job I called “MSgraphSendMail_Job” that sends a very basic email. When the job is run it prompts for various fields including the access token. At first I was getting access tokens from C# code I downloaded from Microsoft but included here is a second X++ job I can now use that gets access tokens from either an authorization code or from a refresh token. This job is also included as “MSgraphRefreshToken_Job” and again does not require any third party SDK’s.

static void MSgraphSendMail_Job(Args _args)
{
    System.Net.HttpWebRequest          webRequest;
    System.Net.HttpWebResponse         webResponse;
    System.IO.Stream                   stream;
    System.IO.StreamReader             streamReader;
    System.Byte[]                      bytes;
    System.Net.WebHeaderCollection     headers;
    str response, respCode, ErrorString;
    System.Text.UTF8Encoding           encoding;
    Dialog      dialog;
    DialogField dialogFieldEA, dialogFieldAT;
    str email_Address, access_token;
    ;

    // Get basic user input
    dialog = new Dialog("Send Email Using MsGrpah API with Token");
    //dialogGroup1 = dialog.addGroup("Enter All These");
    dialogFieldEA = dialog.addFieldValue(extendedTypeStr(InfoMessage),'someone@somewhere.com', 'Email Addr');
    dialogFieldAT = dialog.addFieldValue(extendedTypeStr(InfologText),'0a1b2c3d-4e5f....', 'Access Token');
    dialog.run();
    email_Address = dialogFieldEA.value();
    access_token  = dialogFieldAT.value();

    new InteropPermission(InteropKind::ClrInterop).assert();

    //Create webRequest
    webRequest = System.Net.WebRequest::Create('https://graph.microsoft.com/v1.0/me/sendMail') as  System.Net.HttpWebRequest;
    headers = new System.Net.WebHeaderCollection();
    headers.Add("Authorization: Bearer " + access_token);
    webRequest.set_Headers(headers);
    webRequest.set_Method('POST');
    webRequest.set_ContentType('application/json');
    webRequest.set_Timeout(15000); // Set the 'Timeout' property in Milliseconds.

    try
    {
       //Create the data string to POST
       encoding    = new System.Text.UTF8Encoding();
       bytes  = encoding.GetBytes("{"message": { "subject": "Test Subject", "body": { "contentType": "Text", "content": "Hello World !" }, "toRecipients": [ { "emailAddress": { "address": "" + email_Address + ""} } ] }, "saveToSentItems": "true" }");
       webRequest.set_ContentLength(bytes.get_Length());
       //Setup the stream and Submit the request
       stream = webRequest.GetRequestStream();
       stream.Write(bytes, 0, bytes.get_Length());
       stream.Close();
       //Get the response
       webResponse = webRequest.GetResponse();
       stream = webResponse.GetResponseStream();
       streamReader = new System.IO.StreamReader(stream);
       response = streamReader.ReadToEnd();
       streamReader.Close();
       stream.Close();
    }
    catch
    {  //If contains 401 then get new token
        ErrorString = AifUtil::getClrErrorMessage();
        throw error(ErrorString);
    }

    CodeAccessPermission::revertAssert();
    info("Success");
}
static void MSgraphRefreshToken_Job(Args _args)
{
    System.Net.HttpWebRequest   webRequest;
    System.Net.HttpWebResponse  webResponse;
    System.IO.Stream            stream;
    System.IO.StreamReader      streamReader;
    System.Byte[]               bytes;
    System.Text.UTF8Encoding    encoding;
    System.Net.WebHeaderCollection headers;
    str response, ErrorString;
    int accTokenLabelStart, accTokenDataStart, accTokenDataEnd;
    str accToken;
    int refTokenLabelStart, refTokenDataStart, refTokenDataEnd;
    str refToken;

    Dialog      dialog;
    DialogField dialogFieldTI, dialogFieldCI, dialogFieldRU, dialogFieldSC, dialogFieldGT, dialogFieldAC, dialogFieldRT;
    str tenant, client_id, redirect_uri, scope, code, refresh_token, grant_type, qrystr, endPoint;
    DialogGroup dialogGroup1, dialogGroup2;
    FormBuildCommandButtonControl buttonOK, buttonCN;
    ;

    // Get basic user input
    dialog = new Dialog("Refresh Token");
    //dialogGroup1 = dialog.addGroup("Enter All These");
    dialogFieldTI = dialog.addFieldValue(extendedTypeStr(InfoMessage),'0a1b2c3d-4e5f....', 'tenant');
    dialogFieldCI = dialog.addFieldValue(extendedTypeStr(InfoMessage),'0a1b2c3d-4e5f....', 'client_id');
    dialogFieldRU = dialog.addFieldValue(extendedTypeStr(InfoMessage),'https://login.microsoftonline.com/common/oauth2/nativeclient', 'redirect_uri');
    dialogFieldSC = dialog.addFieldValue(extendedTypeStr(InfoMessage),'mail.send', 'scope');
    //dialogGroup2= dialog.addGroup("Enter Only One");  //InfologText is really long
    dialogFieldAC = dialog.addFieldValue(extendedTypeStr(InfologText),'0.AVgAM....', 'Auth Code or Refresh Token');
    //dialogFieldRT = dialog.addFieldValue(extendedTypeStr(InfologText),'0.AVgAM....', 'refresh_token');
    buttonOK = dialog.dialogForm().buildDesign().control('OkBUtton');
    buttonOK.text('Renew From Code');
    buttonCN = dialog.dialogForm().buildDesign().control('CancelBUtton');
    buttonCN.text('Renew From Token');
    dialog.run();
    // option Ok or cancel
    if(dialog.closedOk())
        grant_type="authorization_code";
    else
        grant_type="refresh_token";
    tenant    = dialogFieldTI.value();
    client_id = dialogFieldCI.value();
    redirect_uri  = dialogFieldRU.value();
    scope     = dialogFieldSC.value();
    code      = dialogFieldAC.value();
    refresh_token = dialogFieldAC.value();

   //Create webRequest
   new InteropPermission(InteropKind::ClrInterop).assert();
   endPoint="https://login.microsoftonline.com/" + tenant + '/oauth2/v2.0/token';
   webRequest = System.Net.WebRequest::Create(endPoint) as  System.Net.HttpWebRequest;
   webRequest.set_Method('POST');
   webRequest.set_ContentType('application/x-www-form-urlencoded');
   webRequest.set_Timeout(15000);  // Set the 'Timeout' property in Milliseconds.
   //Create the data string to POST
   encoding    = new System.Text.UTF8Encoding();
   if (grant_type == 'authorization_code')
      qrystr = "client_id=" + client_id + "&grant_type=" + grant_type + "&scope=" + scope + "&code=" + code + "&redirect_uri=" + redirect_uri;
   else
      qrystr  = "client_id=" + client_id + "&grant_type=" + grant_type + "&scope=" + scope + "&refresh_token=" + refresh_token + "&redirect_uri=" + redirect_uri;
   bytes  = encoding.GetBytes(qrystr);
   webRequest.set_ContentLength(bytes.get_Length());

   try
   {
       //Setup the stream and Submit the request
       stream = webRequest.GetRequestStream();
       stream.Write(bytes, 0, bytes.get_Length());
       stream.Close();
       //Get the response
       webResponse = webRequest.GetResponse();
       stream = webResponse.GetResponseStream();
       streamReader = new System.IO.StreamReader(stream);
       response = streamReader.ReadToEnd();
       streamReader.Close();
       stream.Close();

       //We got here so it was success and the response has the JSON string with the tokens and such
       //At this point lets manually break it apart in place of attempting NewtonSoft

       //Find "access_token" and then look for following double quotes to get data
       accTokenLabelStart = strScan(response, ""access_token"", 0, strLen(response));
       accTokenDataStart = strFind(response, """, accTokenLabelStart + strLen(""access_token""), strLen(response)) + 1;
       accTokenDataEnd = strFind(response, """, accTokenDataStart, strLen(response));
       accToken = subStr(response, accTokenDataStart, accTokenDataEnd-accTokenDataStart);
       info(strFmt("Access Token = %1", accToken ));
       //Find "refresh_token" and then look for following double quotes to get data
       refTokenLabelStart = strScan(response, ""refresh_token"", 0, strLen(response));
       refTokenDataStart = strFind(response, """, refTokenLabelStart + strLen(""refresh_token""), strLen(response)) + 1;
       refTokenDataEnd = strFind(response, """, refTokenDataStart, strLen(response));
       refToken = subStr(response, refTokenDataStart, refTokenDataEnd-refTokenDataStart);
       info(strFmt("Refresh Token = %1", refToken ));
    }
    catch
    {
        ErrorString = AifUtil::getClrErrorMessage();
        throw error(ErrorString);
    }

    CodeAccessPermission::revertAssert();
    info(strFmt("Complete Response = %1", response ));
}

To understand the API I first tested with the soupUI testing tool and was able to successfully get access tokens and send mail with the tokens. Next I wrote some VB.NET as part of a BizTalk interface I also support to utilizes the API to send some other automated emails. This helped me to further understand before I attempted the X++ changes. I have the code from MSgraphRefreshToken implemented in a batch job that runs even an hour to refresh tokens storing them in a new table. I then have the SysMailer class altered to perform a “quickSend” with the code from MSgraphSendMail. Getting attachments in the JSON took a bit to get the syntax correct. Also you have to do some thought on how to throttle your calls to Microsoft else their throttling kicks in and replies with a 429 return code for “Too many request”. Once my changes to the SysMailer class get implemented on my production system I may share the code if anyone is interested.

If you do not know how to setup an application in Azure to allow all this to happen I also have a document that outlines how I did this. The document outlines how I got my initial access and refresh tokens from an authorization code from a specific user login. That user login is then what the emails are sent from using the tokens.

.

Leave a Comment