%% @copyright 2016 Voyager Innovations Inc.
%% @doc SMPP Client
%%
%% This module exposes a start_link/1 function that accepts bind parameters
%% and starts then registers an internal gen_server. A reference will then be
%% returned from the function.
%%
%% The start_link/1 completes the bind process and other exposed functions
%% implements capabilities of a SMPP client (version 3.4)
-module(esmpp).
-export([
start_link/1,
send_sms/4,
send_sms/5,
send_sms/6,
callback/3
]).
-include("types.hrl").
-include("commands.hrl").
%% @doc Starts the connection to the SMPP server and does the binding process.
%%
%% `Options' is a map that should contain the following keys:
%%
%%
`name'
%% If provided, the internal gen_server will be registered with this name.
%% see gen_server:start_link/4
%% `mode'
%% Type of SMPP binding for the client
%% `callback_mo'
%% Tuple of module and function atoms to be executed when a mobile
%% originating message has been received
%% `callback_dr'
%% Tuple of module and function atoms to be executed when a delivery
%% receipt has been received
%% `host'
%% Hostname or IP address of the SMPP server
%% `port'
%% Port of the SMPP server
%% `system_id'
%% Identifier for the SMPP client
%% `password'
%% Password used to authenticate with the system_id
%% `system_type'
%% Identifier for the type of SMPP Client
%% `addr_ton'
%% Type of number for the SMPP server address.
%% If unknown, can be unset. See type of number values
%% `addr_npi'
%% Number planning indicator for the SMPP server address.
%% If unknown, can be unset. See number planning indicator values
%% `addr_range'
%% Used by tranceiver and receiver modes to specify which address range the
%% SMPP client is ready to receive. If unknown, can be unset
%%
%% Type Of Number values:
%%
%% `0 - Unknown'
%% `1 - International'
%% `2 - National'
%% `3 - Network Specific'
%% `4 - Subscriber Number'
%% `5 - Alphanumeric'
%% `6 - Abbreviated'
%%
%% Number Planning Indicator values:
%%
%% `0 - Unknown'
%% `1 - E163/E164'
%% `3 - X.121'
%% `4 - F.69'
%% `6 - E.212'
%% `8 - National Numbering Plan'
%% `9 - Private Numbering Plan'
%% `10 - ETSI DE/PS 3 01-3'
%% `13 - IP'
%% `18 - WAP Client ID'
%%
-spec start_link(Options) -> {ok, pid()} | ignore | {error, term()}
when Options :: #{ name => ServerName ,
mode => Mode ,
callback_mo => Callback ,
callback_dr => Callback ,
host => iodata() ,
port => integer() ,
system_id => iodata() ,
password => iodata() ,
system_type => iodata() ,
addr_ton => integer() ,
addr_npi => integer() ,
addr_range => iodata() },
Callback :: {atom(), atom()},
Mode :: transmitter | transceiver | receiver,
ServerName :: {local, Name :: atom()} |
{global, GlobalName :: term()} |
{via, Module :: atom(), ViaName :: term()}.
start_link(Options) ->
Name = maps:get(name, Options, '__undefined__'),
Mode = maps:get(mode, Options, '__undefined__'),
SystemId = maps:get(system_id, Options, <<0>>),
Password = maps:get(password, Options, <<0>>),
AddrRange = maps:get(addr_range, Options, <<0>>),
SystemType = maps:get(system_type, Options, <<0>>),
AddrTon = maps:get(addr_ton, Options, 0),
AddrNpi = maps:get(addr_npi, Options, 0),
{binding, Binding} = get_binding(Mode),
BindRecord = #bind_pdu{
binding = Binding,
system_id = SystemId,
password = Password,
system_type = SystemType,
addr_ton = AddrTon,
addr_npi = AddrNpi,
addr_range = AddrRange
},
start_link(Name, esmpp_worker, [Options, BindRecord]).
%% @private
start_link('__undefined__', Module, Args) ->
gen_server:start_link(Module, Args, []);
start_link(Name, Module, Args) ->
gen_server:start_link(Name, Module, Args, []).
%% @see send_sms/5
-spec send_sms(pid(), iodata(), iodata(), iodata()) -> [{message_id, iodata()}] | {error, atom()}.
send_sms(C, Sender, Destination, Message) ->
send_sms(C, Sender, Destination, Message, #{}).
%% @see send_sms/6
-spec send_sms(pid(), iodata(), iodata(), iodata(), map()) -> [{message_id, iodata()}] | {error, atom()}.
send_sms(C, Sender, Destination, Message, Options) ->
send_sms(C, Sender, Destination, Message, Options, #{}).
%% @doc Sends a short message to the specified MSISDN
%%
%% `Options' is an optional map that may contain the following keys:
%%
%% `service_type'
%% Indicates the SMS application service associated with the message
%% `protocol_id'
%% Network specific protocol identifier
%% `priority_flag'
%% Designates the priority level of the message.
%% See priority flag values
%% `delivery_time'
%% Either the absolute date and time or relative time from the current
%% SMSC time which the delivery of the message is to be first attempted
%% `validity_period'
%% SMSC expiration time that indicates when the message should be discarded
%% when the message is not delivered
%% `reg_delivery'
%% Indicates if a delivery receipt will be requested from the SMSC.
%% See delivery request values
%% `replace_flag'
%% Used to request the SMSC to replace a previously submitted message,
%% that is pending delivery. The SMSC will replace the existing message
%% if the source address, destination address, and service type match
%% the same fields in the new message
%%
%% `OptionalParameters' is another optional map that goes to the optional
%% parameters part of the PDU. For this particular operation, the following
%% keys can be used:
%%
%% `user_message_reference'
%% Reference number assigned by the SMPP clientto the message
%% `source_port'
%% Indicates the application port number associated with the source
%% address of the message. Should be present for WAP applications
%% `source_addr_subunit'
%% The subcomponent in the destination device which created the user data.
%% See subunit values
%% `destination_port'
%% Indicates the application port number associated with the destination
%% address of the message. Should be present for WAP applications
%% `dest_addr_subunit'
%% The subcomponent in the destination device for which the user data
%% is intended. See subunit values
%% `sar_msg_ref_num'
%% Reference number of a particular concatenated message
%% `sar_total_segments'
%% Total number of messages within the concatenated message
%% `sar_segment_seqnum'
%% Sequence number of a particular message fragment within the concatenated
%% message
%% `more_messages_to_send'
%% Indicates that there are more messages to follow
%% `payload_type'
%% Defines the type of payload. See payload
%% types
%% `message_payload'
%% Extended short message user data
%% `privacy_indicator'
%% Indicates the level of privacy associated with the message.
%% See privacy indicator values
%% `callback_num'
%% Callback number associated with the message
%% `callback_num_pres_ind'
%% Defines the callback number presentation and screening
%% `callback_num_atag'
%% Associates a displayable alphanumeric tag with the callback number
%% `source_subaddress'
%% Subaddress of the message originator
%% `dest_subaddress'
%% Subaddress of the message destination
%% `user_response_code'
%% A user response code. Implementation specific
%% `display_time'
%% Provides the receiving MS with a display time associated with the
%% message
%% `sms_signal'
%% Indicates the alerting mechanism when the message is received by an MS
%% `ms_validity'
%% Indicates the validity information for the message to the receiving MS.
%% See MS validity values
%% `ms_msg_wait_facilities'
%% Controls the indication and specifies the message type at the MS
%% `number_of_messages'
%% Number of messages stored in a mail box
%% `alert_on_msg_delivery'
%% Requests an MS alert signal be invoked on message delivery
%% `language_indicator'
%% Indicates the language of an alphanumeric text. See
%% language indicator values
%% `its_reply_type'
%% The MS user's reply method to a SMS delivery message received from
%% the network. See reply type values
%% `its_session_info'
%% Session control information for Interactive Teleservice
%% `ussd_service_op'
%% Identifies the required USSD Service type when interfacing to
%% a USSD system. See USSD service operation
%% values
%%
%%
%% Priority Flag values:
%%
%% `0 - Level 0 (lowest) priority'
%% `1 - Level 1 priority'
%% `2 - Level 2 priority'
%% `3 - Level 3 (highest) priority'
%%
%% Delivery Request values:
%%
%% `0 - No delivery receipt requested'
%% `1 - Delivery receipt requested (success or failure)'
%% `2 - Delivery receipt requested (failure only)'
%%
%% Subunit values:
%%
%% `0 - Unknown (Default)'
%% `1 - MS Display'
%% `2 - Mobile Equipment'
%% `3 - Smart Card 1'
%% `4 - External Unit 1'
%%
%% Payload Type values:
%%
%% `0 - Default'
%% `1 - WCMP formatted'
%%
%% Privacy Indicator values:
%%
%% `0 - Privacy level 0 (Not Restricted - Default)'
%% `1 - Privacy level 1 (Restricted)'
%% `2 - Privacy level 2 (Confidential)'
%% `3 - Privacy level 3 (Secret)'
%%
%% MS Validity values:
%%
%% `0 - Store Indefinitely (Default)'
%% `1 - Power Down'
%% `2 - SID based registration area'
%% `3 - Display Only'
%%
%% Language Indicator values:
%%
%% `0 - Unspecified (Default)'
%% `1 - English'
%% `2 - French'
%% `3 - Spanish'
%% `4 - German'
%% `5 - Portuguese'
%%
%% Reply Type values:
%%
%% `0 - Digit'
%% `1 - Number'
%% `2 - Telephone No.'
%% `3 - Password'
%% `4 - Character Line'
%% `5 - Menu'
%% `6 - Date'
%% `7 - Time'
%% `8 - Continue'
%%
%% USSD Service Operation values:
%%
%% `0 - PSSD indication'
%% `1 - PSSR indication'
%% `2 - USSR request'
%% `3 - USSN request'
%% `16 - PSSD response'
%% `17 - PSSR response'
%% `18 - USSR confirm'
%% `19 - USSN confirm'
%%
-spec send_sms(pid(), iodata(), iodata(), iodata(), Options, OptionalParams) -> [{message_id, iodata()}] | {error, atom()}
when Options :: #{ service_type => iodata() ,
protocol_id => integer() ,
priority_flag => integer() ,
delivery_time => iodata() ,
validity => iodata() ,
reg_delivery => integer() ,
replace_flag => integer() },
OptionalParams :: #{ user_message_reference => integer() ,
source_port => integer() ,
source_addr_subunit => integer() ,
destination_port => integer() ,
dest_addr_subunit => integer() ,
sar_msg_ref_num => integer() ,
sar_total_segments => integer() ,
sar_segment_seqnum => integer() ,
more_messages_to_send => integer() ,
payload_type => integer() ,
message_payload => iodata() ,
privacy_indicator => integer() ,
callback_num => iodata() ,
callback_num_pres_ind => integer() ,
callback_num_atag => iodata() ,
source_subaddress => iodata() ,
dest_subaddress => iodata() ,
user_response_code => integer() ,
display_time => integer() ,
sms_signal => integer() ,
ms_validity => integer() ,
ms_msg_wait_facilities => integer() ,
number_of_messages => integer() ,
alert_on_msg_delivery => integer() ,
language_indicator => integer() ,
its_reply_type => integer() ,
its_session_info => integer() ,
ussd_service_op => integer() }.
send_sms(C, Sender, Destination, TmpMessage, Options, OptionalParams) ->
ServiceType = maps:get(service_type, Options, <<0>>),
ProtocolId = maps:get(protocol_id, Options, 0),
PriorityFlag = maps:get(priority_flag, Options, 0),
DeliveryTime = maps:get(delivery_time, Options, <<0>>),
Validity = maps:get(validity, Options, <<0>>),
RegDelivery = maps:get(reg_delivery, Options, 0),
ReplaceFlag = maps:get(replace_flag, Options, 0),
DataCoding = get_data_coding(TmpMessage),
SrcAddrTon = get_ton(esmpp_format:ensure_binary(Sender)),
SrcAddrNpi = get_npi(esmpp_format:ensure_binary(Sender)),
DstAddrTon = get_ton(esmpp_format:ensure_binary(Destination)),
DstAddrNpi = get_npi(esmpp_format:ensure_binary(Destination)),
Message = encode(esmpp_format:ensure_binary(TmpMessage), DataCoding),
SubmitSm = #submit_sm_pdu{
service_type = ServiceType,
src_addr_ton = SrcAddrTon,
src_addr_npi = SrcAddrNpi,
src_addr = Sender,
dst_addr_ton = DstAddrTon,
dst_addr_npi = DstAddrNpi,
dst_addr = Destination,
data_coding = DataCoding,
protocol_id = ProtocolId,
priority_flag = PriorityFlag,
delivery_time = DeliveryTime,
validity = Validity,
reg_delivery = RegDelivery,
replace_flag = ReplaceFlag,
sm_length = size(Message),
short_message = Message,
optional_params = OptionalParams
},
send_sm(C, SubmitSm, Message, DataCoding).
%% @doc Assigns callback to a mobile originating message or delivery receipt
-spec callback(Type, atom(), atom()) -> ok | {error, invalid_type}
when Type :: message | delivery_receipt.
callback(message, Module, Function) ->
gen_server:call(callback_mo, Module, Function);
callback(delivery_receipt, Module, Function) ->
gen_server:call(callback_dr, Module, Function);
callback(_Unknown, _Module, _Function) ->
{error, invalid_type}.
%% ----------------------------------------------------------------------------
%% internal
%% ----------------------------------------------------------------------------
%% @private
send_sm(C, Sm, Message, DataCoding) when DataCoding =:= 0, size(Message) =< 160 ->
[gen_server:call(C, {submit_sm, Sm})];
send_sm(C, Sm, Message, DataCoding) when DataCoding =:= 8, size(Message) =< 140 ->
[gen_server:call(C, {submit_sm, Sm})];
send_sm(C, Sm, Message, DataCoding) when DataCoding =:= 0 ->
Ref = random:uniform(255),
Size = size(Message),
Parts = ceil(Size / 153),
send_sm(C, Sm, Message, <<>>, Ref, 1, Parts, 153, []);
send_sm(C, Sm, Message, DataCoding) when DataCoding =:= 8 ->
Ref = random:uniform(255),
Size = size(Message),
Parts = ceil(Size / 134),
send_sm(C, Sm, Message, <<>>, Ref, 1, Parts, 134, []).
%% @private
send_sm(_C, _Sm, <<>>, _Tail, _Ref, _Part, _Parts, _Limit, Acc) ->
Acc;
send_sm(C, Sm, Str, _Tail, Ref, Part, Parts, Limit, Acc) when size(Str) > Limit ->
{BinPart, BinTail} = chop(Str, Limit),
send_sm(C, Sm, BinPart, BinTail, Ref, Part, Parts, Limit, Acc);
send_sm(C, Sm, Str, Tail, Ref, Part, Parts, Limit, Acc) ->
Message = <<5, 0, 3, Ref, Parts, Part, Str/binary>>,
Length = size(Message),
NewSm = Sm#submit_sm_pdu{
esm_class = 16#40,
sm_length = Length,
short_message = Message
},
NewAcc = Acc ++ [gen_server:call(C, {submit_sm, NewSm})],
send_sm(C, NewSm, Tail, <<>>, Ref, Part + 1, Parts, Limit, NewAcc).
%% @private
chop(Str, 153 = Limit) ->
<> = Str,
{TmpPart, TmpTail};
chop(Str, Limit) ->
<> = Str,
Utf8 = unicode:characters_to_binary(TmpPart, utf16),
analyze_unicode(Utf8, Str, Limit, TmpPart, TmpTail).
%% @private
analyze_unicode({incomplete, _U1, _U2}, Str, Limit, _TmpPart, _TmpTail) ->
chop(Str, Limit - 2);
analyze_unicode({error, _U1, _U2}, Str, Limit, _TmpPart, _TmpTail) ->
chop(Str, Limit - 2);
analyze_unicode(_Ok, _Str, _Limit, TmpPart, TmpTail) ->
{TmpPart, TmpTail}.
%% @private
get_binding(transmitter) -> {binding, ?BIND_TRANSMITTER};
get_binding(transceiver) -> {binding, ?BIND_TRANSCEIVER};
get_binding(receiver) -> {binding, ?BIND_RECEIVER}.
%% @private
get_ton(Number) ->
get_ton(Number, get_address_type(Number)).
get_ton(Number, digit) when size(Number) < 3 orelse size(Number) > 15 -> 0;
get_ton(Number, digit) when size(Number) > 2 andalso size(Number) < 9 -> 3;
get_ton(Number, digit) when size(Number) > 8 andalso size(Number) < 16 -> 1;
get_ton(_Number, alpha) -> 5.
%% @private
get_npi(Number) ->
get_npi(Number, get_address_type(Number)).
get_npi(Number, digit) when size(Number) < 3 orelse size(Number) > 15 -> 0;
get_npi(Number, digit) when size(Number) > 2 andalso size(Number) < 9 -> 0;
get_npi(Number, digit) when size(Number) > 8 andalso size(Number) < 16 -> 1;
get_npi(_Number, alpha) -> 0.
%% @private
get_address_type(Number) ->
get_address_type([N || <> <= Number], digit).
get_address_type([], digit) -> digit;
get_address_type([Char | Rest], _Any) ->
case binary:decode_unsigned(Char) of
Val when Val =:= 43 orelse Val > 47 andalso Val < 58 ->
get_address_type(Rest, digit);
_Val ->
alpha
end.
%% @private
get_data_coding(Message) ->
BinMessage = esmpp_format:ensure_binary(Message),
is_basic_latin([N || <> <= BinMessage]).
%% @private
is_basic_latin([]) -> 0;
is_basic_latin([Char | Rest]) ->
case binary:decode_unsigned(Char) of
Val when Val < 128, Val =/= 96 ->
is_basic_latin(Rest);
_Val ->
8
end.
%% @private
encode(Message, 0) ->
gsm0338:from_utf8(Message);
encode(Message, 8) ->
unicode:characters_to_binary(Message, utf8, utf16).
%% @private
ceil(X) when X < 0 ->
trunc(X);
ceil(X) ->
T = trunc(X),
case X - T == 0 of
true -> T;
false -> T + 1
end.