ruby-smpp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ # PDUs are the protcol base units in SMPP
2
+ module Smpp::Pdu
3
+ class Base
4
+ # Error constants
5
+ ESME_ROK = 0x00000000 # OK!
6
+ ESME_RINVMSGLEN = 0x00000001 # Message Length is invalid
7
+ ESME_RINVCMDLEN = 0x00000002 # Command Length is invalid
8
+ ESME_RINVCMDID = 0x00000003 # Invalid Command ID
9
+ ESME_RINVBNDSTS = 0x00000004 # Incorrect BIND Status for given com-
10
+ ESME_RALYBND = 0x00000005 # ESME Already in Bound State
11
+ ESME_RINVPRTFLG = 0x00000006 # Invalid Priority Flag
12
+ ESME_RINVREGDLVFLG = 0x00000007 # Invalid Registered Delivery Flag
13
+ ESME_RSYSERR = 0x00000008 # System Error
14
+ ESME_RINVSRCADR = 0x0000000A # Invalid Source Address
15
+ ESME_RINVDSTADR = 0x0000000B # Invalid Dest Addr
16
+ ESME_RINVMSGID = 0x0000000C # Message ID is invalid
17
+ ESME_RBINDFAIL = 0x0000000D # Bind Failed
18
+ ESME_RINVPASWD = 0x0000000E # Invalid Password
19
+ ESME_RINVSYSID = 0x0000000F # Invalid System ID
20
+ ESME_RCANCELFAIL = 0x00000011 # Cancel SM Failed
21
+ ESME_RREPLACEFAIL = 0x00000013 # Replace SM Failed
22
+ ESME_RMSGQFUL = 0x00000014 # Message Queue Full
23
+ ESME_RINVSERTYP = 0x00000015 # Invalid Service Type
24
+ ESME_RINVNUMDESTS = 0x00000033 # Invalid number of destinations
25
+ ESME_RINVDLNAME = 0x00000034 # Invalid Distribution List name
26
+ ESME_RINVDESTFLAG = 0x00000040 # Destination flag is invalid
27
+ ESME_RINVSUBREP = 0x00000042 # Invalid ‘submit with replace’ request
28
+ ESME_RINVESMCLASS = 0x00000043 # Invalid esm_class field data
29
+ ESME_RCNTSUBDL = 0x00000044 # Cannot Submit to Distribution List
30
+ ESME_RSUBMITFAIL = 0x00000045 # submit_sm or submit_multi failed
31
+ ESME_RINVSRCTON = 0x00000048 # Invalid Source address TON
32
+ ESME_RINVSRCNPI = 0x00000049 # Invalid Source address NPI
33
+ ESME_RINVDSTTON = 0x00000050 # Invalid Destination address TON
34
+ ESME_RINVDSTNPI = 0x00000051 # Invalid Destination address NPI
35
+ ESME_RINVSYSTYP = 0x00000053 # Invalid system_type field
36
+ ESME_RINVREPFLAG = 0x00000054 # Invalid replace_if_present flag
37
+ ESME_RINVNUMMSGS = 0x00000055 # Invalid number of messages
38
+ ESME_RTHROTTLED = 0x00000058 # Throttling error (ESME has exceeded allowed message limits)
39
+
40
+ # PDU types
41
+ GENERIC_NACK = 0X80000000
42
+ BIND_RECEIVER = 0X00000001
43
+ BIND_RECEIVER_RESP = 0X80000001
44
+ BIND_TRANSMITTER = 0X00000002
45
+ BIND_TRANSMITTER_RESP = 0X80000002
46
+ BIND_TRANSCEIVER = 0X00000009
47
+ BIND_TRANSCEIVER_RESP = 0X80000009
48
+ QUERY_SM = 0X00000003
49
+ QUERY_SM_RESP = 0X80000003
50
+ SUBMIT_SM = 0X00000004
51
+ SUBMIT_SM_RESP = 0X80000004
52
+ DELIVER_SM = 0X00000005
53
+ DELIVER_SM_RESP = 0X80000005
54
+ UNBIND = 0X00000006
55
+ UNBIND_RESP = 0X80000006
56
+ REPLACE_SM = 0X00000007
57
+ REPLACE_SM_RESP = 0X80000007
58
+ CANCEL_SM = 0X00000008
59
+ CANCEL_SM_RESP = 0X80000008
60
+ ENQUIRE_LINK = 0X00000015
61
+ ENQUIRE_LINK_RESP = 0X80000015
62
+
63
+ # PDU sequence number.
64
+ @@seq = [Time.now.to_i]
65
+
66
+ # Add monitor to sequence counter for thread safety
67
+ @@seq.extend(MonitorMixin)
68
+
69
+ attr_reader :command_id, :command_status, :sequence_number, :body, :data
70
+
71
+ def initialize(command_id, command_status, seq, body='')
72
+ length = 16 + body.length
73
+ @command_id = command_id
74
+ @command_status = command_status
75
+ @body = body
76
+ @sequence_number = seq
77
+ @data = fixed_int(length) + fixed_int(command_id) + fixed_int(command_status) + fixed_int(seq) + body
78
+ end
79
+
80
+ def logger
81
+ Smpp::Base.logger
82
+ end
83
+
84
+ def to_human
85
+ # convert header (4 bytes) to array of 4-byte ints
86
+ a = @data.to_s.unpack('N4')
87
+ sprintf("(%22s) len=%3d cmd=%8s status=%1d seq=%03d (%s)", self.class.to_s[11..-1], a[0], a[1].to_s(16), a[2], a[3], @body)
88
+ end
89
+
90
+ # return int as binary string of 4 octets
91
+ def fixed_int(value)
92
+ sprintf("%c%c%c%c", value >> 24, value >> 16, value >> 8, value & 0xff)
93
+ end
94
+
95
+ def next_sequence_number
96
+ Base.next_sequence_number
97
+ end
98
+
99
+ def Base.next_sequence_number
100
+ @@seq.synchronize do
101
+ (@@seq[0] += 1) % 512
102
+ end
103
+ end
104
+
105
+ # PDU factory method for common client PDUs (used to create PDUs from wire data)
106
+ def Base.create(data)
107
+ header = data[0..15]
108
+ if !header
109
+ return nil
110
+ end
111
+ len, cmd, status, seq = header.unpack('N4')
112
+ body = data[16..-1]
113
+ case cmd
114
+ when ENQUIRE_LINK:
115
+ EnquireLink.new(seq)
116
+ when ENQUIRE_LINK_RESP:
117
+ EnquireLinkResponse.new(seq)
118
+ when GENERIC_NACK:
119
+ GenericNack.new(seq, status, body)
120
+ when UNBIND:
121
+ Unbind.new(seq)
122
+ when UNBIND_RESP:
123
+ UnbindResponse.new(seq, status)
124
+ when BIND_TRANSMITTER_RESP:
125
+ BindTransmitterResponse.new(seq, status, body) # could be opt'l params too
126
+ when BIND_RECEIVER_RESP:
127
+ BindReceiverResponse.new(seq, status, body)
128
+ when BIND_TRANSCEIVER_RESP:
129
+ BindTransceiverResponse.new(seq, status, body)
130
+ when SUBMIT_SM_RESP:
131
+ SubmitSmResponse.new(seq, status, body)
132
+ when DELIVER_SM:
133
+ DeliverSm.new(seq, status, body)
134
+ else
135
+ Smpp::Base.logger.error "Unknown PDU: 0x#{cmd.to_s(16)}"
136
+ return nil
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,6 @@
1
+ class Smpp::Pdu::BindTransceiver < Smpp::Pdu::Base
2
+ def initialize(system_id, password, addr_ton, addr_npi, address_range)
3
+ body = sprintf("%s\0%s\0\0%c%c%c%s\0", system_id, password, 0x34, addr_ton, addr_npi, address_range)
4
+ super(BIND_TRANSCEIVER, 0, next_sequence_number, body)
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ class Smpp::Pdu::BindTransceiverResponse < Smpp::Pdu::Base
2
+ attr_accessor :system_id
3
+ def initialize(seq, status, system_id)
4
+ super(BIND_TRANSCEIVER_RESP, status, seq, system_id) # pass in system_id as body for simple debugging
5
+ @system_id = system_id
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ # Received for MO message or delivery notification
2
+ class Smpp::Pdu::DeliverSm < Smpp::Pdu::Base
3
+ attr_reader :source_addr, :destination_addr, :short_message, :source_addr, :esm_class, :msg_reference, :stat
4
+ def initialize(seq, status, body)
5
+ # brutally unpack it
6
+ service_type,
7
+ source_addr_ton,
8
+ source_addr_npi,
9
+ @source_addr,
10
+ dest_addr_ton,
11
+ dest_addr_npi,
12
+ @destination_addr,
13
+ @esm_class,
14
+ protocol_id,
15
+ priority_flag,
16
+ schedule_delivery_time,
17
+ validity_period,
18
+ registered_delivery,
19
+ replace_if_present_flag,
20
+ data_coding,
21
+ sm_default_msg_id,
22
+ sm_length,
23
+ @short_message = body.unpack('Z*CCZ*CCZ*CCCZ*Z*CCCCCa*')
24
+ logger.debug "DeliverSM with source_addr=#{@source_addr}, destination_addr=#{@destination_addr}"
25
+
26
+ # Note: if the SM is a delivery receipt (esm_class=4) then the short_message _may_ be in this format:
27
+ # "id:Smsc2013 sub:1 dlvrd:1 submit date:0610171515 done date:0610171515 stat:0 err:0 text:blah"
28
+ # or this format:
29
+ # "4790000000SMSAlert^id:1054BC63 sub:0 dlvrd:1 submit date:0610231217 done date:0610231217 stat:DELIVRD err: text:"
30
+ # (according to the SMPP spec, the format is vendor specific)
31
+ # For example, Tele2 (Norway):
32
+ # "<msisdn><shortcode>?id:10ea34755d3d4f7a20900cdb3349e549 sub:001 dlvrd:001 submit date:0611011228 done date:0611011230 stat:DELIVRD err:000 Text:abc'!10ea34755d3d4f7a20900cdb3349e549"
33
+ if @esm_class == 4
34
+ @msg_reference = @short_message.scanf('id:%s').to_s
35
+ # @stat must be parsed according to the SMSC vendor's specifications (see comment above)
36
+ @stat = 0
37
+ end
38
+ super(DELIVER_SM, status, seq, body)
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ class Smpp::Pdu::DeliverSmResponse < Smpp::Pdu::Base
2
+ def initialize(seq, status=ESME_ROK)
3
+ super(DELIVER_SM_RESP, status, seq, "\000") # body must be NULL..!
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Smpp::Pdu::EnquireLink < Smpp::Pdu::Base
2
+ def initialize(seq = next_sequence_number)
3
+ super(ENQUIRE_LINK, 0, seq)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Smpp::Pdu::EnquireLinkResponse < Smpp::Pdu::Base
2
+ def initialize(seq)
3
+ super(ENQUIRE_LINK_RESP, ESME_ROK, seq)
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ # signals invalid message header
2
+ class Smpp::Pdu::GenericNack < Smpp::Pdu::Base
3
+ attr_accessor :error_code
4
+ def initialize(seq, error_code, original_sequence_code)
5
+ super(GENERIC_NACK, error_code, seq, "Error: #{error_code} Problem sequence: #{original_sequence_code.blank? ? 'unknown' : original_sequence_code }")
6
+ @error_code = error_code
7
+ end
8
+ end
@@ -0,0 +1,44 @@
1
+ # Sending an MT message
2
+ class Smpp::Pdu::SubmitSm < Smpp::Pdu::Base
3
+
4
+ # Note: short_message (the SMS body) must be in iso-8859-1 format
5
+ def initialize(source_addr, destination_addr, short_message, options={})
6
+ options.merge!(
7
+ :esm_class => 0, # default smsc mode
8
+ :dcs => 3 # iso-8859-1
9
+ ) { |key, old_val, new_val| old_val }
10
+
11
+ @msg_body = short_message
12
+
13
+ udh = options[:udh]
14
+ service_type = ''
15
+ source_addr_ton = 0 # network specific
16
+ source_addr_npi = 1 # unknown
17
+ dest_addr_ton = 1 # international
18
+ dest_addr_npi = 1 # unknown
19
+ esm_class = options[:esm_class]
20
+ protocol_id = 0
21
+ priority_flag = 1
22
+ schedule_delivery_time = ''
23
+ validity_period = ''
24
+ registered_delivery = 1 # we want delivery notifications
25
+ replace_if_present_flag = 0
26
+ data_coding = options[:dcs]
27
+ sm_default_msg_id = 0
28
+ payload = udh ? udh + short_message : (short_message + "\0")
29
+ sm_length = payload.length
30
+
31
+ # craft the string/byte buffer
32
+ pdu_body = sprintf("%s\0%c%c%s\0%c%c%s\0%c%c%c%s\0%s\0%c%c%c%c%c%s", service_type, source_addr_ton, source_addr_npi, source_addr,
33
+ dest_addr_ton, dest_addr_npi, destination_addr, esm_class, protocol_id, priority_flag, schedule_delivery_time, validity_period,
34
+ registered_delivery, replace_if_present_flag, data_coding, sm_default_msg_id, sm_length, payload)
35
+ super(SUBMIT_SM, 0, next_sequence_number, pdu_body)
36
+ end
37
+
38
+ # some special formatting is needed for SubmitSm PDUs to show the actual message content
39
+ def to_human
40
+ # convert header (4 bytes) to array of 4-byte ints
41
+ a = @data.to_s.unpack('N4')
42
+ sprintf("(%22s) len=%3d cmd=%8s status=%1d seq=%03d (%s)", self.class.to_s[11..-1], a[0], a[1].to_s(16), a[2], a[3], @msg_body[0..30])
43
+ end
44
+ end
@@ -0,0 +1,8 @@
1
+ class Smpp::Pdu::SubmitSmResponse < Smpp::Pdu::Base
2
+ attr_accessor :message_id
3
+ def initialize(seq, status, message_id)
4
+ message_id = message_id.chomp("\000")
5
+ super(SUBMIT_SM_RESP, status, seq, message_id)
6
+ @message_id = message_id
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ class Smpp::Pdu::Unbind < Smpp::Pdu::Base
2
+ def initialize(seq=next_sequence_number)
3
+ super(UNBIND, 0, seq)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Smpp::Pdu::UnbindResponse < Smpp::Pdu::Base
2
+ def initialize(seq, status)
3
+ super(UNBIND_RESP, status, seq)
4
+ end
5
+ end
@@ -0,0 +1,97 @@
1
+ class Smpp::Transceiver < Smpp::Base
2
+
3
+ # Expects a config hash,
4
+ # a proc to invoke for incoming (MO) messages,
5
+ # a proc to invoke for delivery reports,
6
+ # and optionally a hash-like storage for pending delivery reports.
7
+ def initialize(config, mo_proc, dr_proc, pdr_storage={})
8
+ super(config)
9
+ @state = :unbound
10
+ @mo_proc = mo_proc
11
+ @dr_proc = dr_proc
12
+ @pdr_storage = pdr_storage
13
+
14
+ # Array of un-acked MT message IDs indexed by sequence number.
15
+ # As soon as we receive SubmitSmResponse we will use this to find the
16
+ # associated message ID, and then create a pending delivery report.
17
+ @ack_ids = Array.new(512)
18
+
19
+ ed = @config[:enquire_link_delay_secs] || 5
20
+ comm_inactivity_timeout = [ed - 5, 3].max
21
+ rescue Exception => ex
22
+ logger.error "Exception setting up transceiver: #{ex}"
23
+ raise
24
+ end
25
+
26
+ # Send an MT SMS message
27
+ def send_mt(message_id, source_addr, destination_addr, short_message, options={})
28
+ logger.debug "Sending MT: #{short_message}"
29
+ if @state == :bound
30
+ pdu = Pdu::SubmitSm.new(source_addr, destination_addr, short_message, options)
31
+ write_pdu pdu
32
+
33
+ # keep the message ID so we can associate the SMSC message ID with our message
34
+ # when the response arrives.
35
+ @ack_ids[pdu.sequence_number] = message_id
36
+ else
37
+ raise InvalidStateException, "Transceiver is unbound. Cannot send MT messages."
38
+ end
39
+ end
40
+
41
+ # a PDU is received
42
+ def process_pdu(pdu)
43
+ case pdu
44
+ when Pdu::DeliverSm
45
+ write_pdu(Pdu::DeliverSmResponse.new(pdu.sequence_number))
46
+ logger.debug "ESM CLASS #{pdu.esm_class}"
47
+ if pdu.esm_class != 4
48
+ # MO message; invoke MO proc
49
+ @mo_proc.call(pdu.source_addr, pdu.destination_addr, pdu.short_message)
50
+ else
51
+ # Invoke DR proc (let the owner of the block do the mapping from msg_reference to mt_id)
52
+ @dr_proc.call(pdu.msg_reference.to_s, pdu.stat)
53
+ end
54
+ when Pdu::BindTransceiverResponse
55
+ case pdu.command_status
56
+ when Pdu::Base::ESME_ROK
57
+ logger.debug "Bound OK."
58
+ @state = :bound
59
+ when Pdu::Base::ESME_RINVPASWD
60
+ logger.warn "Invalid password."
61
+ EventMachine::stop_event_loop
62
+ when Pdu::Base::ESME_RINVSYSID
63
+ logger.warn "Invalid system id."
64
+ EventMachine::stop_event_loop
65
+ else
66
+ logger.warn "Unexpected BindTransceiverResponse. Command status: #{pdu.command_status}"
67
+ EventMachine::stop_event_loop
68
+ end
69
+ when Pdu::SubmitSmResponse
70
+ mt_message_id = @ack_ids[pdu.sequence_number]
71
+ if !mt_message_id
72
+ raise "Got SubmitSmResponse for unknown sequence_number: #{pdu.sequence_number}"
73
+ end
74
+ if pdu.command_status != Pdu::Base::ESME_ROK
75
+ logger.error "Error status in SubmitSmResponse: #{pdu.command_status}"
76
+ else
77
+ logger.info "Got OK SubmitSmResponse (#{pdu.message_id} -> #{mt_message_id})"
78
+ end
79
+ # Now we got the SMSC message id; create pending delivery report
80
+ @pdr_storage[pdu.message_id] = mt_message_id
81
+ else
82
+ super
83
+ end
84
+ end
85
+
86
+ # Send BindTransceiverResponse PDU.
87
+ def send_bind
88
+ raise IOError, 'Receiver already bound.' unless @state == :unbound
89
+ pdu = Pdu::BindTransceiver.new(
90
+ @config[:system_id],
91
+ @config[:password],
92
+ @config[:source_ton],
93
+ @config[:source_npi],
94
+ @config[:source_address_range])
95
+ write_pdu(pdu)
96
+ end
97
+ end
@@ -0,0 +1,9 @@
1
+ module Smpp #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
data/lib/sms.rb ADDED
@@ -0,0 +1,9 @@
1
+ # Basic SMS class for sample gateway
2
+
3
+ class Sms
4
+ attr_accessor :id, :from, :to, :body
5
+
6
+ def initialize(body)
7
+ self.body = body
8
+ end
9
+ end
data/script/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/ruby-smpp.rb'}"
9
+ puts "Loading ruby-smpp gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
data/script/destroy ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)