Short Message Peer-to-Peer (SMPP) is a protocol used by the telecommunications industry for exchanging messages between Short Message Service Centers (SMSC) and/or External Short Messaging Entities (ESME).

Introduction

In this post I will build a simple app that will send SMS using the SMPP protocol, bootstraped with Spring Boot. I will use the Cloudhopper SMPP library for sending SMS. For the Camel Component example, refer to this Camel Post.

SMPP is a level-7 TCP/IP protocol, which allows fast delivery of SMS messages. The most conmmonly used versions of SMPP are v3.3, the most widely supported standard, and v3.4, which adds transceiver support (single connections that can send and receive messages).

Prerequisites

Project Structure

At the end of this guide our folder structure will look similar to the following:

.
|__src/
|  |__main/
|  |  |__java/
|  |  |  |__com/
|  |  |  |  |__juliuskrah/
|  |  |  |  |  |__smpp/
|  |  |  |  |  |  |__Application.java
|  |  |  |  |  |  |__ApplicationProperties.java
|  |  |  |  |  |  |__ClientSmppSessionHandler.java
|  |  |  |  |  |  |__DeliveryReceipt.java
|  |  |__resources/
|  |  |  |__application.yaml
|__pom.xml

Project Setup

Create a simple Spring Boot Application using any of the bootstrap options.

Add the following to your pom.xml after creating your Spring-Boot application:

<dependency>
  <groupId>com.fizzed</groupId>
  <artifactId>ch-smpp</artifactId>
  <version>5.0.9</version>
</dependency>

Configuring the SMPP Session

In order to start using SMPP to send messages, you need to establish a session. The session is usually short-lived; we will implement long-lived sessions later in this post by extending the session periodically.

To bind a session, we need a SmppSessionConfiguration and SmppClient. The SmppSessionConfiguration class contains the configurable aspects of the SmppSession. In this class you can configure the:

  • Host: [smpp.serviceprovider.com]
  • Port: default 2775
  • System Id: [username]
  • Password: [password]
  • BindType: TRANSCEIVER, TRANSMITTER or RECEIVER
  • Version: 3.3, 3.4, 5.0

Configuring this class will look something like the following:

public SmppSessionConfiguration sessionConfiguration() {
  SmppSessionConfiguration sessionConfig = new SmppSessionConfiguration();
  sessionConfig.setName("smpp.session");
  sessionConfig.setInterfaceVersion(SmppConstants.VERSION_3_4);
  sessionConfig.setType(SmppBindType.TRANSCEIVER);
  sessionConfig.setHost("<replace>");
  sessionConfig.setPort(2775);
  sessionConfig.setSystemId("<replace>");
  sessionConfig.setPassword("<replace>");
  sessionConfig.setSystemType(null);
  sessionConfig.getLoggingOptions().setLogBytes(false);
  sessionConfig.getLoggingOptions().setLogPdu(true);

  return sessionConfig;
}

NOTE: Be sure to replace the values for Host, SystemId and Password.

I have set the bind-type as TRANSCEIVER because I want to be able to send SMS and receive delivery receipts.

What I have to do next is create the SmppClient:

public SmppClient clientBootstrap() {
  return new DefaultSmppClient(Executors.newCachedThreadPool(), 2);
}

The DefaultSmppClient constructor takes an ExecutorService and expected number of sessions. In the example above I am creating a CachedThreadPoolExecutor and assigning 2 concurrent sessions.

With these two in place I can now establish my SmppSession:

@Bean(destroyMethod = "destroy")
public SmppSession session() throws SmppBindException, SmppTimeoutException, SmppChannelException,
    UnrecoverablePduException, InterruptedException {
  SmppSessionConfiguration config = sessionConfiguration();
  // Will use the DefaultSmppSessionHandler for now
  SmppSession session = clientBootstrap().bind(config, new DefaultSmppSessionHandler());

  return session;
}

In the above snippet, we are using a DefaultSmppSessionHandler. Later in this post I will create a custom SmppSessionListener that can handle delivery receipts.

At this point all necessary configuration is done. Everything wired up together until this point should look like this:

file: src/main/java/com/juliuskrah/smpp/Application.java

