Skip to main content

Notifications

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.

    //Force TLS12 which I had to add later (Mid2022)
    System.Net.ServicePointManager::set_SecurityProtocol(System.Net.SecurityProtocolType::Tls12);
    
    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");
    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');
    dialogFieldAC = dialog.addFieldValue(extendedTypeStr(InfologText),'0.AVgAM....', 'Auth Code or 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());

   //Force TLS12 which I had to add later (Mid2022)
   System.Net.ServicePointManager::set_SecurityProtocol(System.Net.SecurityProtocolType::Tls12);

   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 support to utilize the API to send some other automated emails.  This change has been in our production environment for several weeks.  This helped me to further understand the API before I attempted the X changes.  I have the code from MSgraphRefreshToken implemented in a batch job that runs even 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.  

Months after first publishing this post I added code to request TLS 1.2 which for some reason I suddenly needed else I would get a Bad Request return code.  The above two jobs now include this change.

Comments

*This post is locked for comments

  • Rob Van de Beek Profile Picture Rob Van de Beek 5
    Posted at
    This TLS1.2 helped me, thanks. Can you please provide me the code how you handle attachments?
  • bnorma01 Profile Picture bnorma01 119
    Posted at
    I revisited this code based on a question someone asked and noticed the token refresh code suddenly had issues in my environment. The job was getting a "Bad Request" returned from the Microsoft API. I traced the connection with Wireshark and could see it was using TLS1.0 so I guessed that was the issue given nothing had changed in the code. Sadly the .net webrequest does not expose the details of many errors but in this case I guessed right. I'll update the code in the blog post at some point but I added the below line of code before the try block. The job once again works and I can see it is using TLS1.2 in the Wireshark trace. System.Net.ServicePointManager::set_SecurityProtocol(System.Net.SecurityProtocolType::Tls12);
  • Rob Van de Beek Profile Picture Rob Van de Beek 5
    Posted at
    I would also be interested in the whole solution that is now in production and the documents you mention in your article. Thanks a lot in advance
  • Rob Van de Beek Profile Picture Rob Van de Beek 5
    Posted at
    Great job, this would help me a lot. Can you please also share the code involving the attachments?
  • RSacam01 Profile Picture RSacam01
    Posted at
    I am going to try these scripts. Thank You