With the fourth part in the web services series we are going to look at a web service that draw a lot of attention at Directions and NAV Techdays: sending SMS messages from Dynamics NAV.
Now this is something that has already been done by several others, including ISV’s, and I don’t want to give the impression that this demo is the only way to do it. What I just want to demonstrate is how easy it can be to send out an SMS text message directly from Dynamics NAV code.
The code will again use the basic pattern that was explained in the first part of the series. So make sure to understand at least the basic pattern so you can follow the code.
To send out an SMS text message to someone’s mobile is definitely something you need an external library or service for. There is no such function in Dynamics NAV that you could use or reuse. I don’t think I need to explain in what case you want to send an SMS, right? Just one suggestion: send a text message during login to the user with a code that they have to enter first, before they can continue to open the application. With this procedure we can introduce two factor authentication in Dynamics NAV! Example is included, see below.
The web service
There are a lot of web services out there to send SMS text messages. During my search I found the the service from BulkSMS easy to use and very reliable. What I didn’t know is that BulkSMS is a sponsor of NAV Techdays. I heard that after I did the presentation at Directions and NAV Techdays, but it proves there is a match as BulkSMS is aware of Dynamics NAV.
Like any other mobile service, BulkSMS is not for free. You have to buy credits in your account. I didn’t look at all countries, but it seems that prices in The Netherlands are amongst the highest. Prices and coverage can be found here: http://www.bulksms.com/pricing/. There is no such thing as a developer account with monthly credits. You will have to buy a (small) package for testing and developing purposes. I bought myself a € 10 package before the presentation at Directions EMEA.
A nice feature of BulkSMS is that you can personalize the Sender ID. Instead of the name BulkSMS or a long phone number, the recipient will see your name. You can request a change of the Sender ID in your BulkSMS account settings.
All right, now straight to the web service API. The complete API can be found here: http://developer.bulksms.com/eapi. The API call we are looking for is send_sms (http://developer.bulksms.com/eapi/submission/send_sms). Like other web services in this series, the send_sms service can be used with the HTTP GET method. However, the documentation states that the HTTP POST method is the preferred method, so we will use that.
According to the documentation, we need to provide a number of parameters:
- msisdn (the phone number of the recipient, this can be a comma-separated list for multiple recipients)
First of all we need to save a username and password in the database. I’m not going into too much details as this is somewhat off-topic. However, I want to take the opportunity to bring the pattern for secure storing passwords to your attention.
In the example code the Service Password table is used to store the password. Take a look at table SetPassword and GetPassword function in table 60004 BulkSMS Setup, and how it is used in the Page:
Code in Table BulkSMS Setup
<br /> SetPassword(PasswordText : Text)<br /> IF ISNULLGUID("Password Key") OR NOT ServicePassword.GET("Password Key") THEN BEGIN<br /> ServicePassword.SavePassword(PasswordText);<br /> ServicePassword.INSERT(TRUE);<br /> "Password Key" := ServicePassword.Key;<br /> END ELSE BEGIN<br /> ServicePassword.SavePassword(PasswordText);<br /> ServicePassword.MODIFY;<br /> END;</p> <p>GetPassword() : Text<br /> IF NOT ISNULLGUID("Password Key") THEN<br /> IF ServicePassword.GET("Password Key") THEN<br /> EXIT(ServicePassword.GetPassword());<br />
Code in the Page BulkSMS Setup
<br /> OnAfterGetRecord()<br /> PasswordTemp := '';<br /> IF ("User Name" <> '') AND (NOT ISNULLGUID("Password Key")) THEN<br /> PasswordTemp := '**********';</p> <p>PasswordTemp - OnValidate()<br /> SetPassword(PasswordTemp);<br /> COMMIT;<br /> CurrPage.UPDATE;<br />
Having said that, we can continue to the real stuff: calling the web service. The example code contains a Codeunit 60004 SMS Web Service with a function, SendSMS, which accepts a phone number and a message.
<br /> BulkSMSSetup.GET;<br /> BulkSMSSetup.TESTFIELD("User Name");<br /> BulkSMSSetup.TESTFIELD("Password Key");</p> <p>Window.OPEN('Sending SMS...');</p> <p>data := 'username=' + httpUtility.UrlEncode(BulkSMSSetup."User Name",encoding.GetEncoding('ISO-8859-1'));<br /> data += '&password=' + httpUtility.UrlEncode(BulkSMSSetup.GetPassword(),encoding.GetEncoding('ISO-8859-1'));<br /> data += '&message=' + httpUtility.UrlEncode(MessageText,encoding.GetEncoding('ISO-8859-1'));<br /> data += '&msisdn=' + PhoneNo;<br /> data += '&want_report=0';</p> <p>stringContent := stringContent.StringContent(data,encoding.UTF8,'application/x-www-form-urlencoded');</p> <p>ReturnValue := RESTWSManagement.CallRESTWebService('https://bulksms.vsms.net/',<br /> 'eapi/submission/send_sms/2/2.0',<br /> 'POST',<br /> stringContent,<br /> HttpResponseMessage);<br /> Window.CLOSE;</p> <p>IF NOT ReturnValue THEN<br /> EXIT;</p> <p>result := HttpResponseMessage.Content.ReadAsStringAsync.Result;</p> <p>separator := '|';<br /> resultParts := result.Split(separator.ToCharArray());<br /> statusCode := resultParts.GetValue(0);<br /> statusText := resultParts.GetValue(1);</p> <p>IF NOT (statusCode IN ['0','1']) THEN<br /> ERROR('Sending SMS message failed!\Statuscode: %1\Description: %2',statusCode,statusText);<br />
A few words about the consequences of using the HTTP POST method. This means that the parameters are not send as part of the URL, but they will be send in the request body. This is exactly the same technique when you fill out a form on a web page and click the submit button.
To achieve this behavior the PostAsync of the HttpClient method is used (see the explanation of the basic pattern in this post). The PostAsync method needs an extra parameter of the .Net type HttpContent. This HttpContent is a base class that represents the HTTP body and content headers. The .Net type StringContent that is used in the example code is an inherited class (via the ByteArrayContent) of the HttpContent class.
Basically, when creating a StringContent object, we are creating an HttpContent object based on a string that automatically includes the correct content headers. These content headers are very important for HTTP methods because these headers declare what type of data can be expected in the body.
In this case, we are POSTing data in the same way as a web form. To tell the StringContent that it is sending a web form, we need to provide the correct media type. This is the ‘application/x-www-form-urlencoded’ value that can see in the code.
In the response we get information if the SMS was sent successfully or not. A value of 0 means that it was sent immediately. The value 1 means that it has been scheduled (as per an extra parameter in the request, didn’t implement that in the example). Any other value means that there was an error.
The response is just a string, with the status code and description divided by a | character. The code in the example splits the string into a .Net array. For more information how to split a string to an array, please see this post
Suggestions for usage
How can we use this web service? Just a few suggestions:
Send a text message manually.
A Page is included in the demo code that can be used for this.
Implement two-factor authentication:
- Create an event subscriber for the OnBeforeCompanyOpen event in Codeunit 1.
- In this event generate a random code and send it to the user as an SMS message.
- Open a modal Page, asking the user the enter the code from the SMS message.
- If the user cancels this Page, or types an incorrect code several times, end the session by using the ERROR function.
The demo code contains an example for this purpose.
And the example code in Codeunit 60005 Two Factor Authentication Mgt. This example is based on eventing in NAV 2016 but can easily be modified for earlier versions.
<br /> LOCAL [EventSubscriber] TwoFactorAuthenticationBeforeCompanyOpen()<br /> IF NOT UserSetup.GET(USERID) THEN<br /> EXIT;</p> <p>IF NOT UserSetup."Use Two Factor Authentication" THEN<br /> EXIT;</p> <p>SMSCode := FORMAT(RANDOM(100000000));<br /> MessageText := STRSUBSTNO('Use this code to login in %1: %2',COMPANYNAME,SMSCode);<br /> SMSWebService.SendSMS(UserSetup."Phone No.",MessageText);<br /> TryAgain := TRUE;</p> <p>WHILE TryAgain DO BEGIN<br /> CLEAR(EnterSMSCode);<br /> IF EnterSMSCode.RUNMODAL <> ACTION::OK THEN<br /> ERROR('You canceled the login procedure');</p> <p> UserResponse := EnterSMSCode.GetSMSCode;</p> <p> CodeIsValid := UserResponse = SMSCode;<br /> Counter += 1;<br /> TryAgain := (NOT CodeIsValid) AND (Counter < 3);<br /> END;</p> <p>IF NOT CodeIsValid THEN<br /> ERROR('You entered an invalid code for 3 times');
This is the Page that is displayed before the user actually get access to the application.
Entering an incorrect code for more than 3 times aborts the login procedure.
That’s all about it…
Oh wait, just forgotten to mention one small extra feature in the demo code: Get Credits. In the API of BulkSMS there is method to get the current credits (http://developer.bulksms.com/eapi/account/get_credits). It took no more then 10 minutes to add this web service method to the example code.
<br /> GetCredits(VAR BulkSMSSetup : Record "BulkSMS Setup")<br /> BulkSMSSetup.TESTFIELD("User Name");<br /> BulkSMSSetup.TESTFIELD("Password Key");</p> <p>RESTWSManagement.CallRESTWebService('https://bulksms.vsms.net/',<br /> STRSUBSTNO('eapi/user/get_credits/1/1.1?username=%1&password=%2',BulkSMSSetup."User Name",BulkSMSSetup.GetPassword()),<br /> 'GET',<br /> null,<br /> HttpResponseMessage);</p> <p>result := HttpResponseMessage.Content.ReadAsStringAsync.Result;</p> <p>separator := '|';<br /> resultParts := result.Split(separator.ToCharArray());<br /> statusCode := resultParts.GetValue(0);<br /> statusText := resultParts.GetValue(1);</p> <p>IF statusCode <> '0' THEN<br /> ERROR('GetCredits failed!\Statuscode: %1\Description: %2',statusCode,statusText);</p> <p>EVALUATE(BulkSMSSetup.Credits, statusText,9);<br /> BulkSMSSetup."Credits Checked At" := CURRENTDATETIME;<br /> BulkSMSSetup.MODIFY;<br />
Well, that’s it! Hope you enjoyed it. I’m curious what extraordinary applications you will make with this easy-to-use web service!