@SpringBootApplication
public class Application {
  private static final Logger log = LoggerFactory.getLogger(Application.class);

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

  public SmppSessionConfiguration sessionConfiguration() {
    SmppSessionConfiguration sessionConfig = new SmppSessionConfiguration();
    sessionConfig.setName("smpp.session");
    sessionConfig.setInterfaceVersion(SmppConstants.VERSION_3_4);
    sessionConfig.setType(SmppBindType.TRANSCEIVER);
    sessionConfig.setHost("<replace>");
    sessionConfig.setPort(2775);
    sessionConfig.setSystemId("<replace>");
    sessionConfig.setPassword("<replace>");
    sessionConfig.setSystemType(null);
    sessionConfig.getLoggingOptions().setLogBytes(false);
    sessionConfig.getLoggingOptions().setLogPdu(true);

    return sessionConfig;
  }

  @Bean(destroyMethod = "destroy")
  public SmppSession session() throws SmppBindException, SmppTimeoutException, SmppChannelException,
      UnrecoverablePduException, InterruptedException {
    SmppSessionConfiguration config = sessionConfiguration();
    SmppSession session = clientBootstrap().bind(config, new DefaultSmppSessionHandler());

    return session;
  }

  public SmppClient clientBootstrap() {
    return new DefaultSmppClient(Executors.newCachedThreadPool(), 2);
  }
}

If you have setup your SMPP correctly with your service provider, run the code as a Spring-Boot app:

PS C:\> mvn spring-boot:run

Send an SMS

Stop the aplication if it is still running. I will add a simple method that sends an SMS on application startup:

file: src/main/java/com/juliuskrah/smpp/Application.java

// ...

private void sendTextMessage(SmppSession session, String sourceAddress, String message, String destinationAddress) {
  // Check if session is still active
  if (session.isBound()) {
    try {
      // request delivery
      boolean requestDlr = true;
      SubmitSm submit = new SubmitSm();
      byte[] textBytes;
      textBytes = CharsetUtil.encode(message, CharsetUtil.CHARSET_ISO_8859_1);
      // set encoding for sending SMS
      submit.setDataCoding(SmppConstants.DATA_CODING_LATIN1);
      if (requestDlr) {
        submit.setRegisteredDelivery(SmppConstants.REGISTERED_DELIVERY_SMSC_RECEIPT_REQUESTED);
      }

      if (textBytes != null && textBytes.length > 255) {
        submit.addOptionalParameter(
            new Tlv(SmppConstants.TAG_MESSAGE_PAYLOAD, textBytes, "message_payload"));
      } else {
        submit.setShortMessage(textBytes);
      }

      submit.setSourceAddress(new Address((byte) 0x05, (byte) 0x01, sourceAddress));
      submit.setDestAddress(new Address((byte) 0x01, (byte) 0x01, destinationAddress));
      // submit message to SMSC for delivery with a timeout of 10000ms
      SubmitSmResp submitResponse = session.submit(submit, 10000);
      if (submitResponse.getCommandStatus() == SmppConstants.STATUS_OK) {
        log.info("SMS submitted, message id {}", submitResponse.getMessageId());
      } else {
        throw new IllegalStateException(submitResponse.getResultMessage());
      }
    } catch (RecoverablePduException | UnrecoverablePduException | SmppTimeoutException | 
        SmppChannelException | InterruptedException e) {
        throw new IllegalStateException(e);
    }
    return;
  }
  throw new IllegalStateException("SMPP session is not connected");
}

Finally, let us call the method to send SMS:

file: src/main/java/com/juliuskrah/smpp/Application.java

// ...

public static void main(String[] args) {
  ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args);
  SmppSession session = ctx.getBean(SmppSession.class);
  new Application().sendTextMessage(session, "3299", "Hello World", "<replace with phone number>");
}

Add your phone number to destinationAddress and run the application. Remember to use the international phone number format.

At this point you are done and can choose not to follow the rest of this post.

Making it Better

You may have noticed that, we hardcoded certain things like username, password and host in the source code. This does not make the app very portable. Let’s make use of externalized configuration, which has extensive support in Spring-Boot.

I will create a class ApplicationProperties which contains all the external properties we need:

