Article
· Apr 2 9m read

Emailing with Office 365 and ObjectScript

One of the most common kinds of integration we are asked to do is emailing. One of the most typical email services our customers use is Microsoft’s Office 365. After setting up the right configuration on the Microsoft side, we can email from IRIS with two HTTP requests. By the end of this article, we will be able to send an email with an attachment through our Microsoft 365 service!

Microsoft’s REST API, called Graph, allows you to do a lot of things within Microsoft’s various apps, and it is precisely what we will employ to send our email. We must start by registering our app in Entra located inside the Azure admin portal. You will need to be a Microsoft Entra admin to carry out this process. Once you have logged in there, click the menu button in the top left corner, then look for Microsoft Entra ID. After clicking it, you will be taken to an overview screen where near the top, you will have your tenant ID. Remember to make a note of it since we will need it later on. Finally, click App Registrations on the left side.

In the App Registrations, click the New Registration button. Then, on the next screen, enter the name of your app. In our case, we will leave the default settings for everything else on this screen. If you manage multiple 365 tenants, you may want to look closer at the radio button set. A redirect URI is not necessary for our API usage. Click the Register button at the bottom. It will bring you to an overview screen for your new app registration. Make a note of the Application (client) ID field because we will need it later on as well. You will also see your tenant ID again on this screen, in case you missed it the first time.

 

Next, click the “Add a certificate or secret” link near Client Credentials. Under Client Secrets, click New Client Secret. Give it a description and choose an expiration date. The recommended one is six months, but you can set it to be good for up to two years. Then, give it a name, choose your duration, and click Add. On the next screen, remember to make a note of the Value field. There is a copy-to-clipboard button next to it, so you can take advantage of it. Do not leave the page without making a note of this field! You will not be able to see it again later, so this is your only chance. Otherwise, you will need to start again.

The final piece of the pie is the API permission. First, click API permissions on the left, then the Add a permission button, after that Microsoft Graph, and later on Application Permissions. Find Mail, then Mail.Send and click the Add Permission button. At this point, you have created a permission request, but it has not been granted yet. You should be able to click the “Grant admin consent for (tenant name)” button, giving your app permission to access the Graph API’s mail-sending endpoint.

That’s it for the Microsoft configuration. By this part of the process, you should have three pieces of information: your tenant ID, client ID, and client secret value. In order not to expose my private information, I will be storing these credentials in globals, referring to them in my examples as follows:

Item

Global

 

Tenant ID

^O365(“tenant_id”)

Your tenant ID from Microsoft 365

Client ID

^O365(“client_id”)

The client ID from your app registration

Client Secret

^O365(“client_secret”)

The value of the client secret you created

We must also have an SSL configuration set up in IRIS. To do it, go to your system management portal. Look for System Administration, Security, SSL/TLS Configurations, and click the Create New Configuration button. You can give it a simple name (I have named mine O365), and click the Save button using all of the default values.

Now, we are ready to create our first HTTP request. It will get us an OAuth token to use for the actual email request, and this is where you will need the values mentioned above. Consider the following method:

Method GetToken(Output token) As %Status
{
    try{
        set tokenRequest = ##class(%Net.HttpRequest).%New()
        set tokenRequest.Server = "login.microsoftonline.com"
        set tokenRequest.Location = ^O365("tenant_id")_"/oauth2/v2.0/token"
        set tokenRequest.SSLConfiguration = "O365"
        set tokenRequest.Https = 1
        set tokenRequest.ContentType = "application/x-www-form-urlencoded"
        do tokenRequest.InsertFormData("client_id",^O365("client_id"))
        do tokenRequest.InsertFormData("scope","https://graph.microsoft.com/.default")
        do tokenRequest.InsertFormData("client_secret",^O365("client_secret"))
        do tokenRequest.InsertFormData("grant_type","client_credentials")
        do tokenRequest.Post()
        if tokenRequest.HttpResponse.StatusCode '= "200"{
            $$$ThrowStatus($$$ERROR($$$GeneralError,"HTTP not OK. "_tokenRequest.HttpResponse.StatusCode))
        }
        set tokenObj = ##class(%Library.DynamicObject).%FromJSON(tokenRequest.HttpResponse.Data)
        set token = tokenObj.%Get("access_token")
        return $$$OK
    }
    catch ex{
        return ex.AsStatus()
    }
}

It creates an HTTP request to login.microsoftonline.com. The location will be your tenant ID followed by /oauth2/v2.0/token. We should use our SSL configuration and set the Https property of the request to true. This endpoint expects content to be in the application/x-www-form-urlencoded format, so that is what we will provide it with. We will insert four pieces of form data into the request. The client ID is the one we took notes of earlier from your app registrations. The scope is just https://graph.microsoft.com/.default. The client's secret is the one we created before. The grant type is client_credentials.

When we send this request, provided that everything is set up correctly, we should get an HTTP status code of 200 and a JSON response containing the token. The response also returns when the token expires, but we are primarily interested in the token itself for now. It is a very long string of characters that will be used to give us authorization on the next HTTP request.

Speaking about the next request, it is the one that will send the actual email. Some basic things will always be the same. The server we are sending it to is graph.microsoft.com. The content type will be application/json. We will use our SSL configuration once again and set the HTTP property to true. We will also set a header named Authentication that will contain “Bearer “ followed by our token from the previous request. The token itself will obviously change, but these things will generally stay the same.

