smsframework

SMS framework with pluggable providers

Usage no npm install needed!

<script type="module">
  import smsframework from 'https://cdn.skypack.dev/smsframework';
</script>

README

Version Dependency Status Build Status

SMSframework

SMS framework with pluggable providers.

Key features:

  • Send messages
  • Receive messages
  • Delivery confirmations
  • Handle multiple providers with a single gateway
  • Event-driven interface
  • Reliable message handling
  • Leverages q promises
  • Unit-tested

Table of Contents

Supported Providers

SMSframework supports the following

Also see the full list of providers.

Gateway

SMSframework handles the whole messaging thing with a single Gateway object.

Let's start with initializing a gateway:

var smsframework = require('smsframework')
    ;

var gateway = new smsframework.Gateway();

The Gateway() constructor currently has no arguments.

Providers

A Provider is a package which implements the logic for a specific SMS provider.

Each provider reside in an individual package smsframework-*. You'll probably want to install some of these first.

Gateway.addProvider(provider, alias, config)

Arguments:

  • provider: String is the name of the provider package.
  • alias: String is the provider alias: an arbitrary string that uniquely identifies the provider instance. You'll use this string in order to send messages via a specific provider.
  • config: Object is the Provider-dependent configuration object. Refer to the provider documentation for the details.

When a package is require()d, it registers itself under the smsframework.providers namespace. You don't need to require providers manually, as SMSframework does this for you.

gateway.addProvider('provider1', 'alias1', {}); // package 'smsframework-provider1'
gateway.addProvider('provider2', 'alias2', {}); // package 'smsframework-provider2'

The first provider becomes the default one, unless you use Message Routing.

Gateway.addProvider(providers)

An alternative syntax to add providers in bulk.

Arguments:

  • providers: Array.<{ provider: String, alias: String, config: Object? }> is an array that specifies multiple providers:

    [
        { provider: 'provider1', alias: 'primary', config: {} },
        { provider: 'provider2', alias: 'secondary', config: {} }
    ]
    

This works perfectly when your providers are defined in the configuration. Consider using js-yaml:

providers:
    - { provider: 'provider1', alias: 'primary', config: { apitoken: '123456' } }
    - { provider: 'provider2', alias: 'secondary', config: { apitoken: '987654' } }

Then, in the application:

var config = yaml.load(fs.readFileSync('./config.yml', { encoding: 'utf8' }));
gateway.addProvider(config.providers);

Gateway.getProvider(alias):IProvider

Get a provider instance by its alias.

You don't normally need this, unless the provider has some public API: see provider documentation.

Sending Messages

To send a message, you first create it with Gateway.message(to, body) which returns a fluid interface object.

Arguments:

  • to: String: Recipient number
  • body: String: Message body

Properties:

  • message: OutgoingMessage: The wrapped Outgoing Message object

Methods:

  • send():Q: Send the message.

    The method returns a promise for OutgoingMessage which is resolved when a message is sent, or rejected on sending error. See Handling Send Errors.

  • from(from: String): Set the message originating number.

    Is used to pick a specific source number if the gateway supports that.

  • provider(provider: String): Choose the provider by alias.

    If no provider is specified - the first one is used to deliver the message.

  • route(..values): Specify routing values. See Message Routing.

  • options(options: OutgoingMessageOptions): Specify sending options:

    • allow_reply: Boolean: Allow replies for this message. Default: false

    • status_report: Boolean: Request a delivery report. Default: false. See: status

    • expires: Number?: Message validity period in minutes. Default: none.

    • senderId: String?: SenderID to replace the source number. Default: none.

      NOTE: This advanced feature is not supported by all providers! Moreover, some of them can have special restrictions.

    • escalate: Boolean: Is a high-priority message: these are delivered faster, at a higher price. Default: false.

  • params(params: Object): Specify provider-dependent sending parameters: refer to the provider documentation for the details.

All the above methods are optional, you can just send the message as is:

gateway.message('+123456', 'hi there').send().done(); // using the default provider

Here's the full example:

var smsframework = require('smsframework')
    ;

var gateway = new smsframework.Gateway();
gateway.addProvider('clickatell', 'primary', {});

