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.
.