set myreq = ##class(%Net.HttpRequest).%New()
set myreq.Server = "graph.microsoft.com"
set myreq.ContentType = "application/json"
set myreq.SSLConfiguration = "O365"
set myreq.Https = 1
do ..GetToken(.token)
do myreq.SetHeader("Authorization","Bearer "_token)

The body of this request will have a JSON structure, but it is the one that can get very complicated. It can optionally save the email to the sent items of any email address we are using as a sender. It is determined by the field in the body called saveToSentItems. If it is true, the user whose email address we utilize to send the message will be able to see it in their Sent Items folder. If it is false, they will not be able to do that. In my use case, we are sending a quote to a potential customer, so having this data in our sent items can come in handy when the customer replies. If you need it for more of a no-reply situation, you can opt out of it to save space.

set reqObj = ##class(%Library.DynamicObject).%New()
do reqObj.%Set("saveToSentItems",1,"boolean")

Besides that field, the only thing in our HTTP request body will be a message with a JSON object. Although it sounds simple, it is a complex data structure. The full documentation about it can be found here, but we will walk through the most important parts in the rest of this article.

set messageObj = ##class(%Library.DynamicObject).%New()

The message contains toRecipients, ccRecipients, bccRecipients, from, sender, and replyTo objects. All of them contain an emailAddress object with an email address and may as well include a name, yet it is not required. All of the recipient objects are arrays, whereas the from, sender, and replyTo are one single email address. The recipient ones are self-explanatory. They are the lists of people the address will go to.

We do need to know the email address of the sender, yet the from and sender fields are optional. If choose to set them, they are usually the same. However, if your Microsoft 365 is configured with some delegation options, they may be different. In our case, the email address we are sending from determines where we send the request. We will set the location of the HTTP request to v1.0/users/youremailaddress@yourdomain.com/sendMail. The replyTo is also optional, but it can be used to set a reply-to email address.

set myreq.Location = "v1.0/users/youremail@yourdomain.com/sendMail"

All of the abovementioned is pointless if we do not have anyone to send the message to, of course. For the toRecipients, ccRecipients, and bccRecipients, we can create a dynamic array and add email addresses to them.

set toArray = ##class(%Library.DynamicArray).%New()
set toObj = ##class(%Library.DynamicObject).%New()
set toAddress = ##class(%Library.DynamicObject).%New()
do toAddress.%Set("address","someemail@somedomain.com")
do toAddress.%Set("name","Their Name")
do toObj.%Set("emailAddress",toAddress)
do toArray.%Push(toObj)
do messageObj.%Set("toRecipients",toArray)

The subject field is a simple string.

do messageObj.%Set("subject","Test email")

The message object also contains a body object, which has two fields. The contentType field is a string as well, and it can be either “text” or “HTML”. Today we will use plain text, but if you want to include HTML in the body of your email, set this field accordingly. The other field is content, and it retains the actual contents of the email body. We can use a stream for it.

set bodyObj = ##class(%Library.DynamicObject).%New()
do bodyObj.%Set("contentType","text")
set bodyStream = ##class(%Stream.TmpCharacter).%New()
do bodyStream.Write("Hello World.")
do bodyObj.%Set("content",bodyStream,"stream")
do messageObj.%Set("body",bodyObj)

If we have attachments, we must set the hasAttachments property of the message to true. The attachment itself is another object in the message. This one will be a collection of attachment objects which will contain a field called @odata.type. As for a file attachment, it will always be #microsoft.graph.fileAttachment. The isInline boolean determines whether the attachment is in-line in the email. In our case, it will not be in-line, so we should set it to false. The name field is a string containing the file name, and the contentType field is a string retaining the file’s type, such as text/plain. It is optional. The size field is the size of the attachment. The field called contentBytes should hold the actual contents of the file. With this API, it can be up to 3 MB. A stream will be perfect for it too.

do messageObj.%Set("hasAttachments",1,"boolean")
set attachArray = ##class(%Library.DynamicArray).%New()
set attachObj = ##class(%Library.DynamicObject).%New()
do attachObj.%Set("@odata.type","#microsoft.graph.fileAttachment")
do attachObj.%Set("isInline",0,"boolean")
do attachObj.%Set("name","test.txt")
do attachObj.%Set("contentType","text/plain")
set attachStream = ##class(%Stream.TmpCharacter).%New()
do attachStream.Write("Testing attachments.")
do attachObj.%Set("contentBytes",attachStream,"stream>Base64")
do attachObj.%Set("size",attachStream.Size)
do attachArray.%Push(attachObj)
do messageObj.%Set("attachments",attachArray)

Now, all we have to do is add the message to the request object and write the request object to the HTTP request’s entity body. Then we should post our request.

do reqObj.%Set("message",messageObj)
do myreq.EntityBody.Write(reqObj.%ToJSON())
do myreq.Post()

If we have done everything properly, the request’s HttpResponse will have StatusCode of 202 “Accepted”, meaning that your message is on its way to your recipient!

Feel free to let me know in the comments about the other ways you use it, or hit me up if there are any other parts of the Graph API that you would like to see code samples for! 

Discussion (6)3
Log in or sign up to continue