gateway.message('+123456', 'hi there')
    .from('+1111')
    .provider('primary') // use the named provider
    .options({
        allow_reply: true,
        status_report: false,
        expires: 60,
        senderId: 'smsframework'
    })
    .params({
        // some provider-dependent parameters here
    })
    // Handle success
    .then(function(message){
        console.log('Message sent successfully!');
    })
    .catch(function(err){
        console.error('Failed to send the message', err.stack);
    });

If you dislike promises, you can always get back to the old good NodeJS-style callbacks:

gateway.message('+123456', 'hi there').send()
    .nodeify(function(err, message){
        // NodeJS callback
    });

Handling Send Errors

When you send() a message, the promise may resolve to an error.

The error object is provided as an argument to the callback function, and can be one of the following:

  • Error: unknown provider specified. You may have a typo, or the provider package is missing.
  • Error: runtime error occurred somewhere in the code. Rare.
  • smsframework.errors.SendMessageError: An advanced error object. Has the code field which defines the error conditions.

See smsframework.errors.SendMessageError for the list of supported error codes.

Example:

gateway.message('+123456', 'hi there').send()
    .catch(function(err){
        if (err.code === smsframework.errors.SendMessageError.codes.GEN_CREDIT)
            console.error('Not enough funds:', err);
        else
            console.error(err.stack);
    });

Events

The Gateway object is an EventEmitter which fires the following events:

msg-out

Outgoing Message: a message is being sent.

Arguments:

NOTE: It is not yet known whether the message was accepted by the Provider or not. Also, the msgid and info fields are probably not populated.

msg-sent

Outgoing Message: a message that was successfully sent.

Arguments:

The message object is populated with the additional information from the provider, namely, the msgid and info fields.

msg-in

Incoming Message: a message that was received from the provider.

Arguments:

status

Message Status: a message status reported by the provider.

A status report is only delivered when explicitly requested with options({ status_report: true }).

Arguments:

error

Error object reported by the provider.

Arguments:

  • error: Error|SendMessageError: The error object. See Error Objects.

Useful to attach some centralized logging utility. Consider winston for this purpose.

Functional Handlers

Events are handy, unless you need more reliability: if an event-handler fails to process the message, the Provider still sends 'OK' to the SMS service.. and the message is lost forever.

Functional handlers solve this problem: you register a callback function that returns a promise, and in case the promise is rejected - the provider reports an error to the SMS provider so it retries the delivery later.

Example:

// Handler for incoming messages
gateway.receiveMessage(function(message){
    return Q.nmcall(db, 'save', message);
});

// Handler for incoming status reports
gateway.receiveStatus(function(status){
    return Q.nmcall(db, 'update', status.msgid);
});

Whenever any of the handlers fail - the Provider reports an error to the SMS service, so the data is re-sent later.

Gateway.receiveMessage(callback):Gateway

Subscribe a callback to Incoming Messages. Can be called multiple times.

Arguments:

  • callback: function(IncomingMessage):Q: A callback that processes an Incoming Message. If it returns a rejection - the Provider reports an error to the SMS service.

Gateway.receiveStatus(callback):Gateway

Subscribe a callback to Message Statuses. Can be called multiple times.

Arguments:

  • callback: function(IncomingMessage):Q: A callback that processes a Message Status. If it returns a rejection - the Provider reports an error to the SMS service.

Data Objects

SMSframework uses the following objects to represent message flows.

IncomingMessage

A messsage received from the provider.

Source: lib/data/IncomingMessage.js.

OutgoingMessage

A message being sent.

Source: lib/data/OutgoingMessage.js.

MessageStatus

A status report received from the provider.

Source: lib/data/MessageStatus.js.

Error Objects

Source: lib/data/MessageStatus.js.

Provider HTTP Receivers

The Gateway has an internal express application, which is used by all providers to register their receivers: HTTP endpoints used to interact with the SMS services.

Each provider is locked under the /<alias> prefix.

The resources are provider-dependent: refer to the provider documentation for the details. The recommended approach is to use /im for incoming messages, and /status for status reports.

Gateway.express()

To use the receivers in your application, use Gateway.express() method which returns an express middleware:

var smsframework = require('smsframework'),
    express = require('express')
    ;

// Gateway
var gateway = new smsframework.Gateway();
gateway.addProvider('clickatell', 'primary', {}); // provider, alias 'primary'

// Init express
var app = express();
app.use('/sms', gateway.express()); // mount SMSframework middleware under /sms
app.listen(80); // start

