# Ruby implementation of the MGP protocol used to interface # Enterprise Messaging Server (EMG). # # $Change: 10414 $ # Author:: Infoflex Connect (mailto:support@infoflexconnect.se) # Copyright:: Copyright (c) 2007 Infoflex Connect AB, Sweden require 'socket' require 'openssl' require 'pp' module EMG # This class represents a message class SMS attr_accessor :options def initialize(options = nil) @options = {} unless options.nil? options = [ options ] unless options.instance_of?(Array) options.each {|h| h.each {|k,v| @options[k] = v } } end end # Shortcut to get message id from option list def messageid @options[MGP::OPTION_ID] end end # This class is the actual protocol implementation class MGP attr_reader :host attr_reader :port attr_reader :admin attr_reader :socket attr_reader :credits attr_reader :systemtype attr_reader :charcode attr_reader :name attr_reader :clientconfig attr_reader :remoteip attr_reader :perms # Operations OP_LOGON = (1) OP_LOGON_RESP = (101) OP_SENDMESSAGE = (2) OP_SENDMESSAGE_RESP = (102) OP_LOGOFF = (3) OP_LOGOFF_RESP = (103) OP_BEGINBATCH = (4) OP_BEGINBATCH_RESP = (104) OP_ENDBATCH = (5) OP_ENDBATCH_RESP = (105) OP_GETCONNECTORINFO = (6) OP_GETCONNECTORINFO_RESP = (106) OP_REFRESH = (7) OP_REFRESH_RESP = (107) OP_GETCONNECTORQUEUE = (8) OP_GETCONNECTORQUEUE_RESP = (108) OP_GETORPHANS = (9) OP_GETORPHANS_RESP = (109) OP_QUERYMESSAGE = (10) OP_QUERYMESSAGE_RESP = (110) OP_DELETEMESSAGE = (11) OP_DELETEMESSAGE_RESP = (111) OP_MODIFYMESSAGE = (12) OP_MODIFYMESSAGE_RESP = (112) OP_KEEPALIVE = (13) OP_KEEPALIVE_RESP = (113) OP_GETROUTELOG = (14) OP_GETROUTELOG_RESP = (114) OP_PBCREATE = (20) OP_PBCREATE_RESP = (120) OP_PBDELETE = (21) OP_PBDELETE_RESP = (121) OP_PBADD = (22) OP_PBADD_RESP = (122) OP_PBUPDATE = (23) OP_PBUPDATE_RESP = (123) OP_PBREMOVE = (24) OP_PBREMOVE_RESP = (124) OP_PBLIST = (25) OP_PBLIST_RESP = (125) OP_REQUESTMESSAGE = (26) OP_REQUESTMESSAGE_RESP = (126) OP_CONNECTORHOLD = (27) OP_CONNECTORHOLD_RESP = (127) OP_DBREQUEST = (28) OP_DBREQUEST_RESP = (128) OP_GETFIRSTMESSAGE = (29) OP_GETFIRSTMESSAGE_RESP = (129) OP_CONFIRMMESSAGE = (30) OP_CONFIRMMESSAGE_RESP = (130) OP_GETDBINFO = (31) OP_GETDBINFO_RESP = (131) OP_RELOAD = (32) OP_RELOAD_RESP = (132) # Options OPTION_ID = 0x0001 OPTION_SOURCEADDR = 0x0002 OPTION_SOURCEADDRTON = 0x0003 OPTION_SOURCEADDRNPI = 0x0004 OPTION_SOURCESUBADDRESS = 0x0005 OPTION_SOURCEPORT = 0x0006 OPTION_SOURCEADDRSUBUNIT = 0x0007 OPTION_DESTADDR = 0x0008 OPTION_DESTADDRTON = 0x0009 OPTION_DESTADDRNPI = 0x000a OPTION_DESTSUBADDRESS = 0x000b OPTION_DESTPORT = 0x000c OPTION_DESTADDRSUBUNIT = 0x000d OPTION_UDH = 0x000e OPTION_UDHLEN = 0x000f OPTION_MESSAGE = 0x0010 OPTION_MESSAGELEN = 0x0011 OPTION_VP = 0x0012 OPTION_DLR = 0x0013 OPTION_DELTIME = 0x0014 OPTION_SCTS = 0x0015 OPTION_USERNAME = 0x0016 OPTION_PASSWORD = 0x0017 OPTION_NEWPASSWORD = 0x0018 OPTION_MSGTYPE = 0x0019 OPTION_MSGSUBTYPE = 0x001a OPTION_MSGCLASS = 0x001b OPTION_CHARCODE = 0x001c OPTION_AUTHCODE = 0x001d OPTION_REPLYPATH = 0x001f OPTION_PRIORITY = 0x0020 OPTION_TARIFFCLASS = 0x0021 OPTION_REMOTEIP = 0x0022 OPTION_SYSTEMTYPE = 0x0023 OPTION_SMSCOP = 0x0024 OPTION_ROUTE = 0x0026 OPTION_ROUTEDLR = 0x0027 OPTION_RETCODE = 0x0028 OPTION_SERVICETYPE = 0x0029 OPTION_MESSAGEMODE = 0x002a OPTION_PROTOCOLID = 0x002b OPTION_REPLACEIFPRESENTFLAG = 0x002c OPTION_USERMESSAGEREFERENCE = 0x002d OPTION_USERRESPONSECODE = 0x002e OPTION_PRIVACYINDICATOR = 0x002f OPTION_CALLBACKNUM = 0x0030 OPTION_LANGUAGEINDICATOR = 0x0031 OPTION_TTSSESSIONINFO = 0x0032 OPTION_NETWORKERRORCODE = 0x0033 OPTION_MESSAGESTATE = 0x0034 OPTION_RECEIPTEDMESSAGEID = 0x0035 OPTION_LONGMESSAGE = 0x0036 OPTION_LONGMODE = 0x0037 OPTION_REGISTEREDDELIVERY = 0x0038 OPTION_CANCELMODE = 0x0039 OPTION_INTERFACEVERSION = 0x003a OPTION_CONNECTOR = 0x003b OPTION_OUTCONNECTOR = 0x003c OPTION_STATUS = 0x003d OPTION_SOURCENETWORKTYPE = 0x003e OPTION_DESTNETWORKTYPE = 0x003f OPTION_SMSCID = 0x0040 OPTION_OPSENTEXPIRES = 0x0041 OPTION_DLREXPIRES = 0x0042 OPTION_ROUTE2 = 0x0043 OPTION_TCPSOURCEPORT = 0x0044 OPTION_MAPPING = 0x0045 OPTION_DLRADDRESS = 0x0046 OPTION_DLRPID = 0x0047 OPTION_DOMAIN = 0x0048 OPTION_CONCATSMSREF = 0x0049 OPTION_CONCATSMSSEQ = 0x004a OPTION_CONCATSMSMAX = 0x004b OPTION_REQUIREPREFIX = 0x004c OPTION_AUTHTON = 0x004d OPTION_AUTHNPI = 0x004e OPTION_BILLINGID = 0x004f OPTION_SINGLESHOT = 0x0050 OPTION_DLRID = 0x0051 OPTION_STATE = 0x0053 OPTION_PROTOCOL = 0x0054 OPTION_INSTANCES = 0x0055 OPTION_QUEUESIZE = 0x0056 OPTION_TYPE = 0x0057 OPTION_QSTATS1 = 0x0058 OPTION_QSTATS5 = 0x0059 OPTION_QSTATS15 = 0x005a OPTION_INSTANCE = 0x005b OPTION_STARTSECS = 0x005d OPTION_STARTMSECS = 0x005e OPTION_ENDSECS = 0x005f OPTION_ENDMSECS = 0x0060 OPTION_NOTE = 0x0061 OPTION_CLIENTCONFIG = 0x0062 OPTION_COMPANY = 0x0063 OPTION_NAME = 0x0064 OPTION_PBNAME = 0x0065 OPTION_PBTYPE = 0x0066 OPTION_REASON = 0x0067 OPTION_PBID = 0x0068 OPTION_ISADMIN = 0x0069 OPTION_UDHI = 0x006a OPTION_REPLACEPID = 0x006b OPTION_LRADDR = 0x006c OPTION_LRPID = 0x006d OPTION_HPLMNADDR = 0x006e OPTION_SUBJECT = 0x006f OPTION_OTOA = 0x0070 OPTION_DCS = 0x0071 OPTION_MAXMESSAGELENGTH = 0x0072 OPTION_HEADER = 0x0073 OPTION_KEYWORD = 0x0074 OPTION_REMOVEPREFIX = 0x0075 OPTION_QPRIORITY = 0x0076 OPTION_XUSERNAME = 0x0077 OPTION_MAXINSTANCES = 0x0078 OPTION_AVGINSTANCES1 = 0x0079 OPTION_STATIC = 0x007a OPTION_MODE = 0x007b OPTION_DBSQL = 0x007c OPTION_DBNAME = 0x007d OPTION_DBDATA = 0x007e OPTION_CREDITS = 0x007f OPTION_SOURCEADDRTYPE = 0x0080 OPTION_DESTADDRTYPE = 0x0081 OPTION_REQUIREPREFIX_SOURCEADDR = 0x0082 OPTION_REMOVEPREFIX_SOURCEADDR = 0x0083 OPTION_PDUSEQ = 0x0084 OPTION_PDUSEQMAX = 0x0085 OPTION_ORIGSOURCEADDR = 0x0086 OPTION_ORIGDESTADDR = 0x0087 OPTION_SERVICEDESCRIPTION = 0x0088 OPTION_SENDERTS = 0x0089 OPTION_IMSI = 0x008a OPTION_VLR = 0x008b OPTION_ORIGID = 0x008c OPTION_SERVICEID = 0x008d OPTION_ACLENTRYWHOID = 0x008e OPTION_ACLENTRYWHEREID = 0x008f OPTION_PLUGINARG = 0x0090 OPTION_MMSDESTADDR = 0x0091 OPTION_MSISDN = 0x0092 OPTION_XPRIORITY = 0x0093 OPTION_TCPSOURCEIP = 0x0094 OPTION_SENDERADDRESS = 0x0095 OPTION_ORIGIN = 0x0096 OPTION_SUBMITTS = 0x0097 OPTION_DONETS = 0x0098 OPTION_MESSAGEID = 0x0099 OPTION_INREPLYTO = 0x009a OPTION_REFERENCES = 0x009b OPTION_QUOTEDREPLY = 0x009c OPTION_QUOTEDREPLY_SEPARATOR = 0x009d OPTION_SERVICETYPEIN = 0x009e OPTION_SOURCEFULLNAME = 0x009f OPTION_CONTENTTYPE = 0x00a0 OPTION_CONTENTLOCATION = 0x00a1 OPTION_DESTFULLNAME = 0x00a2 OPTION_DLRBUF = 0x00a3 OPTION_ARCORMOD = 0x00a4 OPTION_ARCORFUNC = 0x00a5 OPTION_ARCORUNIT = 0x00a6 OPTION_SCAADDR = 0x00a7 OPTION_SCAADDRTON = 0x00a8 OPTION_SCAADDRNPI = 0x00a9 OPTION_REASONTEXT = 0x00aa OPTION_ITSSESSIONINFO = 0x00ab OPTION_LASTDLRSECS = 0x00ac OPTION_LASTDLRMSECS = 0x00ad OPTION_SMTP_RET = 0x00ae OPTION_INSTANCES_INUSE = 0x00af OPTION_DESTNETWORK = 0x00b0 OPTION_TARIFFNAME = 0x00b1 OPTION_SMPPOPTION = 0x00b2 OPTION_BUFFEREDSTATUS = 0x00b3 OPTION_LAST = 0x00b3 # Phone book types PBTYPE_USER = 0 PBTYPE_SYSTEM = 1 # Return codes ERR_OK = 0 ERR_UNKNOWN = 1 ERR_SYNTAX = 2 ERR_LOGIN = 3 ERR_BOUND = 4 ERR_INVALARG = 5 ERR_INVALCMD = 6 ERR_INVALMSGID = 7 ERR_INVALDESTADDR = 8 ERR_INVALSOURCEADDR = 9 ERR_NOACCESS = 10 ERR_MESSAGE = 11 ERR_INVALRESP = 12 ERR_COMM = 13 ERR_DB = 14 ERR_UDH = 15 ERR_CREDITS = 16 ERR_BUSY = 17 ERR_TOOLONG = 18 private # Misc A_STX = 2 A_ETX = 3 def do_send_operation(op, params = nil) buf = "%c%03d\n" % [ A_STX, op ] params.each {|key, value| value = value.unpack('H*') if key == OPTION_MESSAGE buf += "#{key}:#{value}\n" } unless params.nil? buf += "%c" % A_ETX @socket.write buf begin c = @socket.read(1) return [{ OPTION_RETCODE => ERR_OK }] if c.nil? c = c.unpack("C").first end until c == A_STX c = @socket.read(4) op = c[0..2].to_i c = @socket.read(1).unpack("C1").first ret = [] while c != A_ETX key = "#{c - 48}" + @socket.read(3) key = key.to_i value = @socket.gets.strip vi = value.to_i value = vi if value == vi.to_s value = [value].pack('H*') if key == OPTION_MESSAGE ret.push({ key => value }) c = @socket.read(1) break if c.nil? c = c.unpack("C1").first end ret end def openssl @sslctx = OpenSSL::SSL::SSLContext.new if @sslctx.nil? sslsocket = OpenSSL::SSL::SSLSocket.new(@socket, @sslctx) unless sslsocket.nil? sslsocket.sync_close = true sslsocket.connect end @socket = sslsocket end def reconnect @socket = TCPSocket.new(@host, @port) openssl if @usessl !@socket.nil? end def closing_op(op) return ![OP_LOGOFF, OP_REFRESH].index(op).nil? end def send_operation(op, params = nil) reconnect if @socket.nil? begin result = do_send_operation(op, params) rescue Errno::ECONNRESET => err raise err unless closing_op(op) ensure self.close if closing_op(op) end result end def extract(result, key) ret = nil return nil if result.nil? result.delete_if {|h| (hk,hv) = h.detect {|k,v| k == key} ret ||= hv !hv.nil? } ret end def find(result, key) result.each {|h| h.each {|k,v| return v if k == key } } unless result.nil? nil end def extract_sms(result) list = sms = nil result.each {|h| h.each {|k,v| (list ||= []).push(sms = SMS.new([h])) if k == OPTION_ID sms.options[k] = v unless sms.nil? } } list end def extract_info(result, tag) list = obj = child = target = nil result.each {|h| h.each {|k,v| case k when tag (list ||= {})[v] = target = obj = {} child = nil when OPTION_INSTANCE (obj[OPTION_INSTANCE] ||= {})[v] = target = child = {} else target[k] = v unless target.nil? end } } list end def do_get_list(op, params = {}) maxsize = params[:maxsize] username = params[:username] (args ||= {})[OPTION_QUEUESIZE] = maxsize unless maxsize.nil? (args ||= {})[OPTION_USERNAME] = username unless username.nil? result = send_operation(op, args) [extract(result, OPTION_RETCODE), extract_sms(result)] end def do_get_info(op, key, value, separator) result = send_operation(op, ({ key=> value } unless value.nil?)) retcode = find(result, OPTION_RETCODE) || ERR_OK [retcode, extract_info(result, separator)] end public def initialize @admin = false end # Connect to EMG server on specified host and port # The EMG server must have an incoming MGP connector listening # on the IP address/hostname and port specified def connect(host, port, options = {}) self.close @host = host @port = port.to_i raise "Invalid port number #{port}" if @port == 0 @usessl = options[:ssl] ret = reconnect if block_given? begin if options[:username] && options[:password] self.logon(options[:username], options[:password]) end yield(self) ensure self.logoff unless @socket.nil? end end ret end # Close connection to server def close return if @socket.nil? @socket.close @socket = nil end # Test if we are a user with "admin" privileges def admin? @admin = false if @socket.nil? @admin end # Log on to the server using the specified username and password # The connector must have a USERS file with the user credentials # or reference a databaseprofile (USERDB) with the user specified def logon(username, password) params = { OPTION_USERNAME => username, OPTION_PASSWORD => password } result = send_operation(OP_LOGON, params) @admin = (find(result, OPTION_ISADMIN) || 0) & 1 == 1 @systemtype = find(result, OPTION_SYSTEMTYPE) @charcode = find(result, OPTION_CHARCODE) @name = find(result, OPTION_NAME) @remoteip = find(result, OPTION_REMOTEIP) @perms = find(result, OPTION_USERRESPONSECODE) @credits = find(result, OPTION_CREDITS) if block_given? begin yield(self) ensure self.logoff unless @socket.nil? end end find(result, OPTION_RETCODE) end # Log out from EMG server def logoff result = send_operation(OP_LOGOFF) find(result, OPTION_RETCODE) end # Send the message specified # Returns the result code and (possibly) the message id (if successful) def send_message(sms) result = send_operation(OP_SENDMESSAGE, sms.options) @credits = find(result, OPTION_CREDITS) return [find(result, OPTION_RETCODE), find(result, OPTION_ID)] end def query_message(id) result = send_operation(OP_QUERYMESSAGE, { OPTION_ID => id }) retcode = extract(result, OPTION_RETCODE) [retcode, SMS.new(result)] end # Delete the message with the specified message id def delete_message(id) result = send_operation(OP_DELETEMESSAGE, { OPTION_ID => id }) find(result, OPTION_RETCODE) end def get_first_message result = send_operation(OP_GETFIRSTMESSAGE) retcode = extract(result, OPTION_RETCODE) queuesize = extract(result, (OPTION_QUEUESIZE)) || 0 [retcode, queuesize, result.empty? ? nil : SMS.new(result)] end def confirm_message(id, ret) result = send_operation(OP_CONFIRMMESSAGE, { OPTION_ID => id, OPTION_RETCODE => ret }) find(result, OPTION_RETCODE) end def get_connector_queue(connector, maxsize = nil) args = { OPTION_CONNECTOR => connector } args[OPTION_QUEUESIZE] = maxsize unless maxsize.nil? result = send_operation(OP_GETCONNECTORQUEUE, args) [extract(result, OPTION_RETCODE), extract_sms(result)] end def get_routelog(p = {}) do_get_list(OP_GETROUTELOG, p) end def get_orphans(p = {}) do_get_list(OP_GETORPHANS, p) end # Handle the "hold" status of the connector identified by "name" # The "status" should either be "1" to "hold" the connector or "0" # to "un-hold" a connector previously put on hold. def connector_hold(name, status) result = send_operation(OP_CONNECTORHOLD, { OPTION_CONNECTOR => name, OPTION_STATUS => (status ? 1 : 0) }) [find(result, OPTION_RETCODE), find(result, OPTION_STATUS)] end def get_connector_info(connector = nil) do_get_info(OP_GETCONNECTORINFO, OPTION_CONNECTOR, connector, OPTION_CONNECTOR) end def get_db_info(db = nil) do_get_info(OP_GETDBINFO, OPTION_DBNAME, db, OPTION_NAME) end def pb_list(pbid = nil) result = send_operation(OP_PBLIST, ({ OPTION_PBID => pbid } unless pbid.nil?)) retcode = extract(result, OPTION_RETCODE) [retcode, extract_info(result, OPTION_PBID)] end def pb_create(pbtype, name) result = send_operation(OP_PBCREATE, { OPTION_PBTYPE => pbtype, OPTION_PBNAME => name }) find(result, OPTION_RETCODE) end def pb_delete(pbid) result = send_operation(OP_PBDELETE, { OPTION_PBID => pbid }) find(result, OPTION_RETCODE) end def pb_add(pbid, company, name, destaddr) result = send_operation(OP_PBADD, { OPTION_PBID => pbid, OPTION_COMPANY => company, OPTION_NAME => name, OPTION_DESTADDR => destaddr }) find(result, OPTION_RETCODE) end def pb_update(id, params = {}) args = { OPTION_PBID => id } params.each {|k,v| case k when :company args[OPTION_COMPANY] = v when :name args[OPTION_NAME] = v when :destaddr args[OPTION_DESTADDR] = v else raise "Unknown PB entry parameter `#{k}'" end } result = send_operation(OP_PBUPDATE, args) find(result, OPTION_RETCODE) end def pb_remove(entryid) result = send_operation(OP_PBREMOVE, { OPTION_PBID => entryid }) find(result, OPTION_RETCODE) end # Reload or refresh the EMG server # When closeall parameter is true all connections will be closed # and the current MGP connection will also be lost. This corresponds # to running "emgd -refresh" from the command-line. # When closeall parameter is false connections will be kept. This # corresponds to running "emgd -reload" from the command-line. def reload(closeall = false) result = send_operation(closeall ? OP_REFRESH : OP_RELOAD) find(result, OPTION_RETCODE) end end end