file: src/main/java/com/juliuskrah/smpp/ApplicationProperties.java

@ConfigurationProperties("sms")
public class ApplicationProperties {
  @NestedConfigurationProperty
  private final Async async = new Async();
  @NestedConfigurationProperty
  private final SMPP smpp = new SMPP();

  public Async getAsync() {
    return async;
  }

  public SMPP getSmpp() {
    return smpp;
  }

  public static class Async {
    /**
     * This number should be lower than the value assigned to the core-pool-size
     */
    private int smppSessionSize = 2;
    private int corePoolSize = 5;
    private int maxPoolSize = 50;
    private int queueCapacity = 10000;
    private int initialDelay = 1000;
    private int timeout = 10000;

    // omitted getters and setters
  }

  public static class SMPP {
    private String host;
    private String userId;
    private String password;
    private int port = 2775;
    private boolean requestDelivery = false;
    private boolean detectDlrByOpts = false;

    // omitted getters and setters
  }
}

I have added default values for most of the setings in the above class. I will override some of the defaults in a yaml file:

file: src/main/resources/application.yaml

sms:
  smpp:
    host: <replace>
    user-id: <replace>
    password: <replace>
    requestDelivery: true

In the above, I am overriding the default value of requestDelivery to true.

In order to use this in our simple application, I need to register the ApplicationProperties configuration class:

file: src/main/java/com/juliuskrah/smpp/Application.java

@SpringBootApplication
@EnableConfigurationProperties(ApplicationProperties.class)
public class Application {
  // ...
}

I will rewrite sessionConfiguration method to use the externalized configuration:

file: src/main/java/com/juliuskrah/smpp/Application.java

// ...

public SmppSessionConfiguration sessionConfiguration(ApplicationProperties properties) {
SmppSessionConfiguration sessionConfig = new SmppSessionConfiguration();
  sessionConfig.setName("smpp.session");
  sessionConfig.setInterfaceVersion(SmppConstants.VERSION_3_4);
  sessionConfig.setType(SmppBindType.TRANSCEIVER);
  sessionConfig.setHost(properties.getSmpp().getHost());
  sessionConfig.setPort(properties.getSmpp().getPort());
  sessionConfig.setSystemId(properties.getSmpp().getUserId());
  sessionConfig.setPassword(properties.getSmpp().getPassword());
  sessionConfig.setSystemType(null);
  sessionConfig.getLoggingOptions().setLogBytes(false);
  sessionConfig.getLoggingOptions().setLogPdu(true);

  return sessionConfig;
}

At the beginning of this post I talked about receiving delivery receipts. The default implementation of SmppSessionListener is to discard received PDUs. So I will extend the default implementation class and handle delivery receipts in there:

file: src/main/java/com/juliuskrah/smpp/ClientSmppSessionHandler.java

public class ClientSmppSessionHandler extends DefaultSmppSessionHandler {
  private static final Logger log = LoggerFactory.getLogger(ClientSmppSessionHandler.class);
  private final ApplicationProperties properties;

  public ClientSmppSessionHandler(ApplicationProperties properties) {
    this.properties = properties;
  }

  private String mapDataCodingToCharset(byte dataCoding) {
    // implementaion in the github repository
  }

  @Override
  @SuppressWarnings("rawtypes")
  public PduResponse firePduRequestReceived(PduRequest request) {
    PduResponse response = null;
    try {
      if (request instanceof DeliverSm) {
        String sourceAddress = ((DeliverSm) request).getSourceAddress().getAddress();
        String message = CharsetUtil.decode(((DeliverSm) request).getShortMessage(),
            mapDataCodingToCharset(((DeliverSm) request).getDataCoding()));
        log.debug("SMS Message Received: {}, Source Address: {}", message.trim(), sourceAddress);

        boolean isDeliveryReceipt = false;
        if (properties.getSmpp().isDetectDlrByOpts()) {
          isDeliveryReceipt = request.getOptionalParameters() != null;
        } else {
          isDeliveryReceipt = SmppUtil.isMessageTypeAnyDeliveryReceipt(
            ((DeliverSm) request).getEsmClass());
        }

        if (isDeliveryReceipt) {
          DeliveryReceipt dlr = DeliveryReceipt.parseShortMessage(message, ZoneOffset.UTC);
          // logging delivery here, but you can do something more useful over here
          log.info("Received delivery from {} at {} with message-id {} and status {}", sourceAddress,
              dlr.getDoneDate(), dlr.getMessageId(), DeliveryReceipt.toStateText(dlr.getState()));
        }
      }
      response = request.createResponse();
    } catch (Throwable error) {
      log.warn("Error while handling delivery", error);
      response = request.createResponse();
      response.setResultMessage(error.getMessage());
      response.setCommandStatus(SmppConstants.STATUS_UNKNOWNERR);
    }
    return response;
  }
}