// Ready to receive messages
gateway.on('msg-in', function(message){
    console.log('SMS from ' + message.from + ': ' + message.body);
});

Assuming that the provider declares a receiver as '/receiver', we now have a 'http://localhost:80/sms/primary/receiver' path available.

In your Clickatell admin area, add this URL so Clickatell passes the incoming messages to us.

Gateway.listen()

Sugar to start listening on the specified network address immediately.

Footprints (see net.Server):

  • `Gateway.listen(port[, host])
  • `Gateway.listen(path)
  • `Gateway.listen(handle)

Returns: a promise for the http.Server object.

Example:

gateway.listen(80).then(function(server){
    console.log('Listening on ', server.address()); // Get the address
    server.close(); // stop listening
});

Receiver Security

It's adviced to mount the receivers under some difficult-to-guess path: otherwise, attackers can send fake messages into your system

Secure example:

app.use('/sms/Zei6Ohth', gateway.express());

NOTE: Other mechanisms, such as basic authentication, are not typically useful as some services do not support that.

Message Routing

SMSframework requires you to explicitly specify the provider for each message, or uses the first one.

In real world conditions with multiple providers, you may want a router function that decides on which provider to use and which options to pick.

In order to achive flexible message routing, we need to associate some metadata with each message, for instance:

  • module: name of the sending module: e.g. "users"
  • type: type of the message: e.g. "notification"

These 2 arbitrary strings need to be standardized in the application code, thus offering the possibility to define complex routing rules.

When creating the message, use route() function to specify these values:

gateway.message('+1234', 'hi')
    .route('users', 'notification')
    .send().done();

Now, set a router function: a function which gets an outgoing message + some additional routing values, and decides on the provider to use:

gateway.addProvider('clickatell', 'primary', {});
gateway.addProvider('clickatell', 'secondary', {});
gateway.addProvider('clickatell', 'usa', {});

gateway.setRouter(function(message, module, type){
    // Use 'usa' for all messages to USA
    if (/^+1/.test(message.to))
        return 'usa';
    // Use 'secondary' for notifications
    if (type === 'notification')
        return 'secondary';
    // Use 'primary' as a default
    return 'primary';
});

Router function is also the right place to specify message options & parameters.

To unset the router function, call gateway.setRouter() with no arguments.

Bundled Providers

The following providers are bundled with SMSframework and thus require no additional packages.

NullProvider

Source: lib/providers/null.js

The 'null' provider just ignores all outgoing messages.

Example:

gw.addProvider('null', 'null'); // provider, alias

LogProvider

Source: lib/providers/log.js

The 'log' provider just logs all outgoing messages without sending them anywhere.

Config:

  • log: function(OutgoingMessage): The logger function. Defalt: log message destination & text to the console.

Example:

gw.addProvider('log', 'log', {
    log: function(message){
        console.log('SMS to', message.to, ':', message.body);
    }
});

LoopbackProvider

Source: lib/providers/loopback.js

The 'loopback' provider is used as a dummy for testing purposes.

It consumes all messages, supports delivery notifications, and even has a '/im' HTTP receiver.

All messages flowing through it get incremental msgids starting from 1.

LoopbackProvider.getTraffic():Array.<IncomingMessage|OutgoingMessage>

LoopbackProvider stores all messages that go through it. To get those messages, call .getTraffic().

This method empties the message log.

gateway.addProvider('loopback', 'lo', {});
gateway.message('+123', 'hi').send()
    .then(function(){
        gateway.getProvider('lo').getTraffic(); // array of messages
    });

LoopbackProvider.receive(from, body):Q

Simulate an incoming message.

Arguments:

  • from: String: Source number
  • body: String: Message text

Returns: a promise for a message processed by SMSframework

gateway.on('msg-in', function(message){
    console.log(message.from, ':', message.body);
});
gateway.getProvider('lo').receive('+1111', 'notification!');

LoopbackProvider.subscribe(sim, callback)

Register a virtual subscriber which receives messages to the matching number.

Arguments:

  • sim: String: Subscriber phone number

  • callback: function(from: String, body: String, reply:function(String):Q): A callback which gets the messages sent to this subscriber.

    The last argument is a convenience function to send a reply. It wraps LoopbackProvider.receive().

gateway.getProvider('lo').subscribe('+123456', function(from, body, reply){
    reply('Hi '+from+'!');
});
gateway.message('+123456', 'hi').send();