%% @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
%%
`reconnect'
%%
Reconnect time for lazy initialization. This indicates the number of milliseconds %% to sleep before attempting to connect to the SMSC. This accepts a module and function %% atom tuple for external sources.
%%
`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 , reconnect => Reconnect , 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, Reconnect :: integer() | {atom(), atom()}, 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.