The DeliveryReceipt class is a utility class I created to extract delivery details from PduRequest. I will not paste the contents of the class here (it is quite lengthy), you can view its contents in the Github repository.

We just need to register this handler to take advantage of the delivery receipt handling:

file: src/main/java/com/juliuskrah/smpp/Application.java

@Bean(destroyMethod = "destroy")
public SmppSession session(ApplicationProperties properties) throws SmppBindException, 
    SmppTimeoutException, SmppChannelException,
    UnrecoverablePduException, InterruptedException {
  SmppSessionConfiguration config = sessionConfiguration(properties);
  SmppSession session = clientBootstrap().bind(config, new ClientSmppSessionHandler(properties));

  return session;
}

If you run the application now, you will see delivery receipts printed in the console. This solution however is far from ideal. As I said earlier, the Sessions are short-lived; given a scenario where a mobile device is switched off, the SMS will be delivered only when the device is switched back on. In this same scenario, the SMPP Session may be closed when the delivery is received, leading to lost delivery receipts. To overcome this limitation I have to design the application to periodically refresh the session before it un-binds.

I will use EnquireLinkResp to periodically extend the session:

file: src/main/java/com/juliuskrah/smpp/Application.java

@EnableScheduling
@SpringBootApplication
@EnableConfigurationProperties(ApplicationProperties.class)
public class Application {
  // ...

  @Scheduled(
      initialDelayString = "${sms.async.initial-delay}", 
      fixedDelayString = "${sms.async.initial-delay}")
  void enquireLinkJob() {
    if (session.isBound()) {
      try {
        log.info("sending enquire_link");
        EnquireLinkResp enquireLinkResp = session.enquireLink(new EnquireLink(),
            properties.getAsync().getTimeout());
        log.info("enquire_link_resp: {}", enquireLinkResp);
      } catch (SmppTimeoutException e) {
        log.info("Enquire link failed, executing reconnect; " + e);
        log.error("", e);
      } catch (SmppChannelException e) {
        log.info("Enquire link failed, executing reconnect; " + e);
        log.warn("", e);
      } catch (InterruptedException e) {
        log.info("Enquire link interrupted, probably killed by reconnecting");
      } catch (Exception e) {
        log.error("Enquire link failed, executing reconnect", e);
      }
    } else {
      log.error("enquire link running while session is not connected");
    }
  }
}

I have added annotation on top of the class to @EnableScheduling which allows spring to process the @Scheduled annotation. The fixedDelayString value ${sms.async.initial-delay} is set in the application.yaml file:

file: src/main/resources/application.yaml

sms:
  async:
    initial-delay: 30000

This will cause the application to enquire link every 30 seconds. This solution is far from perfect. When internet connectivity is lost on the server running the application, this will not work. Also when the Session unexpectedly unbinds this solution will not work. I have implemented a more robust solution on the persistent branch of the Github repository accompanying this post.

Before we wrap up, it is not very good practice to store sentive information in plain text on files. There are several best practices and solutions out there e.g. using Vault from Hashicorp. I am going to implement the simplest solution by using environment variables:

PS C:\> set SMS_SMPP_HOST=<host>
PS C:\> set SMS_SMPP_USER_ID=<user>
PS C:\> set SMS_SMPP_PASSWORD=<password>

Conclusion

In this post we looked briefly at the SMPP protocol and its usage within the JVM. We also covered how to use Spring Boot to make developing an SMPP client easier.

As usual you can find the full example to this guide in the github repository. Until the next post, keep doing cool things :+1:.