├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.rst ├── credentials_test.py ├── requirements-dev.pip ├── requirements.pip ├── run-tests.sh ├── setup.cfg ├── setup.py ├── smpp ├── __init__.py ├── clickatell.py ├── esme.py ├── pdu.py ├── pdu_builder.py ├── pdu_inspector.py └── smsc.py ├── test ├── __init__.py ├── pdu.py ├── pdu_asserts.py ├── pdu_hex.py ├── pdu_hex_asserts.py └── test_multipart.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | coverage-html/ 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # Custom 63 | *.swp 64 | ve/ 65 | *_priv.py 66 | pep8.txt 67 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.6 4 | - 2.7 5 | - pypy 6 | install: 7 | - pip install coveralls 8 | - pip install -r requirements.pip 9 | - pip install -r requirements-dev.pip 10 | script: 11 | - ./run-tests.sh 12 | after_success: 13 | - coveralls 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Areski Belaid 2 | David Maclay 3 | Jeremy Thurgood 4 | Rudi Giesler 5 | Simon Cross 6 | Simon de Haan 7 | Simon St John-Green 8 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | v0.1.9 5 | ------ 6 | 7 | :Date: 24 March 2016 8 | :Pull requests: 12, 13, 14 9 | 10 | - Numerous PEP8 fixes. 11 | - Add tests for unpack_pdu_objects. 12 | - Allow the validity period of submit_sm PDUs to be of variable length 13 | (as required by section 4.4.1 of the SMPP v3.4 specification). 14 | - Add CHANGELOG and AUTHORS files. 15 | 16 | 17 | v0.1.8 18 | ------ 19 | 20 | :Date: 24 September 2015 21 | :Pull requests: 11 22 | 23 | - Move requirements into setup.py (they were in requirements.pip). 24 | - Add missing files to source distribution. 25 | - Fix name of license file. 26 | 27 | 28 | v0.1.7 29 | ------ 30 | 31 | :Date: 4 August 2015 32 | :Pull requests: None 33 | 34 | - Fix reading of requirements in setup.py. 35 | 36 | 37 | v0.1.6 38 | ------ 39 | 40 | :Date: 4 August 2015 41 | :Pull requests: None 42 | 43 | - Fix reading long description in setup.py. 44 | 45 | 46 | v0.1.5 47 | ------ 48 | 49 | :Date: 12 February 2015 50 | :Pull requests: 9 51 | 52 | - Fix reassembly of multipart messages. 53 | 54 | 55 | v0.1.4 56 | ------ 57 | 58 | :Date: 15 October 2014 59 | 60 | - Initial release. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Praekelt Foundation and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the Praekelt Foundation nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 22 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Source distribution files. 2 | include LICENSE 3 | include README.rst 4 | include requirements.pip requirements-dev.pip 5 | include setup.py setup.cfg 6 | 7 | exclude test/* 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python SMPP 2 | =========== 3 | 4 | An SMPP version 3.4 library written in Python, suitable for use in Twisted_. 5 | 6 | |travis|_ |coveralls|_ 7 | 8 | 9 | To get started with development:: 10 | 11 | $ virtualenv --no-site-packages ve/ 12 | $ source ve/bin/activate 13 | (ve)$ pip install -r requirements.pip 14 | (ve)$ python 15 | >>> import smpp 16 | >>> 17 | 18 | Run the tests with nose 19 | 20 | (ve)$ nosetests 21 | 22 | .. _Twisted: http://www.twistedmatrix.com 23 | .. |travis| image:: https://travis-ci.org/praekelt/python-smpp.png?branch=develop 24 | .. _travis: https://travis-ci.org/praekelt/python-smpp 25 | 26 | .. |coveralls| image:: https://coveralls.io/repos/praekelt/python-smpp/badge.png?branch=develop 27 | .. _coveralls: https://coveralls.io/r/praekelt/python-smpp 28 | -------------------------------------------------------------------------------- /credentials_test.py: -------------------------------------------------------------------------------- 1 | 2 | logica = { 3 | 'host': 'localhost', 4 | 'port': 2775, 5 | 'system_id': 'test id', 6 | 'password': 'abc 123', 7 | } 8 | -------------------------------------------------------------------------------- /requirements-dev.pip: -------------------------------------------------------------------------------- 1 | nose 2 | coverage 3 | pep8 4 | -------------------------------------------------------------------------------- /requirements.pip: -------------------------------------------------------------------------------- 1 | # Our dependencies are all specified in setup.py. 2 | -e . 3 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | nosetests --with-doctest --with-coverage --cover-package=smpp --with-xunit && \ 3 | coverage xml smpp/* -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | with-doctest=1 3 | nocapture=1 4 | with-coverage=1 5 | cover-package=smpp 6 | cover-html=1 7 | cover-html-dir=./coverage-html/ 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def read_file(filename): 6 | filepath = os.path.join(os.path.dirname(__file__), filename) 7 | return open(filepath, 'r').read() 8 | 9 | setup( 10 | name="python-smpp", 11 | version="0.1.10a", 12 | url='http://github.com/praekelt/python-smpp', 13 | license='BSD', 14 | description="Python SMPP Library", 15 | long_description=read_file('README.rst'), 16 | author='Praekelt Foundation', 17 | author_email='dev@praekeltfoundation.org', 18 | packages=find_packages(), 19 | install_requires=[ 20 | ], 21 | classifiers=[ 22 | 'Development Status :: 4 - Beta', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: BSD License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Topic :: Software Development :: Libraries :: Python Modules', 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /smpp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praekelt/python-smpp/8a0753fc498ab6bcd6243aed5953cddd69cef2c0/smpp/__init__.py -------------------------------------------------------------------------------- /smpp/clickatell.py: -------------------------------------------------------------------------------- 1 | 2 | clickatell_defaults = { 3 | 'interface_version': '34', 4 | 'dest_addr_ton': 1, 5 | 'dest_addr_npi': 1, 6 | } 7 | -------------------------------------------------------------------------------- /smpp/esme.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from pdu_builder import * 4 | 5 | 6 | class ESME(object): 7 | 8 | def __init__(self): 9 | self.state = 'CLOSED' 10 | self.sequence_number = 1 11 | self.conn = None 12 | self.defaults = { 13 | 'host': '127.0.0.1', 14 | 'port': 2775, 15 | 'dest_addr_ton': 0, 16 | 'dest_addr_npi': 0, 17 | } 18 | 19 | def loadDefaults(self, defaults): 20 | self.defaults = dict(self.defaults, **defaults) 21 | 22 | def connect(self): 23 | if self.state in ['CLOSED']: 24 | self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 25 | self.conn.connect((self.defaults['host'], self.defaults['port'])) 26 | self.state = 'OPEN' 27 | 28 | def disconnect(self): 29 | if self.state in ['BOUND_TX', 'BOUND_RX', 'BOUND_TRX']: 30 | self.__unbind() 31 | if self.state in ['OPEN']: 32 | self.conn.close() 33 | self.state = 'CLOSED' 34 | 35 | def __recv(self): 36 | pdu = None 37 | length_bin = self.conn.recv(4) 38 | if not length_bin: 39 | return None 40 | else: 41 | # print 'length_bin', len(length_bin), length_bin 42 | if len(length_bin) == 4: 43 | length = int(binascii.b2a_hex(length_bin), 16) 44 | rest_bin = self.conn.recv(length-4) 45 | pdu = unpack_pdu(length_bin + rest_bin) 46 | print '...', pdu['header']['sequence_number'], 47 | print '>', pdu['header']['command_id'], 48 | print '...', pdu['header']['command_status'] 49 | return pdu 50 | 51 | def __is_ok(self, pdu, id_check=None): 52 | if (isinstance(pdu, dict) and 53 | pdu.get('header', {}).get('command_status') == 'ESME_ROK' and 54 | (id_check is None or id_check == pdu['header'].get('command_id'))): 55 | return True 56 | else: 57 | return False 58 | 59 | def bind_transmitter(self): 60 | if self.state in ['CLOSED']: 61 | self.connect() 62 | if self.state in ['OPEN']: 63 | pdu = BindTransmitter(self.sequence_number, **self.defaults) 64 | self.conn.send(pdu.get_bin()) 65 | self.sequence_number += 1 66 | if self.__is_ok(self.__recv(), 'bind_transmitter_resp'): 67 | self.state = 'BOUND_TX' 68 | 69 | def __unbind(self): 70 | if self.state in ['BOUND_TX', 'BOUND_RX', 'BOUND_TRX']: 71 | pdu = Unbind(self.sequence_number) 72 | self.conn.send(pdu.get_bin()) 73 | self.sequence_number += 1 74 | if self.__is_ok(self.__recv(), 'unbind_resp'): 75 | self.state = 'OPEN' 76 | 77 | # def unbind(self): # will probably be deprecated 78 | # self.__unbind() 79 | 80 | def submit_sm(self, **kwargs): 81 | if self.state in ['BOUND_TX', 'BOUND_TRX']: 82 | print dict(self.defaults, **kwargs) 83 | pdu = SubmitSM(self.sequence_number, **dict(self.defaults, **kwargs)) 84 | self.conn.send(pdu.get_bin()) 85 | self.sequence_number += 1 86 | submit_sm_resp = self.__recv() 87 | # print self.__is_ok(submit_sm_resp, 'submit_sm_resp') 88 | 89 | def submit_multi(self, dest_address=[], **kwargs): 90 | if self.state in ['BOUND_TX', 'BOUND_TRX']: 91 | pdu = SubmitMulti(self.sequence_number, **dict(self.defaults, **kwargs)) 92 | for item in dest_address: 93 | if isinstance(item, str): # assume strings are addresses not lists 94 | pdu.addDestinationAddress( 95 | item, 96 | dest_addr_ton=self.defaults['dest_addr_ton'], 97 | dest_addr_npi=self.defaults['dest_addr_npi'], 98 | ) 99 | elif isinstance(item, dict): 100 | if item.get('dest_flag') == 1: 101 | pdu.addDestinationAddress( 102 | item.get('destination_addr', ''), 103 | dest_addr_ton=item.get('dest_addr_ton', 104 | self.defaults['dest_addr_ton']), 105 | dest_addr_npi=item.get('dest_addr_npi', 106 | self.defaults['dest_addr_npi']), 107 | ) 108 | elif item.get('dest_flag') == 2: 109 | pdu.addDistributionList(item.get('dl_name')) 110 | self.conn.send(pdu.get_bin()) 111 | self.sequence_number += 1 112 | submit_multi_resp = self.__recv() 113 | # print self.__is_ok(submit_multi_resp, 'submit_multi_resp') 114 | -------------------------------------------------------------------------------- /smpp/pdu.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import re 3 | try: 4 | import json 5 | except: 6 | import simplejson as json 7 | 8 | 9 | # Inserting certain referenced dicts in here means they can be declared in the same order as in the spec. 10 | maps = {} 11 | 12 | 13 | # SMPP PDU Definition - SMPP v3.4, section 4, page 45 14 | 15 | mandatory_parameter_lists = { 16 | 'bind_transmitter': [ # SMPP v3.4, section 4.1.1, table 4-1, page 46 17 | {'name': 'system_id', 'min': 1, 'max': 16, 'var': True, 'type': 'string', 'map': None}, 18 | {'name': 'password', 'min': 1, 'max': 9, 'var': True, 'type': 'string', 'map': None}, 19 | {'name': 'system_type', 'min': 1, 'max': 13, 'var': True, 'type': 'string', 'map': None}, 20 | {'name': 'interface_version', 'min': 1, 'max': 1, 'var': False, 'type': 'hex', 'map': None}, 21 | {'name': 'addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 22 | {'name': 'addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 23 | {'name': 'address_range', 'min': 1, 'max': 41, 'var': True, 'type': 'string', 'map': None} 24 | ], 25 | 'bind_transmitter_resp': [ # SMPP v3.4, section 4.1.2, table 4-2, page 47 26 | {'name': 'system_id', 'min': 1, 'max': 16, 'var': True, 'type': 'string', 'map': None} 27 | ], 28 | 'bind_receiver': [ # SMPP v3.4, section 4.1.3, table 4-3, page 48 29 | {'name': 'system_id', 'min': 1, 'max': 16, 'var': True, 'type': 'string', 'map': None}, 30 | {'name': 'password', 'min': 1, 'max': 9, 'var': True, 'type': 'string', 'map': None}, 31 | {'name': 'system_type', 'min': 1, 'max': 13, 'var': True, 'type': 'string', 'map': None}, 32 | {'name': 'interface_version', 'min': 1, 'max': 1, 'var': False, 'type': 'hex', 'map': None}, 33 | {'name': 'addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 34 | {'name': 'addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 35 | {'name': 'address_range', 'min': 1, 'max': 41, 'var': True, 'type': 'string', 'map': None} 36 | ], 37 | 'bind_receiver_resp': [ # SMPP v3.4, section 4.1.4, table 4-4, page 50 38 | {'name': 'system_id', 'min': 1, 'max': 16, 'var': True, 'type': 'string', 'map': None} 39 | ], 40 | 'bind_transceiver': [ # SMPP v3.4, section 4.1.5, table 4-5, page 51 41 | {'name': 'system_id', 'min': 1, 'max': 16, 'var': True, 'type': 'string', 'map': None}, 42 | {'name': 'password', 'min': 1, 'max': 9, 'var': True, 'type': 'string', 'map': None}, 43 | {'name': 'system_type', 'min': 1, 'max': 13, 'var': True, 'type': 'string', 'map': None}, 44 | {'name': 'interface_version', 'min': 1, 'max': 1, 'var': False, 'type': 'hex', 'map': None}, 45 | {'name': 'addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 46 | {'name': 'addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 47 | {'name': 'address_range', 'min': 1, 'max': 41, 'var': True, 'type': 'string', 'map': None} 48 | ], 49 | 'bind_transceiver_resp': [ # SMPP v3.4, section 4.1.6, table 4-6, page 52 50 | {'name': 'system_id', 'min': 1, 'max': 16, 'var': True, 'type': 'string', 'map': None} 51 | ], 52 | 'outbind': [ # SMPP v3.4, section 4.1.7.1, page 54 53 | {'name': 'system_id', 'min': 1, 'max': 16, 'var': True, 'type': 'string', 'map': None}, 54 | {'name': 'password', 'min': 1, 'max': 9, 'var': True, 'type': 'string', 'map': None} 55 | ], 56 | 'unbind': [ # SMPP v3.4, section 4.2.1, table 4-7, page 56 57 | ], 58 | 'unbind_resp': [ # SMPP v3.4, section 4.2.2, table 4-8, page 56 59 | ], 60 | 'generic_nack': [ # SMPP v3.4, section 4.3.1, table 4-9, page 57 61 | ], 62 | 'submit_sm': [ # SMPP v3.4, section 4.4.1, table 4-10, page 59-61 63 | {'name': 'service_type', 'min': 1, 'max': 6, 'var': True, 'type': 'string', 'map': None}, 64 | {'name': 'source_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 65 | {'name': 'source_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 66 | {'name': 'source_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None}, 67 | {'name': 'dest_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 68 | {'name': 'dest_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 69 | {'name': 'destination_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None}, 70 | {'name': 'esm_class', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 71 | {'name': 'protocol_id', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 72 | {'name': 'priority_flag', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 73 | {'name': 'schedule_delivery_time', 'min': 1, 'max': 17, 'var': False, 'type': 'string', 'map': None}, 74 | {'name': 'validity_period', 'min': 1, 'max': 17, 'var': True, 'type': 'string', 'map': None}, 75 | {'name': 'registered_delivery', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 76 | {'name': 'replace_if_present_flag', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 77 | {'name': 'data_coding', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 78 | {'name': 'sm_default_msg_id', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 79 | {'name': 'sm_length', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 80 | {'name': 'short_message', 'min': 0, 'max': 254, 'var': 'sm_length', 'type': 'xstring', 'map': None} 81 | ], 82 | 'submit_sm_resp': [ # SMPP v3.4, section 4.4.2, table 4-11, page 67 83 | {'name': 'message_id', 'min': 0, 'max': 65, 'var': True, 'type': 'string', 'map': None} 84 | ], 85 | 'submit_multi': [ # SMPP v3.4, section 4.5.1, table 4-12, page 69-71 86 | {'name': 'service_type', 'min': 1, 'max': 6, 'var': True, 'type': 'string', 'map': None}, 87 | {'name': 'source_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 88 | {'name': 'source_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 89 | {'name': 'source_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None}, 90 | {'name': 'number_of_dests', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 91 | {'name': 'dest_address', 'min': 0, 'max': 0, 'var': 'number_of_dests', 'type': 'dest_address', 'map': None}, 92 | {'name': 'esm_class', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 93 | {'name': 'protocol_id', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 94 | {'name': 'priority_flag', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 95 | {'name': 'schedule_delivery_time', 'min': 1, 'max': 17, 'var': False, 'type': 'string', 'map': None}, 96 | {'name': 'validity_period', 'min': 1, 'max': 17, 'var': False, 'type': 'string', 'map': None}, 97 | {'name': 'registered_delivery', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 98 | {'name': 'replace_if_present_flag', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 99 | {'name': 'data_coding', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 100 | {'name': 'sm_default_msg_id', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 101 | {'name': 'sm_length', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 102 | {'name': 'short_message', 'min': 0, 'max': 254, 'var': 'sm_length', 'type': 'xstring', 'map': None} 103 | ], 104 | 'dest_address': [ # SMPP v3.4, section 4.5.1.1, table 4-13, page 75 105 | {'name': 'dest_flag', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None} 106 | # 'sme_dest_address' or 'distribution_list' goes here 107 | ], 108 | 'sme_dest_address': [ # SMPP v3.4, section 4.5.1.1, table 4-14, page 75 109 | {'name': 'dest_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 110 | {'name': 'dest_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 111 | {'name': 'destination_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None} 112 | ], 113 | 'distribution_list': [ # SMPP v3.4, section 4.5.1.2, table 4-15, page 75 114 | {'name': 'dl_name', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None} 115 | ], 116 | 'submit_multi_resp': [ # SMPP v3.4, section 4.5.2, table 4-16, page 76 117 | {'name': 'message_id', 'min': 1, 'max': 65, 'var': True, 'type': 'string', 'map': None}, 118 | {'name': 'no_unsuccess', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 119 | {'name': 'unsuccess_sme', 'min': 0, 'max': 0, 'var': 'no_unsuccess', 'type': 'unsuccess_sme', 'map': None} 120 | ], 121 | 'unsuccess_sme': [ # SMPP v3.4, section 4.5.2.1, table 4-17, page 77 122 | {'name': 'dest_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 123 | {'name': 'dest_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 124 | {'name': 'destination_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None}, 125 | {'name': 'error_status_code', 'min': 4, 'max': 4, 'var': False, 'type': 'integer', 'map': None} 126 | ], 127 | 'deliver_sm': [ # SMPP v3.4, section 4.6.1, table 4-18, page 79-81 128 | {'name': 'service_type', 'min': 1, 'max': 6, 'var': True, 'type': 'string', 'map': None}, 129 | {'name': 'source_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 130 | {'name': 'source_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 131 | {'name': 'source_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None}, 132 | {'name': 'dest_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 133 | {'name': 'dest_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 134 | {'name': 'destination_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None}, 135 | {'name': 'esm_class', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 136 | {'name': 'protocol_id', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 137 | {'name': 'priority_flag', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 138 | {'name': 'schedule_delivery_time', 'min': 1, 'max': 1, 'var': False, 'type': 'string', 'map': None}, 139 | {'name': 'validity_period', 'min': 1, 'max': 1, 'var': False, 'type': 'string', 'map': None}, 140 | {'name': 'registered_delivery', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 141 | {'name': 'replace_if_present_flag', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 142 | {'name': 'data_coding', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 143 | {'name': 'sm_default_msg_id', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 144 | {'name': 'sm_length', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 145 | {'name': 'short_message', 'min': 0, 'max': 254, 'var': 'sm_length', 'type': 'xstring', 'map': None} 146 | ], 147 | 'deliver_sm_resp': [ # SMPP v3.4, section 4.6.2, table 4-19, page 85 148 | {'name': 'message_id', 'min': 1, 'max': 1, 'var': False, 'type': 'string', 'map': None} 149 | ], 150 | 'data_sm': [ # SMPP v3.4, section 4.7.1, table 4-20, page 87-88 151 | {'name': 'service_type', 'min': 1, 'max': 6, 'var': True, 'type': 'string', 'map': None}, 152 | {'name': 'source_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 153 | {'name': 'source_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 154 | {'name': 'source_addr', 'min': 1, 'max': 65, 'var': True, 'type': 'string', 'map': None}, 155 | {'name': 'dest_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 156 | {'name': 'dest_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 157 | {'name': 'destination_addr', 'min': 1, 'max': 65, 'var': True, 'type': 'string', 'map': None}, 158 | {'name': 'esm_class', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 159 | {'name': 'registered_delivery', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 160 | {'name': 'data_coding', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None} 161 | ], 162 | 'data_sm_resp': [ # SMPP v3.4, section 4.7.2, table 4-21, page 93 163 | {'name': 'message_id', 'min': 1, 'max': 65, 'var': True, 'type': 'string', 'map': None} 164 | ], 165 | 'query_sm': [ # SMPP v3.4, section 4.8.1, table 4-22, page 95 166 | {'name': 'message_id', 'min': 1, 'max': 65, 'var': True, 'type': 'string', 'map': None}, 167 | {'name': 'source_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 168 | {'name': 'source_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 169 | {'name': 'source_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None} 170 | ], 171 | 'query_sm_resp': [ # SMPP v3.4, section 4.7.2, table 4-21, page 93 172 | {'name': 'message_id', 'min': 1, 'max': 65, 'var': True, 'type': 'string', 'map': None}, 173 | {'name': 'final_date', 'min': 1, 'max': 17, 'var': False, 'type': 'string', 'map': None}, 174 | {'name': 'message_state', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 175 | {'name': 'error_code', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None} 176 | ], 177 | 'cancel_sm': [ # SMPP v3.4, section 4.9.1, table 4-24, page 98-99 178 | {'name': 'service_type', 'min': 1, 'max': 6, 'var': True, 'type': 'string', 'map': None}, 179 | {'name': 'message_id', 'min': 1, 'max': 65, 'var': True, 'type': 'string', 'map': None}, 180 | {'name': 'source_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 181 | {'name': 'source_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 182 | {'name': 'source_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None}, 183 | {'name': 'dest_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 184 | {'name': 'dest_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 185 | {'name': 'destination_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None} 186 | ], 187 | 'cancel_sm_resp': [ # SMPP v3.4, section 4.9.2, table 4-25, page 100 188 | ], 189 | 'replace_sm': [ # SMPP v3.4, section 4.10.1, table 4-26, page 102-103 190 | {'name': 'message_id', 'min': 1, 'max': 65, 'var': True, 'type': 'string', 'map': None}, 191 | {'name': 'source_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 192 | {'name': 'source_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 193 | {'name': 'source_addr', 'min': 1, 'max': 21, 'var': True, 'type': 'string', 'map': None}, 194 | {'name': 'schedule_delivery_time', 'min': 1, 'max': 17, 'var': False, 'type': 'string', 'map': None}, 195 | {'name': 'validity_period', 'min': 1, 'max': 17, 'var': False, 'type': 'string', 'map': None}, 196 | {'name': 'registered_delivery', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 197 | {'name': 'replace_if_present_flag', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 198 | {'name': 'data_coding', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 199 | {'name': 'sm_default_msg_id', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 200 | {'name': 'sm_length', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': None}, 201 | {'name': 'short_message', 'min': 0, 'max': 254, 'var': 'sm_length', 'type': 'xstring', 'map': None} 202 | ], 203 | 'replace_sm_resp': [ # SMPP v3.4, section 4.10.2, table 4-27, page 104 204 | ], 205 | 'enquire_link': [ # SMPP v3.4, section 4.11.1, table 4-28, page 106 206 | ], 207 | 'enquire_link_resp': [ # SMPP v3.4, section 4.11.2, table 4-29, page 106 208 | ], 209 | 'alert_notification': [ # SMPP v3.4, section 4.12.1, table 4-30, page 108 210 | {'name': 'source_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 211 | {'name': 'source_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 212 | {'name': 'source_addr', 'min': 1, 'max': 65, 'var': True, 'type': 'string', 'map': None}, 213 | {'name': 'esme_addr_ton', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_ton'}, 214 | {'name': 'esme_addr_npi', 'min': 1, 'max': 1, 'var': False, 'type': 'integer', 'map': 'addr_npi'}, 215 | {'name': 'esme_addr', 'min': 1, 'max': 65, 'var': True, 'type': 'string', 'map': None}, 216 | ] 217 | } 218 | 219 | 220 | def mandatory_parameter_list_by_command_name(command_name): 221 | return mandatory_parameter_lists.get(command_name, []) 222 | 223 | 224 | # Command IDs - SMPP v3.4, section 5.1.2.1, table 5-1, page 110-111 225 | command_id_by_hex = { 226 | '80000000': {'hex': '80000000', 'name': 'generic_nack'}, 227 | '00000001': {'hex': '00000001', 'name': 'bind_receiver'}, 228 | '80000001': {'hex': '80000001', 'name': 'bind_receiver_resp'}, 229 | '00000002': {'hex': '00000002', 'name': 'bind_transmitter'}, 230 | '80000002': {'hex': '80000002', 'name': 'bind_transmitter_resp'}, 231 | '00000003': {'hex': '00000003', 'name': 'query_sm'}, 232 | '80000003': {'hex': '80000003', 'name': 'query_sm_resp'}, 233 | '00000004': {'hex': '00000004', 'name': 'submit_sm'}, 234 | '80000004': {'hex': '80000004', 'name': 'submit_sm_resp'}, 235 | '00000005': {'hex': '00000005', 'name': 'deliver_sm'}, 236 | '80000005': {'hex': '80000005', 'name': 'deliver_sm_resp'}, 237 | '00000006': {'hex': '00000006', 'name': 'unbind'}, 238 | '80000006': {'hex': '80000006', 'name': 'unbind_resp'}, 239 | '00000007': {'hex': '00000007', 'name': 'replace_sm'}, 240 | '80000007': {'hex': '80000007', 'name': 'replace_sm_resp'}, 241 | '00000008': {'hex': '00000008', 'name': 'cancel_sm'}, 242 | '80000008': {'hex': '80000008', 'name': 'cancel_sm_resp'}, 243 | '00000009': {'hex': '00000009', 'name': 'bind_transceiver'}, 244 | '80000009': {'hex': '80000009', 'name': 'bind_transceiver_resp'}, 245 | '0000000b': {'hex': '0000000b', 'name': 'outbind'}, 246 | '00000015': {'hex': '00000015', 'name': 'enquire_link'}, 247 | '80000015': {'hex': '80000015', 'name': 'enquire_link_resp'}, 248 | '00000021': {'hex': '00000021', 'name': 'submit_multi'}, 249 | '80000021': {'hex': '80000021', 'name': 'submit_multi_resp'}, 250 | '00000102': {'hex': '00000102', 'name': 'alert_notification'}, 251 | '00000103': {'hex': '00000103', 'name': 'data_sm'}, 252 | '80000103': {'hex': '80000103', 'name': 'data_sm_resp'}, 253 | 254 | # v4 codes 255 | 256 | '80010000': {'hex': '80010000', 'name': 'generic_nack_v4'}, 257 | '00010001': {'hex': '00010001', 'name': 'bind_receiver_v4'}, 258 | '80010001': {'hex': '80010001', 'name': 'bind_receiver_resp_v4'}, 259 | '00010002': {'hex': '00010002', 'name': 'bind_transmitter_v4'}, 260 | '80010002': {'hex': '80010002', 'name': 'bind_transmitter_resp_v4'}, 261 | '00010003': {'hex': '00010003', 'name': 'query_sm_v4'}, 262 | '80010003': {'hex': '80010003', 'name': 'query_sm_resp_v4'}, 263 | '00010004': {'hex': '00010004', 'name': 'submit_sm_v4'}, 264 | '80010004': {'hex': '80010004', 'name': 'submit_sm_resp_v4'}, 265 | '00010005': {'hex': '00010005', 'name': 'deliver_sm_v4'}, 266 | '80010005': {'hex': '80010005', 'name': 'deliver_sm_resp_v4'}, 267 | '00010006': {'hex': '00010006', 'name': 'unbind_v4'}, 268 | '80010006': {'hex': '80010006', 'name': 'unbind_resp_v4'}, 269 | '00010007': {'hex': '00010007', 'name': 'replace_sm_v4'}, 270 | '80010007': {'hex': '80010007', 'name': 'replace_sm_resp_v4'}, 271 | '00010008': {'hex': '00010008', 'name': 'cancel_sm_v4'}, 272 | '80010008': {'hex': '80010008', 'name': 'cancel_sm_resp_v4'}, 273 | '00010009': {'hex': '00010009', 'name': 'delivery_receipt_v4'}, 274 | '80010009': {'hex': '80010009', 'name': 'delivery_receipt_resp_v4'}, 275 | '0001000a': {'hex': '0001000a', 'name': 'enquire_link_v4'}, 276 | '8001000a': {'hex': '8001000a', 'name': 'enquire_link_resp_v4'}, 277 | '0001000b': {'hex': '0001000b', 'name': 'outbind_v4'}, 278 | } 279 | 280 | 281 | def command_id_name_by_hex(x): 282 | return command_id_by_hex.get(x, {}).get('name') 283 | 284 | 285 | command_id_by_name = { 286 | 'generic_nack' :{ 'hex': '80000000', 'name': 'generic_nack'}, 287 | 'bind_receiver' :{ 'hex': '00000001', 'name': 'bind_receiver'}, 288 | 'bind_receiver_resp' :{ 'hex': '80000001', 'name': 'bind_receiver_resp'}, 289 | 'bind_transmitter' :{ 'hex': '00000002', 'name': 'bind_transmitter'}, 290 | 'bind_transmitter_resp' :{ 'hex': '80000002', 'name': 'bind_transmitter_resp'}, 291 | 'query_sm' :{ 'hex': '00000003', 'name': 'query_sm'}, 292 | 'query_sm_resp' :{ 'hex': '80000003', 'name': 'query_sm_resp'}, 293 | 'submit_sm' :{ 'hex': '00000004', 'name': 'submit_sm'}, 294 | 'submit_sm_resp' :{ 'hex': '80000004', 'name': 'submit_sm_resp'}, 295 | 'deliver_sm' :{ 'hex': '00000005', 'name': 'deliver_sm'}, 296 | 'deliver_sm_resp' :{ 'hex': '80000005', 'name': 'deliver_sm_resp'}, 297 | 'unbind' :{ 'hex': '00000006', 'name': 'unbind'}, 298 | 'unbind_resp' :{ 'hex': '80000006', 'name': 'unbind_resp'}, 299 | 'replace_sm' :{ 'hex': '00000007', 'name': 'replace_sm'}, 300 | 'replace_sm_resp' :{ 'hex': '80000007', 'name': 'replace_sm_resp'}, 301 | 'cancel_sm' :{ 'hex': '00000008', 'name': 'cancel_sm'}, 302 | 'cancel_sm_resp' :{ 'hex': '80000008', 'name': 'cancel_sm_resp'}, 303 | 'bind_transceiver' :{ 'hex': '00000009', 'name': 'bind_transceiver'}, 304 | 'bind_transceiver_resp' :{ 'hex': '80000009', 'name': 'bind_transceiver_resp'}, 305 | 'outbind' :{ 'hex': '0000000b', 'name': 'outbind'}, 306 | 'enquire_link' :{ 'hex': '00000015', 'name': 'enquire_link'}, 307 | 'enquire_link_resp' :{ 'hex': '80000015', 'name': 'enquire_link_resp'}, 308 | 'submit_multi' :{ 'hex': '00000021', 'name': 'submit_multi'}, 309 | 'submit_multi_resp' :{ 'hex': '80000021', 'name': 'submit_multi_resp'}, 310 | 'alert_notification' :{ 'hex': '00000102', 'name': 'alert_notification'}, 311 | 'data_sm' :{ 'hex': '00000103', 'name': 'data_sm'}, 312 | 'data_sm_resp' :{ 'hex': '80000103', 'name': 'data_sm_resp'}, 313 | 314 | # v4 codes 315 | 316 | 'generic_nack_v4' :{ 'hex': '80010000', 'name': 'generic_nack_v4'}, 317 | 'bind_receiver_v4' :{ 'hex': '00010001', 'name': 'bind_receiver_v4'}, 318 | 'bind_receiver_resp_v4' :{ 'hex': '80010001', 'name': 'bind_receiver_resp_v4'}, 319 | 'bind_transmitter_v4' :{ 'hex': '00010002', 'name': 'bind_transmitter_v4'}, 320 | 'bind_transmitter_resp_v4': {'hex': '80010002', 'name': 'bind_transmitter_resp_v4'}, 321 | 'query_sm_v4' :{ 'hex': '00010003', 'name': 'query_sm_v4'}, 322 | 'query_sm_resp_v4' :{ 'hex': '80010003', 'name': 'query_sm_resp_v4'}, 323 | 'submit_sm_v4' :{ 'hex': '00010004', 'name': 'submit_sm_v4'}, 324 | 'submit_sm_resp_v4' :{ 'hex': '80010004', 'name': 'submit_sm_resp_v4'}, 325 | 'deliver_sm_v4' :{ 'hex': '00010005', 'name': 'deliver_sm_v4'}, 326 | 'deliver_sm_resp_v4' :{ 'hex': '80010005', 'name': 'deliver_sm_resp_v4'}, 327 | 'unbind_v4' :{ 'hex': '00010006', 'name': 'unbind_v4'}, 328 | 'unbind_resp_v4' :{ 'hex': '80010006', 'name': 'unbind_resp_v4'}, 329 | 'replace_sm_v4' :{ 'hex': '00010007', 'name': 'replace_sm_v4'}, 330 | 'replace_sm_resp_v4' :{ 'hex': '80010007', 'name': 'replace_sm_resp_v4'}, 331 | 'cancel_sm_v4' :{ 'hex': '00010008', 'name': 'cancel_sm_v4'}, 332 | 'cancel_sm_resp_v4' :{ 'hex': '80010008', 'name': 'cancel_sm_resp_v4'}, 333 | 'delivery_receipt_v4' :{ 'hex': '00010009', 'name': 'delivery_receipt_v4'}, 334 | 'delivery_receipt_resp_v4':{ 'hex': '80010009', 'name': 'delivery_receipt_resp_v4'}, 335 | 'enquire_link_v4' :{ 'hex': '0001000a', 'name': 'enquire_link_v4'}, 336 | 'enquire_link_resp_v4' :{ 'hex': '8001000a', 'name': 'enquire_link_resp_v4'}, 337 | 'outbind_v4' :{ 'hex': '0001000b', 'name': 'outbind_v4'} 338 | } 339 | 340 | 341 | def command_id_hex_by_name(n): 342 | return command_id_by_name.get(n, {}).get('hex') 343 | 344 | 345 | # SMPP Error Codes (ESME) - SMPP v3.4, section 5.1.3, table 5-2, page 112-114 346 | 347 | command_status_by_hex = { 348 | '00000000': {'hex': '00000000', 'name': 'ESME_ROK', 'description': 'No error'}, 349 | '00000001': {'hex': '00000001', 'name': 'ESME_RINVMSGLEN', 'description': 'Message Length is invalid'}, 350 | '00000002': {'hex': '00000002', 'name': 'ESME_RINVCMDLEN', 'description': 'Command Length is invalid'}, 351 | '00000003': {'hex': '00000003', 'name': 'ESME_RINVCMDID', 'description': 'Invalid Command ID'}, 352 | '00000004': {'hex': '00000004', 'name': 'ESME_RINVBNDSTS', 'description': 'Incorrect BIND Status for given command'}, 353 | '00000005': {'hex': '00000005', 'name': 'ESME_RALYBND', 'description': 'ESME Already in bound state'}, 354 | '00000006': {'hex': '00000006', 'name': 'ESME_RINVPRTFLG', 'description': 'Invalid priority flag'}, 355 | '00000007': {'hex': '00000007', 'name': 'ESME_RINVREGDLVFLG', 'description': 'Invalid registered delivery flag'}, 356 | '00000008': {'hex': '00000008', 'name': 'ESME_RSYSERR', 'description': 'System Error'}, 357 | '0000000a': {'hex': '0000000a', 'name': 'ESME_RINVSRCADR', 'description': 'Invalid source address'}, 358 | '0000000b': {'hex': '0000000b', 'name': 'ESME_RINVDSTADR', 'description': 'Invalid destination address'}, 359 | '0000000c': {'hex': '0000000c', 'name': 'ESME_RINVMSGID', 'description': 'Message ID is invalid'}, 360 | '0000000d': {'hex': '0000000d', 'name': 'ESME_RBINDFAIL', 'description': 'Bind failed'}, 361 | '0000000e': {'hex': '0000000e', 'name': 'ESME_RINVPASWD', 'description': 'Invalid password'}, 362 | '0000000f': {'hex': '0000000f', 'name': 'ESME_RINVSYSID', 'description': 'Invalid System ID'}, 363 | '00000011': {'hex': '00000011', 'name': 'ESME_RCANCELFAIL', 'description': 'Cancel SM Failed'}, 364 | '00000013': {'hex': '00000013', 'name': 'ESME_RREPLACEFAIL', 'description': 'Replace SM Failed'}, 365 | '00000014': {'hex': '00000014', 'name': 'ESME_RMSGQFUL', 'description': 'Message queue full'}, 366 | '00000015': {'hex': '00000015', 'name': 'ESME_RINVSERTYP', 'description': 'Invalid service type'}, 367 | '00000033': {'hex': '00000033', 'name': 'ESME_RINVNUMDESTS', 'description': 'Invalid number of destinations'}, 368 | '00000034': {'hex': '00000034', 'name': 'ESME_RINVDLNAME', 'description': 'Invalid distribution list name'}, 369 | '00000040': {'hex': '00000040', 'name': 'ESME_RINVDESTFLAG', 'description': 'Destination flag is invalid (submit_multi)'}, 370 | '00000042': {'hex': '00000042', 'name': 'ESME_RINVSUBREP', 'description': "Invalid `submit with replace' request (i.e. submit_sm with replace_if_present_flag set)"}, 371 | '00000043': {'hex': '00000043', 'name': 'ESME_RINVESMCLASS', 'description': 'Invalid esm_class field data'}, 372 | '00000044': {'hex': '00000044', 'name': 'ESME_RCNTSUBDL', 'description': 'Cannot submit to distribution list'}, 373 | '00000045': {'hex': '00000045', 'name': 'ESME_RSUBMITFAIL', 'description': 'submit_sm or submit_multi failed'}, 374 | '00000048': {'hex': '00000048', 'name': 'ESME_RINVSRCTON', 'description': 'Invalid source address TON'}, 375 | '00000049': {'hex': '00000049', 'name': 'ESME_RINVSRCNPI', 'description': 'Invalid source address NPI'}, 376 | '00000050': {'hex': '00000050', 'name': 'ESME_RINVDSTTON', 'description': 'Invalid destination address TON'}, 377 | '00000051': {'hex': '00000051', 'name': 'ESME_RINVDSTNPI', 'description': 'Invalid destination address NPI'}, 378 | '00000053': {'hex': '00000053', 'name': 'ESME_RINVSYSTYP', 'description': 'Invalid system_type field'}, 379 | '00000054': {'hex': '00000054', 'name': 'ESME_RINVREPFLAG', 'description': 'Invalid replace_if_present flag'}, 380 | '00000055': {'hex': '00000055', 'name': 'ESME_RINVNUMMSGS', 'description': 'Invalid number of messages'}, 381 | '00000058': {'hex': '00000058', 'name': 'ESME_RTHROTTLED', 'description': 'Throttling error (ESME has exceeded allowed message limits)'}, 382 | '00000061': {'hex': '00000061', 'name': 'ESME_RINVSCHED', 'description': 'Invalid scheduled delivery time'}, 383 | '00000062': {'hex': '00000062', 'name': 'ESME_RINVEXPIRY', 'description': 'Invalid message validity period (expiry time)'}, 384 | '00000063': {'hex': '00000063', 'name': 'ESME_RINVDFTMSGID', 'description': 'Predefined message invalid or not found'}, 385 | '00000064': {'hex': '00000064', 'name': 'ESME_RX_T_APPN', 'description': 'ESME Receiver Temporary App Error Code'}, 386 | '00000065': {'hex': '00000065', 'name': 'ESME_RX_P_APPN', 'description': 'ESME Receiver Permanent App Error Code'}, 387 | '00000066': {'hex': '00000066', 'name': 'ESME_RX_R_APPN', 'description': 'ESME Receiver Reject Message Error Code'}, 388 | '00000067': {'hex': '00000067', 'name': 'ESME_RQUERYFAIL', 'description': 'query_sm request failed'}, 389 | '000000c0': {'hex': '000000c0', 'name': 'ESME_RINVOPTPARSTREAM', 'description': 'Error in the optional part of the PDU Body'}, 390 | '000000c1': {'hex': '000000c1', 'name': 'ESME_ROPTPARNOTALLWD', 'description': 'Optional paramenter not allowed'}, 391 | '000000c2': {'hex': '000000c2', 'name': 'ESME_RINVPARLEN', 'description': 'Invalid parameter length'}, 392 | '000000c3': {'hex': '000000c3', 'name': 'ESME_RMISSINGOPTPARAM', 'description': 'Expected optional parameter missing'}, 393 | '000000c4': {'hex': '000000c4', 'name': 'ESME_RINVOPTPARAMVAL', 'description': 'Invalid optional parameter value'}, 394 | '000000fe': {'hex': '000000fe', 'name': 'ESME_RDELIVERYFAILURE', 'description': 'Delivery Failure (used for data_sm_resp)'}, 395 | '000000ff': {'hex': '000000ff', 'name': 'ESME_RUNKNOWNERR', 'description': 'Unknown error'} 396 | } 397 | 398 | 399 | def command_status_name_by_hex(x): 400 | return command_status_by_hex.get(x, {}).get('name') 401 | 402 | command_status_by_name = { 403 | 'ESME_ROK' :{ 'hex': '00000000', 'name': 'ESME_ROK', 'description': 'No error'}, 404 | 'ESME_RINVMSGLEN' :{ 'hex': '00000001', 'name': 'ESME_RINVMSGLEN', 'description': 'Message Length is invalid'}, 405 | 'ESME_RINVCMDLEN' :{ 'hex': '00000002', 'name': 'ESME_RINVCMDLEN', 'description': 'Command Length is invalid'}, 406 | 'ESME_RINVCMDID' :{ 'hex': '00000003', 'name': 'ESME_RINVCMDID', 'description': 'Invalid Command ID'}, 407 | 'ESME_RINVBNDSTS' :{ 'hex': '00000004', 'name': 'ESME_RINVBNDSTS', 'description': 'Incorrect BIND Status for given command'}, 408 | 'ESME_RALYBND' :{ 'hex': '00000005', 'name': 'ESME_RALYBND', 'description': 'ESME Already in bound state'}, 409 | 'ESME_RINVPRTFLG' :{ 'hex': '00000006', 'name': 'ESME_RINVPRTFLG', 'description': 'Invalid priority flag'}, 410 | 'ESME_RINVREGDLVFLG' :{ 'hex': '00000007', 'name': 'ESME_RINVREGDLVFLG', 'description': 'Invalid registered delivery flag'}, 411 | 'ESME_RSYSERR' :{ 'hex': '00000008', 'name': 'ESME_RSYSERR', 'description': 'System Error'}, 412 | 'ESME_RINVSRCADR' :{ 'hex': '0000000a', 'name': 'ESME_RINVSRCADR', 'description': 'Invalid source address'}, 413 | 'ESME_RINVDSTADR' :{ 'hex': '0000000b', 'name': 'ESME_RINVDSTADR', 'description': 'Invalid destination address'}, 414 | 'ESME_RINVMSGID' :{ 'hex': '0000000c', 'name': 'ESME_RINVMSGID', 'description': 'Message ID is invalid'}, 415 | 'ESME_RBINDFAIL' :{ 'hex': '0000000d', 'name': 'ESME_RBINDFAIL', 'description': 'Bind failed'}, 416 | 'ESME_RINVPASWD' :{ 'hex': '0000000e', 'name': 'ESME_RINVPASWD', 'description': 'Invalid password'}, 417 | 'ESME_RINVSYSID' :{ 'hex': '0000000f', 'name': 'ESME_RINVSYSID', 'description': 'Invalid System ID'}, 418 | 'ESME_RCANCELFAIL' :{ 'hex': '00000011', 'name': 'ESME_RCANCELFAIL', 'description': 'Cancel SM Failed'}, 419 | 'ESME_RREPLACEFAIL' :{ 'hex': '00000013', 'name': 'ESME_RREPLACEFAIL', 'description': 'Replace SM Failed'}, 420 | 'ESME_RMSGQFUL' :{ 'hex': '00000014', 'name': 'ESME_RMSGQFUL', 'description': 'Message queue full'}, 421 | 'ESME_RINVSERTYP' :{ 'hex': '00000015', 'name': 'ESME_RINVSERTYP', 'description': 'Invalid service type'}, 422 | 'ESME_RINVNUMDESTS' :{ 'hex': '00000033', 'name': 'ESME_RINVNUMDESTS', 'description': 'Invalid number of destinations'}, 423 | 'ESME_RINVDLNAME' :{ 'hex': '00000034', 'name': 'ESME_RINVDLNAME', 'description': 'Invalid distribution list name'}, 424 | 'ESME_RINVDESTFLAG' :{ 'hex': '00000040', 'name': 'ESME_RINVDESTFLAG', 'description': 'Destination flag is invalid (submit_multi)'}, 425 | 'ESME_RINVSUBREP' :{ 'hex': '00000042', 'name': 'ESME_RINVSUBREP', 'description': "Invalid `submit with replace' request (i.e. submit_sm with replace_if_present_flag set)"}, 426 | 'ESME_RINVESMCLASS' :{ 'hex': '00000043', 'name': 'ESME_RINVESMCLASS', 'description': 'Invalid esm_class field data'}, 427 | 'ESME_RCNTSUBDL' :{ 'hex': '00000044', 'name': 'ESME_RCNTSUBDL', 'description': 'Cannot submit to distribution list'}, 428 | 'ESME_RSUBMITFAIL' :{ 'hex': '00000045', 'name': 'ESME_RSUBMITFAIL', 'description': 'submit_sm or submit_multi failed'}, 429 | 'ESME_RINVSRCTON' :{ 'hex': '00000048', 'name': 'ESME_RINVSRCTON', 'description': 'Invalid source address TON'}, 430 | 'ESME_RINVSRCNPI' :{ 'hex': '00000049', 'name': 'ESME_RINVSRCNPI', 'description': 'Invalid source address NPI'}, 431 | 'ESME_RINVDSTTON' :{ 'hex': '00000050', 'name': 'ESME_RINVDSTTON', 'description': 'Invalid destination address TON'}, 432 | 'ESME_RINVDSTNPI' :{ 'hex': '00000051', 'name': 'ESME_RINVDSTNPI', 'description': 'Invalid destination address NPI'}, 433 | 'ESME_RINVSYSTYP' :{ 'hex': '00000053', 'name': 'ESME_RINVSYSTYP', 'description': 'Invalid system_type field'}, 434 | 'ESME_RINVREPFLAG' :{ 'hex': '00000054', 'name': 'ESME_RINVREPFLAG', 'description': 'Invalid replace_if_present flag'}, 435 | 'ESME_RINVNUMMSGS' :{ 'hex': '00000055', 'name': 'ESME_RINVNUMMSGS', 'description': 'Invalid number of messages'}, 436 | 'ESME_RTHROTTLED' :{ 'hex': '00000058', 'name': 'ESME_RTHROTTLED', 'description': 'Throttling error (ESME has exceeded allowed message limits)'}, 437 | 'ESME_RINVSCHED' :{ 'hex': '00000061', 'name': 'ESME_RINVSCHED', 'description': 'Invalid scheduled delivery time'}, 438 | 'ESME_RINVEXPIRY' :{ 'hex': '00000062', 'name': 'ESME_RINVEXPIRY', 'description': 'Invalid message validity period (expiry time)'}, 439 | 'ESME_RINVDFTMSGID' :{ 'hex': '00000063', 'name': 'ESME_RINVDFTMSGID', 'description': 'Predefined message invalid or not found'}, 440 | 'ESME_RX_T_APPN' :{ 'hex': '00000064', 'name': 'ESME_RX_T_APPN', 'description': 'ESME Receiver Temporary App Error Code'}, 441 | 'ESME_RX_P_APPN' :{ 'hex': '00000065', 'name': 'ESME_RX_P_APPN', 'description': 'ESME Receiver Permanent App Error Code'}, 442 | 'ESME_RX_R_APPN' :{ 'hex': '00000066', 'name': 'ESME_RX_R_APPN', 'description': 'ESME Receiver Reject Message Error Code'}, 443 | 'ESME_RQUERYFAIL' :{ 'hex': '00000067', 'name': 'ESME_RQUERYFAIL', 'description': 'query_sm request failed'}, 444 | 'ESME_RINVOPTPARSTREAM': {'hex': '000000c0', 'name': 'ESME_RINVOPTPARSTREAM', 'description': 'Error in the optional part of the PDU Body'}, 445 | 'ESME_ROPTPARNOTALLWD' :{ 'hex': '000000c1', 'name': 'ESME_ROPTPARNOTALLWD', 'description': 'Optional paramenter not allowed'}, 446 | 'ESME_RINVPARLEN' :{ 'hex': '000000c2', 'name': 'ESME_RINVPARLEN', 'description': 'Invalid parameter length'}, 447 | 'ESME_RMISSINGOPTPARAM': {'hex': '000000c3', 'name': 'ESME_RMISSINGOPTPARAM', 'description': 'Expected optional parameter missing'}, 448 | 'ESME_RINVOPTPARAMVAL' :{ 'hex': '000000c4', 'name': 'ESME_RINVOPTPARAMVAL', 'description': 'Invalid optional parameter value'}, 449 | 'ESME_RDELIVERYFAILURE': {'hex': '000000fe', 'name': 'ESME_RDELIVERYFAILURE', 'description': 'Delivery Failure (used for data_sm_resp)'}, 450 | 'ESME_RUNKNOWNERR' :{ 'hex': '000000ff', 'name': 'ESME_RUNKNOWNERR', 'description': 'Unknown error'} 451 | } 452 | 453 | 454 | def command_status_hex_by_name(n): 455 | return command_status_by_name.get(n, {}).get('hex') 456 | 457 | 458 | # Type of Number (TON) - SMPP v3.4, section 5.2.5, table 5-3, page 117 459 | 460 | maps['addr_ton_by_name'] = { 461 | 'unknown' : '00', 462 | 'international' : '01', 463 | 'national' : '02', 464 | 'network_specific' : '03', 465 | 'subscriber_number': '04', 466 | 'alphanumeric' : '05', 467 | 'abbreviated' : '06' 468 | } 469 | 470 | maps['addr_ton_by_hex'] = { 471 | '00': 'unknown', 472 | '01': 'international', 473 | '02': 'national', 474 | '03': 'network_specific', 475 | '04': 'subscriber_number', 476 | '05': 'alphanumeric', 477 | '06': 'abbreviated' 478 | } 479 | 480 | 481 | # Numberic Plan Indicator (NPI) - SMPP v3.4, section 5.2.6, table 5-4, page 118 482 | 483 | maps['addr_npi_by_name'] = { 484 | 'unknown' : '00', 485 | 'ISDN' : '01', 486 | 'data' : '03', 487 | 'telex' : '04', 488 | 'land_mobile': '06', 489 | 'national' : '08', 490 | 'private' : '09', 491 | 'ERMES' : '0a', 492 | 'internet' : '0e', 493 | 'WAP' : '12' 494 | } 495 | 496 | maps['addr_npi_by_hex'] = { 497 | '00': 'unknown', 498 | '01': 'ISDN', 499 | '03': 'data', 500 | '04': 'telex', 501 | '06': 'land_mobile', 502 | '08': 'national', 503 | '09': 'private', 504 | '0a': 'ERMES', 505 | '0e': 'internet', 506 | '12': 'WAP' 507 | } 508 | 509 | 510 | # ESM Class bits - SMPP v3.4, section 5.2.12, page 121 511 | 512 | maps['esm_class_bits'] = { 513 | 'mode_mask' : '03', 514 | 'type_mask' : '3c', 515 | 'feature_mask' : 'c0', 516 | 517 | 'mode_default' : '00', 518 | 'mode_datagram' : '01', 519 | 'mode_forward' : '02', 520 | 'mode_store_and_forward' : '03', 521 | 522 | 'type_default' : '00', 523 | 'type_delivery_receipt' : '04', 524 | 'type_delivery_ack' : '08', 525 | 'type_0011' : '0a', 526 | 'type_user_ack' : '10', 527 | 'type_0101' : '14', 528 | 'type_conversation_abort' : '18', 529 | 'type_0111' : '1a', 530 | 'type_intermed_deliv_notif' : '20', 531 | 'type_1001' : '24', 532 | 'type_1010' : '28', 533 | 'type_1011' : '2a', 534 | 'type_1100' : '30', 535 | 'type_1101' : '34', 536 | 'type_1110' : '38', 537 | 'type_1111' : '3a', 538 | 539 | 'feature_none' : '00', 540 | 'feature_UDHI' : '40', 541 | 'feature_reply_path' : '80', 542 | 'feature_UDHI_and_reply_path': 'c0' 543 | } 544 | 545 | 546 | # Registered Delivery bits - SMPP v3.4, section 5.2.17, page 124 547 | 548 | maps['registered_delivery_bits'] = { 549 | 'receipt_mask' : '03', 550 | 'ack_mask' : '0c', 551 | 'intermed_notif_mask' : '80', 552 | 553 | 'receipt_none' : '00', 554 | 'receipt_always' : '01', 555 | 'receipt_on_fail' : '02', 556 | 'receipt_res' : '03', 557 | 558 | 'ack_none' : '00', 559 | 'ack_delivery' : '04', 560 | 'ack_user' : '08', 561 | 'ack_delivery_and_user': '0c', 562 | 563 | 'intermed_notif_none' : '00', 564 | 'intermed_notif' : '10' 565 | } 566 | 567 | 568 | # submit_multi dest_flag constants - SMPP v3.4, section 5.2.25, page 129 569 | # maps['dest_flag_by_name'] = { 570 | # 'SME Address' :1, 571 | # 'Distribution List Name': 2 572 | # } 573 | 574 | 575 | # Message State codes returned in query_sm_resp PDUs - SMPP v3.4, section 5.2.28, table 5-6, page 130 576 | maps['message_state_by_name'] = { 577 | 'ENROUTE' : 1, 578 | 'DELIVERED' : 2, 579 | 'EXPIRED' : 3, 580 | 'DELETED' : 4, 581 | 'UNDELIVERABLE': 5, 582 | 'ACCEPTED' : 6, 583 | 'UNKNOWN' : 7, 584 | 'REJECTED' : 8 585 | } 586 | 587 | 588 | # Facility Code bits for SMPP v4 589 | 590 | maps['facility_code_bits'] = { 591 | 'GF_PVCY' : '00000001', 592 | 'GF_SUBADDR': '00000002', 593 | 'NF_CC' : '00080000', 594 | 'NF_PDC' : '00010000', 595 | 'NF_IS136' : '00020000', 596 | 'NF_IS95A' : '00040000' 597 | } 598 | 599 | 600 | # Optional Parameter Tags - SMPP v3.4, section 5.3.2, Table 5-7, page 132-133 601 | 602 | optional_parameter_tag_by_hex = { 603 | '0005': {'hex': '0005', 'name': 'dest_addr_subunit', 'type': 'integer', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.1, page 134 604 | '0006': {'hex': '0006', 'name': 'dest_network_type', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.3, page 135 605 | '0007': {'hex': '0007', 'name': 'dest_bearer_type', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.5, page 136 606 | '0008': {'hex': '0008', 'name': 'dest_telematics_id', 'type': 'integer', 'tech': 'GSM', 'min': 2}, # SMPP v3.4, section 5.3.2.7, page 137 607 | 608 | '000d': {'hex': '000d', 'name': 'source_addr_subunit', 'type': 'integer', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.2, page 134 609 | '000e': {'hex': '000e', 'name': 'source_network_type', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.4, page 135 610 | '000f': {'hex': '000f', 'name': 'source_bearer_type', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.6, page 136 611 | '0010': {'hex': '0010', 'name': 'source_telematics_id', 'type': 'integer', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.8, page 137 612 | 613 | '0017': {'hex': '0017', 'name': 'qos_time_to_live', 'type': 'integer', 'tech': 'Generic', 'min': 4}, # SMPP v3.4, section 5.3.2.9, page 138 614 | '0019': {'hex': '0019', 'name': 'payload_type', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.10, page 138 615 | 616 | '001d': {'hex': '001d', 'name': 'additional_status_info_text', 'type': 'string', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.11, page 139 617 | '001e': {'hex': '001e', 'name': 'receipted_message_id', 'type': 'string', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.12, page 139 618 | 619 | '0030': {'hex': '0030', 'name': 'ms_msg_wait_facilities', 'type': 'bitmask', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.13, page 140 620 | 621 | '0101': {'hex': '0101', 'name': 'PVCY_AuthenticationStr', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 58-62 622 | 623 | '0201': {'hex': '0201', 'name': 'privacy_indicator', 'type': 'integer', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.14, page 141 624 | '0202': {'hex': '0202', 'name': 'source_subaddress', 'type': 'hex', 'tech': 'CDMA, TDMA', 'min': 2}, # SMPP v3.4, section 5.3.2.15, page 142 625 | '0203': {'hex': '0203', 'name': 'dest_subaddress', 'type': 'hex', 'tech': 'CDMA, TDMA', 'min': 2}, # SMPP v3.4, section 5.3.2.16, page 143 626 | '0204': {'hex': '0204', 'name': 'user_message_reference', 'type': 'integer', 'tech': 'Generic', 'min': 2}, # SMPP v3.4, section 5.3.2.17, page 143 627 | '0205': {'hex': '0205', 'name': 'user_response_code', 'type': 'integer', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.18, page 144 628 | 629 | '020a': {'hex': '020a', 'name': 'source_port', 'type': 'integer', 'tech': 'Generic', 'min': 2}, # SMPP v3.4, section 5.3.2.20, page 145 630 | '020b': {'hex': '020b', 'name': 'destination_port', 'type': 'integer', 'tech': 'Generic', 'min': 2}, # SMPP v3.4, section 5.3.2.21, page 145 631 | '020c': {'hex': '020c', 'name': 'sar_msg_ref_num', 'type': 'integer', 'tech': 'Generic', 'min': 2}, # SMPP v3.4, section 5.3.2.22, page 146 632 | '020d': {'hex': '020d', 'name': 'language_indicator', 'type': 'integer', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.19, page 144 633 | '020e': {'hex': '020e', 'name': 'sar_total_segments', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.23, page 147 634 | '020f': {'hex': '020f', 'name': 'sar_segment_seqnum', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.24, page 147 635 | '0210': {'hex': '0210', 'name': 'sc_interface_version', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.25, page 148 636 | 637 | '0301': {'hex': '0301', 'name': 'CC_CBN', 'type': None, 'tech': 'V4'}, # v4 page 70 638 | '0302': {'hex': '0302', 'name': 'callback_num_pres_ind', 'type': 'bitmask', 'tech': 'TDMA'}, # SMPP v3.4, section 5.3.2.37, page 156 639 | '0303': {'hex': '0303', 'name': 'callback_num_atag', 'type': 'hex', 'tech': 'TDMA'}, # SMPP v3.4, section 5.3.2.38, page 157 640 | '0304': {'hex': '0304', 'name': 'number_of_messages', 'type': 'integer', 'tech': 'CDMA'}, # SMPP v3.4, section 5.3.2.39, page 158 641 | '0381': {'hex': '0381', 'name': 'callback_num', 'type': 'hex', 'tech': 'CDMA, TDMA, GSM, iDEN', 'min': 4}, # SMPP v3.4, section 5.3.2.36, page 155 642 | 643 | '0420': {'hex': '0420', 'name': 'dpf_result', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.28, page 149 644 | '0421': {'hex': '0421', 'name': 'set_dpf', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.29, page 150 645 | '0422': {'hex': '0422', 'name': 'ms_availability_status', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.30, page 151 646 | '0423': {'hex': '0423', 'name': 'network_error_code', 'type': 'hex', 'tech': 'Generic', 'min': 3}, # SMPP v3.4, section 5.3.2.31, page 152 647 | '0424': {'hex': '0424', 'name': 'message_payload', 'type': 'hex', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.32, page 153 648 | '0425': {'hex': '0425', 'name': 'delivery_failure_reason', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.33, page 153 649 | '0426': {'hex': '0426', 'name': 'more_messages_to_send', 'type': 'integer', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.34, page 154 650 | '0427': {'hex': '0427', 'name': 'message_state', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.35, page 154 651 | '0428': {'hex': '0428', 'name': 'congestion_state', 'type': None, 'tech': 'Generic'}, 652 | 653 | '0501': {'hex': '0501', 'name': 'ussd_service_op', 'type': 'hex', 'tech': 'GSM (USSD)'}, # SMPP v3.4, section 5.3.2.44, page 161 654 | 655 | '0600': {'hex': '0600', 'name': 'broadcast_channel_indicator', 'type': None, 'tech': 'GSM'}, 656 | '0601': {'hex': '0601', 'name': 'broadcast_content_type', 'type': None, 'tech': 'CDMA, TDMA, GSM'}, 657 | '0602': {'hex': '0602', 'name': 'broadcast_content_type_info', 'type': None, 'tech': 'CDMA, TDMA'}, 658 | '0603': {'hex': '0603', 'name': 'broadcast_message_class', 'type': None, 'tech': 'GSM'}, 659 | '0604': {'hex': '0604', 'name': 'broadcast_rep_num', 'type': None, 'tech': 'GSM'}, 660 | '0605': {'hex': '0605', 'name': 'broadcast_frequency_interval', 'type': None, 'tech': 'CDMA, TDMA, GSM'}, 661 | '0606': {'hex': '0606', 'name': 'broadcast_area_identifier', 'type': None, 'tech': 'CDMA, TDMA, GSM'}, 662 | '0607': {'hex': '0607', 'name': 'broadcast_error_status', 'type': None, 'tech': 'CDMA, TDMA, GSM'}, 663 | '0608': {'hex': '0608', 'name': 'broadcast_area_success', 'type': None, 'tech': 'GSM'}, 664 | '0609': {'hex': '0609', 'name': 'broadcast_end_time', 'type': None, 'tech': 'CDMA, TDMA, GSM'}, 665 | '060a': {'hex': '060a', 'name': 'broadcast_service_group', 'type': None, 'tech': 'CDMA, TDMA'}, 666 | '060b': {'hex': '060b', 'name': 'billing_identification', 'type': None, 'tech': 'Generic'}, 667 | 668 | '060d': {'hex': '060d', 'name': 'source_network_id', 'type': None, 'tech': 'Generic'}, 669 | '060e': {'hex': '060e', 'name': 'dest_network_id', 'type': None, 'tech': 'Generic'}, 670 | '060f': {'hex': '060f', 'name': 'source_node_id', 'type': None, 'tech': 'Generic'}, 671 | '0610': {'hex': '0610', 'name': 'dest_node_id', 'type': None, 'tech': 'Generic'}, 672 | '0611': {'hex': '0611', 'name': 'dest_addr_np_resolution', 'type': None, 'tech': 'CDMA, TDMA (US Only)'}, 673 | '0612': {'hex': '0612', 'name': 'dest_addr_np_information', 'type': None, 'tech': 'CDMA, TDMA (US Only)'}, 674 | '0613': {'hex': '0613', 'name': 'dest_addr_np_country', 'type': None, 'tech': 'CDMA, TDMA (US Only)'}, 675 | 676 | '1101': {'hex': '1101', 'name': 'PDC_MessageClass', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 75 677 | '1102': {'hex': '1102', 'name': 'PDC_PresentationOption', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 76 678 | '1103': {'hex': '1103', 'name': 'PDC_AlertMechanism', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 76 679 | '1104': {'hex': '1104', 'name': 'PDC_Teleservice', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 77 680 | '1105': {'hex': '1105', 'name': 'PDC_MultiPartMessage', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 77 681 | '1106': {'hex': '1106', 'name': 'PDC_PredefinedMsg', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 78 682 | 683 | '1201': {'hex': '1201', 'name': 'display_time', 'type': 'integer', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.26, page 148 684 | 685 | '1203': {'hex': '1203', 'name': 'sms_signal', 'type': 'integer', 'tech': 'TDMA', 'min': 2}, # SMPP v3.4, section 5.3.2.40, page 158 686 | '1204': {'hex': '1204', 'name': 'ms_validity', 'type': 'integer', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.27, page 149 687 | 688 | '1304': {'hex': '1304', 'name': 'IS95A_AlertOnDelivery', 'type': None, 'tech': 'CDMA'}, # v4 page 85 689 | '1306': {'hex': '1306', 'name': 'IS95A_LanguageIndicator', 'type': None, 'tech': 'CDMA'}, # v4 page 86 690 | 691 | '130c': {'hex': '130c', 'name': 'alert_on_message_delivery', 'type': None, 'tech': 'CDMA'}, # SMPP v3.4, section 5.3.2.41, page 159 692 | 693 | '1380': {'hex': '1380', 'name': 'its_reply_type', 'type': 'integer', 'tech': 'CDMA'}, # SMPP v3.4, section 5.3.2.42, page 159 694 | '1383': {'hex': '1383', 'name': 'its_session_info', 'type': 'hex', 'tech': 'CDMA', 'min': 2}, # SMPP v3.4, section 5.3.2.43, page 160 695 | 696 | '1402': {'hex': '1402', 'name': 'operator_id', 'type': None, 'tech': 'vendor extension'}, 697 | '1403': {'hex': '1403', 'name': 'tariff', 'type': None, 'tech': 'Mobile Network Code vendor extension'}, 698 | '1450': {'hex': '1450', 'name': 'mcc', 'type': None, 'tech': 'Mobile Country Code vendor extension'}, 699 | '1451': {'hex': '1451', 'name': 'mnc', 'type': None, 'tech': 'Mobile Network Code vendor extension'} 700 | } 701 | 702 | 703 | def optional_parameter_tag_name_by_hex(x): 704 | return optional_parameter_tag_by_hex.get(x, {}).get('name') 705 | 706 | 707 | def optional_parameter_tag_type_by_hex(x): 708 | return optional_parameter_tag_by_hex.get(x, {}).get('type') 709 | 710 | 711 | def optional_parameter_tag_min_by_hex(x): 712 | return optional_parameter_tag_by_hex.get(x, {}).get('min', 0) 713 | 714 | 715 | optional_parameter_tag_by_name = { 716 | 'dest_addr_subunit' :{ 'hex': '0005', 'name': 'dest_addr_subunit', 'type': 'integer', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.1, page 134 717 | 'dest_network_type' :{ 'hex': '0006', 'name': 'dest_network_type', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.3, page 135 718 | 'dest_bearer_type' :{ 'hex': '0007', 'name': 'dest_bearer_type', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.5, page 136 719 | 'dest_telematics_id' :{ 'hex': '0008', 'name': 'dest_telematics_id', 'type': 'integer', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.7, page 137 720 | 721 | 'source_addr_subunit' :{ 'hex': '000d', 'name': 'source_addr_subunit', 'type': 'integer', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.2, page 134 722 | 'source_network_type' :{ 'hex': '000e', 'name': 'source_network_type', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.4, page 135 723 | 'source_bearer_type' :{ 'hex': '000f', 'name': 'source_bearer_type', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.6, page 136 724 | 'source_telematics_id' :{ 'hex': '0010', 'name': 'source_telematics_id', 'type': 'integer', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.8, page 137 725 | 726 | 'qos_time_to_live' :{ 'hex': '0017', 'name': 'qos_time_to_live', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.9, page 138 727 | 'payload_type' :{ 'hex': '0019', 'name': 'payload_type', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.10, page 138 728 | 729 | 'additional_status_info_text' :{ 'hex': '001d', 'name': 'additional_status_info_text', 'type': 'string', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.11, page 139 730 | 'receipted_message_id' :{ 'hex': '001e', 'name': 'receipted_message_id', 'type': 'string', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.12, page 139 731 | 732 | 'ms_msg_wait_facilities' :{ 'hex': '0030', 'name': 'ms_msg_wait_facilities', 'type': 'bitmask', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.13, page 140 733 | 734 | 'PVCY_AuthenticationStr' :{ 'hex': '0101', 'name': 'PVCY_AuthenticationStr', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 58-62 735 | 736 | 'privacy_indicator' :{ 'hex': '0201', 'name': 'privacy_indicator', 'type': 'integer', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.14, page 141 737 | 'source_subaddress' :{ 'hex': '0202', 'name': 'source_subaddress', 'type': 'hex', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.15, page 142 738 | 'dest_subaddress' :{ 'hex': '0203', 'name': 'dest_subaddress', 'type': 'hex', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.16, page 143 739 | 'user_message_reference' :{ 'hex': '0204', 'name': 'user_message_reference', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.17, page 143 740 | 'user_response_code' :{ 'hex': '0205', 'name': 'user_response_code', 'type': 'integer', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.18, page 144 741 | 742 | 'source_port' :{ 'hex': '020a', 'name': 'source_port', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.20, page 145 743 | 'destination_port' :{ 'hex': '020b', 'name': 'destination_port', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.21, page 145 744 | 'sar_msg_ref_num' :{ 'hex': '020c', 'name': 'sar_msg_ref_num', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.22, page 146 745 | 'language_indicator' :{ 'hex': '020d', 'name': 'language_indicator', 'type': 'integer', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.19, page 144 746 | 'sar_total_segments' :{ 'hex': '020e', 'name': 'sar_total_segments', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.23, page 147 747 | 'sar_segment_seqnum' :{ 'hex': '020f', 'name': 'sar_segment_seqnum', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.24, page 147 748 | 'sc_interface_version' :{ 'hex': '0210', 'name': 'sc_interface_version', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.25, page 148 749 | 750 | 'CC_CBN' :{ 'hex': '0301', 'name': 'CC_CBN', 'type': None, 'tech': 'V4'}, # v4 page 70 751 | 'callback_num_pres_ind' :{ 'hex': '0302', 'name': 'callback_num_pres_ind', 'type': 'bitmask', 'tech': 'TDMA'}, # SMPP v3.4, section 5.3.2.37, page 156 752 | 'callback_num_atag' :{ 'hex': '0303', 'name': 'callback_num_atag', 'type': 'hex', 'tech': 'TDMA'}, # SMPP v3.4, section 5.3.2.38, page 157 753 | 'number_of_messages' :{ 'hex': '0304', 'name': 'number_of_messages', 'type': 'integer', 'tech': 'CDMA'}, # SMPP v3.4, section 5.3.2.39, page 158 754 | 'callback_num' :{ 'hex': '0381', 'name': 'callback_num', 'type': 'hex', 'tech': 'CDMA, TDMA, GSM, iDEN'}, # SMPP v3.4, section 5.3.2.36, page 155 755 | 756 | 'dpf_result' :{ 'hex': '0420', 'name': 'dpf_result', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.28, page 149 757 | 'set_dpf' :{ 'hex': '0421', 'name': 'set_dpf', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.29, page 150 758 | 'ms_availability_status' :{ 'hex': '0422', 'name': 'ms_availability_status', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.30, page 151 759 | 'network_error_code' :{ 'hex': '0423', 'name': 'network_error_code', 'type': 'hex', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.31, page 152 760 | 'message_payload' :{ 'hex': '0424', 'name': 'message_payload', 'type': 'hex', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.32, page 153 761 | 'delivery_failure_reason' :{ 'hex': '0425', 'name': 'delivery_failure_reason', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.33, page 153 762 | 'more_messages_to_send' :{ 'hex': '0426', 'name': 'more_messages_to_send', 'type': 'integer', 'tech': 'GSM'}, # SMPP v3.4, section 5.3.2.34, page 154 763 | 'message_state' :{ 'hex': '0427', 'name': 'message_state', 'type': 'integer', 'tech': 'Generic'}, # SMPP v3.4, section 5.3.2.35, page 154 764 | 'congestion_state' :{ 'hex': '0428', 'name': 'congestion_state', 'type': None, 'tech': 'Generic'}, 765 | 766 | 'ussd_service_op' :{ 'hex': '0501', 'name': 'ussd_service_op', 'type': 'hex', 'tech': 'GSM (USSD)'}, # SMPP v3.4, section 5.3.2.44, page 161 767 | 768 | 'broadcast_channel_indicator' :{ 'hex': '0600', 'name': 'broadcast_channel_indicator', 'type': None, 'tech': 'GSM'}, 769 | 'broadcast_content_type' :{ 'hex': '0601', 'name': 'broadcast_content_type', 'type': None, 'tech': 'CDMA, TDMA, GSM'}, 770 | 'broadcast_content_type_info' :{ 'hex': '0602', 'name': 'broadcast_content_type_info', 'type': None, 'tech': 'CDMA, TDMA'}, 771 | 'broadcast_message_class' :{ 'hex': '0603', 'name': 'broadcast_message_class', 'type': None, 'tech': 'GSM'}, 772 | 'broadcast_rep_num' :{ 'hex': '0604', 'name': 'broadcast_rep_num', 'type': None, 'tech': 'GSM'}, 773 | 'broadcast_frequency_interval': {'hex': '0605', 'name': 'broadcast_frequency_interval', 'type': None, 'tech': 'CDMA, TDMA, GSM'}, 774 | 'broadcast_area_identifier' :{ 'hex': '0606', 'name': 'broadcast_area_identifier', 'type': None, 'tech': 'CDMA, TDMA, GSM'}, 775 | 'broadcast_error_status' :{ 'hex': '0607', 'name': 'broadcast_error_status', 'type': None, 'tech': 'CDMA, TDMA, GSM'}, 776 | 'broadcast_area_success' :{ 'hex': '0608', 'name': 'broadcast_area_success', 'type': None, 'tech': 'GSM'}, 777 | 'broadcast_end_time' :{ 'hex': '0609', 'name': 'broadcast_end_time', 'type': None, 'tech': 'CDMA, TDMA, GSM'}, 778 | 'broadcast_service_group' :{ 'hex': '060a', 'name': 'broadcast_service_group', 'type': None, 'tech': 'CDMA, TDMA'}, 779 | 'billing_identification' :{ 'hex': '060b', 'name': 'billing_identification', 'type': None, 'tech': 'Generic'}, 780 | 781 | 'source_network_id' :{ 'hex': '060d', 'name': 'source_network_id', 'type': None, 'tech': 'Generic'}, 782 | 'dest_network_id' :{ 'hex': '060e', 'name': 'dest_network_id', 'type': None, 'tech': 'Generic'}, 783 | 'source_node_id' :{ 'hex': '060f', 'name': 'source_node_id', 'type': None, 'tech': 'Generic'}, 784 | 'dest_node_id' :{ 'hex': '0610', 'name': 'dest_node_id', 'type': None, 'tech': 'Generic'}, 785 | 'dest_addr_np_resolution' :{ 'hex': '0611', 'name': 'dest_addr_np_resolution', 'type': None, 'tech': 'CDMA, TDMA (US Only)'}, 786 | 'dest_addr_np_information' :{ 'hex': '0612', 'name': 'dest_addr_np_information', 'type': None, 'tech': 'CDMA, TDMA (US Only)'}, 787 | 'dest_addr_np_country' :{ 'hex': '0613', 'name': 'dest_addr_np_country', 'type': None, 'tech': 'CDMA, TDMA (US Only)'}, 788 | 789 | 'PDC_MessageClass' :{ 'hex': '1101', 'name': 'PDC_MessageClass', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 75 790 | 'PDC_PresentationOption' :{ 'hex': '1102', 'name': 'PDC_PresentationOption', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 76 791 | 'PDC_AlertMechanism' :{ 'hex': '1103', 'name': 'PDC_AlertMechanism', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 76 792 | 'PDC_Teleservice' :{ 'hex': '1104', 'name': 'PDC_Teleservice', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 77 793 | 'PDC_MultiPartMessage' :{ 'hex': '1105', 'name': 'PDC_MultiPartMessage', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 77 794 | 'PDC_PredefinedMsg' :{ 'hex': '1106', 'name': 'PDC_PredefinedMsg', 'type': None, 'tech': '? (J-Phone)'}, # v4 page 78 795 | 796 | 'display_time' :{ 'hex': '1201', 'name': 'display_time', 'type': 'integer', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.26, page 148 797 | 798 | 'sms_signal' :{ 'hex': '1203', 'name': 'sms_signal', 'type': 'integer', 'tech': 'TDMA'}, # SMPP v3.4, section 5.3.2.40, page 158 799 | 'ms_validity' :{ 'hex': '1204', 'name': 'ms_validity', 'type': 'integer', 'tech': 'CDMA, TDMA'}, # SMPP v3.4, section 5.3.2.27, page 149 800 | 801 | 'IS95A_AlertOnDelivery' :{ 'hex': '1304', 'name': 'IS95A_AlertOnDelivery', 'type': None, 'tech': 'CDMA'}, # v4 page 85 802 | 'IS95A_LanguageIndicator' :{ 'hex': '1306', 'name': 'IS95A_LanguageIndicator', 'type': None, 'tech': 'CDMA'}, # v4 page 86 803 | 804 | 'alert_on_message_delivery' :{ 'hex': '130c', 'name': 'alert_on_message_delivery', 'type': None, 'tech': 'CDMA'}, # SMPP v3.4, section 5.3.2.41, page 159 805 | 806 | 'its_reply_type' :{ 'hex': '1380', 'name': 'its_reply_type', 'type': 'integer', 'tech': 'CDMA'}, # SMPP v3.4, section 5.3.2.42, page 159 807 | 'its_session_info' :{ 'hex': '1383', 'name': 'its_session_info', 'type': 'hex', 'tech': 'CDMA'}, # SMPP v3.4, section 5.3.2.43, page 160 808 | 809 | 'operator_id' :{ 'hex': '1402', 'name': 'operator_id', 'type': None, 'tech': 'vendor extension'}, 810 | 'tariff' :{ 'hex': '1403', 'name': 'tariff', 'type': None, 'tech': 'Mobile Network Code vendor extension'}, 811 | 'mcc' :{ 'hex': '1450', 'name': 'mcc', 'type': None, 'tech': 'Mobile Country Code vendor extension'}, 812 | 'mnc' :{ 'hex': '1451', 'name': 'mnc', 'type': None, 'tech': 'Mobile Network Code vendor extension'} 813 | } 814 | 815 | 816 | def optional_parameter_tag_hex_by_name(n): 817 | return optional_parameter_tag_by_name.get(n, {}).get('hex') 818 | 819 | 820 | # Decoding functions ####################################################### 821 | 822 | def unpack_pdu(pdu_bin): 823 | return decode_pdu(binascii.b2a_hex(pdu_bin)) 824 | 825 | 826 | def decode_pdu(pdu_hex): 827 | hex_ref = [pdu_hex] 828 | pdu = {} 829 | pdu['header'] = decode_header(hex_ref) 830 | command = pdu['header'].get('command_id', None) 831 | if command is not None: 832 | body = decode_body(command, hex_ref) 833 | if len(body) > 0: 834 | pdu['body'] = body 835 | return pdu 836 | 837 | 838 | def decode_header(hex_ref): 839 | pdu_hex = hex_ref[0] 840 | header = {} 841 | (command_length, command_id, command_status, sequence_number, hex_ref[0]) = \ 842 | (pdu_hex[0:8], pdu_hex[8:16], pdu_hex[16:24], pdu_hex[24:32], pdu_hex[32:]) 843 | length = int(command_length, 16) 844 | command = command_id_name_by_hex(command_id) 845 | status = command_status_name_by_hex(command_status) 846 | sequence = int(sequence_number, 16) 847 | header = {} 848 | header['command_length'] = length 849 | header['command_id'] = command 850 | header['command_status'] = status 851 | header['sequence_number'] = sequence 852 | return header 853 | 854 | 855 | def decode_body(command, hex_ref): 856 | body = {} 857 | if command is not None: 858 | fields = mandatory_parameter_list_by_command_name(command) 859 | mandatory = decode_mandatory_parameters(fields, hex_ref) 860 | if len(mandatory) > 0: 861 | body['mandatory_parameters'] = mandatory 862 | optional = decode_optional_parameters(hex_ref) 863 | if len(optional) > 0: 864 | body['optional_parameters'] = optional 865 | return body 866 | 867 | 868 | def decode_mandatory_parameters(fields, hex_ref): 869 | mandatory_parameters = {} 870 | if len(hex_ref[0]) > 1: 871 | for field in fields: 872 | # old = len(hex_ref[0]) 873 | data = '' 874 | octet = '' 875 | count = 0 876 | if field['var'] is True or field['var'] is False: 877 | while (len(hex_ref[0]) > 1 and (count < field['min'] or 878 | (field['var'] is True and count < field['max']+1 and 879 | octet != '00'))): 880 | octet = octpop(hex_ref) 881 | data += octet 882 | count += 1 883 | elif field['type'] in ['string', 'xstring']: 884 | count = mandatory_parameters[field['var']] 885 | if count == 0: 886 | data = None 887 | else: 888 | for i in range(count): 889 | if len(hex_ref[0]) > 1: 890 | data += octpop(hex_ref) 891 | else: 892 | count = mandatory_parameters[field['var']] 893 | if field['map'] is not None: 894 | mandatory_parameters[field['name']] = maps[field['map']+'_by_hex'].get(data, None) 895 | if field['map'] is None or mandatory_parameters[field['name']] is None: 896 | mandatory_parameters[field['name']] = decode_hex_type(data, field['type'], count, hex_ref) 897 | # print field['type'], (old - len(hex_ref[0]))/2, repr(data), field['name'], mandatory_parameters[field['name']] 898 | return mandatory_parameters 899 | 900 | 901 | def decode_optional_parameters(hex_ref): 902 | optional_parameters = [] 903 | hex = hex_ref[0] 904 | while len(hex) > 0: 905 | if len(hex) < 8: 906 | # We don't have enough data here for this to be a valid param. 907 | # TODO: Something better than `print` here. 908 | print "Invalid optional param data, ignoring: %s" % (hex,) 909 | break 910 | (tag_hex, length_hex, rest) = (hex[0:4], hex[4:8], hex[8:]) 911 | tag = optional_parameter_tag_name_by_hex(tag_hex) 912 | if tag is None: 913 | tag = tag_hex 914 | length = int(length_hex, 16) 915 | (value_hex, tail) = (rest[0:length*2], rest[length*2:]) 916 | if len(value_hex) == 0: 917 | value = None 918 | else: 919 | value = decode_hex_type(value_hex, optional_parameter_tag_type_by_hex(tag_hex)) 920 | hex = tail 921 | optional_parameters.append({'tag': tag, 'length': length, 'value': value}) 922 | return optional_parameters 923 | 924 | 925 | def decode_hex_type(hex, type, count=0, hex_ref=['']): 926 | if hex is None: 927 | return hex 928 | elif type == 'integer': 929 | return int(hex, 16) 930 | elif type == 'string': 931 | return re.sub('00', '', hex).decode('hex') 932 | elif type == 'xstring': 933 | return hex.decode('hex') 934 | elif (type == 'dest_address' or type == 'unsuccess_sme'): 935 | list = [] 936 | fields = mandatory_parameter_list_by_command_name(type) 937 | for i in range(count): 938 | item = decode_mandatory_parameters(fields, hex_ref) 939 | if item.get('dest_flag', None) == 1: # 'dest_address' only 940 | subfields = mandatory_parameter_list_by_command_name('sme_dest_address') 941 | rest = decode_mandatory_parameters(subfields, hex_ref) 942 | item.update(rest) 943 | elif item.get('dest_flag', None) == 2: # 'dest_address' only 944 | subfields = mandatory_parameter_list_by_command_name('distribution_list') 945 | rest = decode_mandatory_parameters(subfields, hex_ref) 946 | item.update(rest) 947 | list.append(item) 948 | return list 949 | else: 950 | return hex 951 | 952 | 953 | def octpop(hex_ref): 954 | octet = None 955 | if len(hex_ref[0]) > 1: 956 | (octet, hex_ref[0]) = (hex_ref[0][0:2], hex_ref[0][2:]) 957 | return octet 958 | 959 | 960 | # Encoding functions 961 | def pack_pdu(pdu_obj): 962 | return binascii.a2b_hex(encode_pdu(pdu_obj)) 963 | 964 | 965 | def encode_pdu(pdu_obj): 966 | header = pdu_obj.get('header', {}) 967 | body = pdu_obj.get('body', {}) 968 | mandatory = body.get('mandatory_parameters', {}) 969 | optional = body.get('optional_parameters', []) 970 | body_hex = '' 971 | fields = mandatory_parameter_list_by_command_name(header['command_id']) 972 | body_hex += encode_mandatory_parameters(mandatory, fields) 973 | for opt in optional: 974 | body_hex += encode_optional_parameter(opt['tag'], opt['value']) 975 | actual_length = 16 + len(body_hex)/2 976 | command_length = '%08x' % actual_length 977 | command_id = command_id_hex_by_name(header['command_id']) 978 | command_status = command_status_hex_by_name(header['command_status']) 979 | sequence_number = '%08x' % header['sequence_number'] 980 | pdu_hex = command_length + command_id + command_status + sequence_number + body_hex 981 | return pdu_hex 982 | 983 | 984 | def encode_mandatory_parameters(mandatory_obj, fields): 985 | mandatory_hex_array = [] 986 | index_names = {} 987 | index = 0 988 | for field in fields: 989 | param = mandatory_obj.get(field['name'], None) 990 | param_length = None 991 | if param is not None or field['min'] > 0: 992 | map = None 993 | if field['map'] is not None: 994 | map = maps.get(field['map']+'_by_name', None) 995 | if isinstance(param, list): 996 | hex_list = [] 997 | for item in param: 998 | flagfields = mandatory_parameter_list_by_command_name(field['type']) 999 | plusfields = [] 1000 | if item.get('dest_flag', None) == 1: 1001 | plusfields = mandatory_parameter_list_by_command_name('sme_dest_address') 1002 | elif item.get('dest_flag', None) == 2: 1003 | plusfields = mandatory_parameter_list_by_command_name('distribution_list') 1004 | hex_item = encode_mandatory_parameters(item, flagfields + plusfields) 1005 | if isinstance(hex_item, str) and len(hex_item) > 0: 1006 | hex_list.append(hex_item) 1007 | param_length = len(hex_list) 1008 | mandatory_hex_array.append(''.join(hex_list)) 1009 | else: 1010 | hex_param = encode_param_type( 1011 | param, field['type'], field['min'], field['max'], map) 1012 | param_length = len(hex_param)/2 1013 | mandatory_hex_array.append(hex_param) 1014 | index_names[field['name']] = index 1015 | length_index = index_names.get(field['var'], None) 1016 | if length_index is not None and param_length is not None: 1017 | mandatory_hex_array[length_index] = encode_param_type( 1018 | param_length, 1019 | 'integer', 1020 | len(mandatory_hex_array[length_index])/2) 1021 | index += 1 1022 | return ''.join(mandatory_hex_array) 1023 | 1024 | 1025 | def encode_optional_parameter(tag, value): 1026 | optional_hex_array = [] 1027 | tag_hex = optional_parameter_tag_hex_by_name(tag) 1028 | if tag_hex is not None: 1029 | value_hex = encode_param_type( 1030 | value, 1031 | optional_parameter_tag_type_by_hex(tag_hex), 1032 | optional_parameter_tag_min_by_hex(tag_hex), 1033 | ) 1034 | length_hex = '%04x' % (len(value_hex)/2) 1035 | optional_hex_array.append(tag_hex + length_hex + value_hex) 1036 | return ''.join(optional_hex_array) 1037 | 1038 | 1039 | def encode_param_type(param, type, min=0, max=None, map=None): 1040 | if param is None: 1041 | hex = None 1042 | elif map is not None: 1043 | if type == 'integer' and isinstance(param, int): 1044 | hex = ('%0'+str(min*2)+'x') % param 1045 | else: 1046 | hex = map.get(param, ('%0'+str(min*2)+'x') % 0) 1047 | elif type == 'integer': 1048 | hex = ('%0'+str(min*2)+'x') % int(param) 1049 | elif type == 'string': 1050 | hex = param.encode('hex') + '00' 1051 | elif type == 'xstring': 1052 | hex = param.encode('hex') 1053 | elif type == 'bitmask': 1054 | hex = param 1055 | elif type == 'hex': 1056 | hex = param 1057 | else: 1058 | hex = None 1059 | if hex: 1060 | if len(hex) % 2: 1061 | # pad odd length hex strings 1062 | hex = '0' + hex 1063 | if None not in (max, hex) and len(hex) > 2 * max: 1064 | raise ValueError("Value exceeds maximum size of %s." % (max,)) 1065 | return hex 1066 | -------------------------------------------------------------------------------- /smpp/pdu_builder.py: -------------------------------------------------------------------------------- 1 | from pdu import * 2 | 3 | 4 | class PDU(object): 5 | def __init__(self, command_id, command_status, sequence_number, **kwargs): 6 | super(PDU, self).__init__() 7 | self.obj = {} 8 | self.obj['header'] = {} 9 | self.obj['header']['command_length'] = 0 10 | self.obj['header']['command_id'] = command_id 11 | self.obj['header']['command_status'] = command_status 12 | self.obj['header']['sequence_number'] = sequence_number 13 | 14 | def add_optional_parameter(self, tag, value): 15 | if self.obj.get('body') is None: 16 | self.obj['body'] = {} 17 | if self.obj['body'].get('optional_parameters') is None: 18 | self.obj['body']['optional_parameters'] = [] 19 | self.obj['body']['optional_parameters'].append({ 20 | 'tag': tag, 21 | 'length': 0, 22 | 'value': value, 23 | }) 24 | 25 | def __add_optional_parameter(self, tag, value): 26 | # This is deprecated, but not everything has been updated yet. 27 | return self.add_optional_parameter(tag, value) 28 | 29 | def set_sar_msg_ref_num(self, value): 30 | self.add_optional_parameter('sar_msg_ref_num', value) 31 | 32 | def set_sar_segment_seqnum(self, value): 33 | self.add_optional_parameter('sar_segment_seqnum', value) 34 | 35 | def set_sar_total_segments(self, value): 36 | self.add_optional_parameter('sar_total_segments', value) 37 | 38 | def get_obj(self): 39 | return self.obj 40 | 41 | def get_hex(self): 42 | return encode_pdu(self.obj) 43 | 44 | def get_bin(self): 45 | return pack_pdu(self.obj) 46 | 47 | 48 | class Bind(PDU): 49 | def __init__(self, command_id, sequence_number, system_id='', password='', system_type='', 50 | interface_version='34', addr_ton=0, addr_npi=0, address_range='', **kwargs): 51 | super(Bind, self).__init__(command_id, 'ESME_ROK', sequence_number) 52 | self.obj['body'] = {} 53 | self.obj['body']['mandatory_parameters'] = {} 54 | self.obj['body']['mandatory_parameters']['system_id'] = system_id 55 | self.obj['body']['mandatory_parameters']['password'] = password 56 | self.obj['body']['mandatory_parameters']['system_type'] = system_type 57 | self.obj['body']['mandatory_parameters']['interface_version'] = interface_version 58 | self.obj['body']['mandatory_parameters']['addr_ton'] = addr_ton 59 | self.obj['body']['mandatory_parameters']['addr_npi'] = addr_npi 60 | self.obj['body']['mandatory_parameters']['address_range'] = address_range 61 | 62 | 63 | class BindTransmitter(Bind): 64 | def __init__(self, sequence_number, **kwargs): 65 | super(BindTransmitter, self).__init__('bind_transmitter', sequence_number, **kwargs) 66 | 67 | 68 | class BindReceiver(Bind): 69 | def __init__(self, sequence_number, **kwargs): 70 | super(BindReceiver, self).__init__('bind_receiver', sequence_number, **kwargs) 71 | 72 | 73 | class BindTransceiver(Bind): 74 | def __init__(self, sequence_number, **kwargs): 75 | super(BindTransceiver, self).__init__('bind_transceiver', sequence_number, **kwargs) 76 | 77 | 78 | class BindResp(PDU): 79 | def __init__(self, command_id, command_status, sequence_number, system_id='', **kwargs): 80 | super(BindResp, self).__init__(command_id, command_status, sequence_number) 81 | self.obj['body'] = {} 82 | self.obj['body']['mandatory_parameters'] = {} 83 | self.obj['body']['mandatory_parameters']['system_id'] = system_id 84 | 85 | 86 | class BindTransmitterResp(BindResp): 87 | def __init__(self, sequence_number, command_status="ESME_ROK", **kwargs): 88 | super(BindTransmitterResp, self).__init__('bind_transmitter_resp', command_status, sequence_number, **kwargs) 89 | 90 | 91 | class BindReceiverResp(BindResp): 92 | def __init__(self, sequence_number, command_status="ESME_ROK", **kwargs): 93 | super(BindReceiverResp, self).__init__('bind_receiver_resp', command_status, sequence_number, **kwargs) 94 | 95 | 96 | class BindTransceiverResp(BindResp): 97 | def __init__(self, sequence_number, command_status="ESME_ROK", **kwargs): 98 | super(BindTransceiverResp, self).__init__('bind_transceiver_resp', command_status, sequence_number, **kwargs) 99 | 100 | 101 | class Unbind(PDU): 102 | def __init__(self, sequence_number, **kwargs): 103 | super(Unbind, self).__init__('unbind', 'ESME_ROK', sequence_number, **kwargs) 104 | 105 | 106 | class UnbindResp(PDU): 107 | def __init__(self, sequence_number, **kwargs): 108 | super(UnbindResp, self).__init__('unbind_resp', 'ESME_ROK', sequence_number, **kwargs) 109 | 110 | 111 | class SM1(PDU): 112 | def __init__(self, command_id, sequence_number, service_type='', source_addr_ton=0, 113 | source_addr_npi=0, source_addr='', esm_class=0, protocol_id=0, priority_flag=0, 114 | schedule_delivery_time='', validity_period='', registered_delivery=0, 115 | replace_if_present_flag=0, data_coding=0, sm_default_msg_id=0, sm_length=0, 116 | short_message=None, **kwargs): 117 | super(SM1, self).__init__(command_id, 'ESME_ROK', sequence_number) 118 | self.obj['body'] = {} 119 | self.obj['body']['mandatory_parameters'] = {} 120 | self.obj['body']['mandatory_parameters']['service_type'] = service_type 121 | self.obj['body']['mandatory_parameters']['source_addr_ton'] = source_addr_ton 122 | self.obj['body']['mandatory_parameters']['source_addr_npi'] = source_addr_npi 123 | self.obj['body']['mandatory_parameters']['source_addr'] = source_addr 124 | self.obj['body']['mandatory_parameters']['esm_class'] = esm_class 125 | self.obj['body']['mandatory_parameters']['protocol_id'] = protocol_id 126 | self.obj['body']['mandatory_parameters']['priority_flag'] = priority_flag 127 | self.obj['body']['mandatory_parameters']['schedule_delivery_time'] = schedule_delivery_time 128 | self.obj['body']['mandatory_parameters']['validity_period'] = validity_period 129 | self.obj['body']['mandatory_parameters']['registered_delivery'] = registered_delivery 130 | self.obj['body']['mandatory_parameters']['replace_if_present_flag'] = replace_if_present_flag 131 | self.obj['body']['mandatory_parameters']['data_coding'] = data_coding 132 | self.obj['body']['mandatory_parameters']['sm_default_msg_id'] = sm_default_msg_id 133 | self.obj['body']['mandatory_parameters']['sm_length'] = sm_length 134 | self.obj['body']['mandatory_parameters']['short_message'] = short_message 135 | 136 | def add_message_payload(self, value): 137 | self.obj['body']['mandatory_parameters']['sm_length'] = 0 138 | self.obj['body']['mandatory_parameters']['short_message'] = None 139 | self.add_optional_parameter('message_payload', value) 140 | 141 | 142 | class SubmitMulti(SM1): 143 | def __init__(self, sequence_number, number_of_dests=0, dest_address=[], **kwargs): 144 | super(SubmitMulti, self).__init__('submit_multi', sequence_number, **kwargs) 145 | mandatory_parameters = self.obj['body']['mandatory_parameters'] 146 | mandatory_parameters['number_of_dests'] = number_of_dests 147 | mandatory_parameters['dest_address'] = [] + dest_address 148 | 149 | def addDestinationAddress(self, destination_addr, dest_addr_ton=0, dest_addr_npi=0): 150 | if isinstance(destination_addr, str) and len(destination_addr) > 0: 151 | new_entry = { 152 | 'dest_flag': 1, 153 | 'dest_addr_ton': dest_addr_ton, 154 | 'dest_addr_npi': dest_addr_npi, 155 | 'destination_addr': destination_addr, 156 | } 157 | mandatory_parameters = self.obj['body']['mandatory_parameters'] 158 | mandatory_parameters['dest_address'].append(new_entry) 159 | mandatory_parameters['number_of_dests'] = len( 160 | mandatory_parameters['dest_address']) 161 | return True 162 | else: 163 | return False 164 | 165 | def addDistributionList(self, dl_name): 166 | if isinstance(dl_name, str) and len(dl_name) > 0: 167 | new_entry = { 168 | 'dest_flag': 2, 169 | 'dl_name': dl_name, 170 | } 171 | mandatory_parameters = self.obj['body']['mandatory_parameters'] 172 | mandatory_parameters['dest_address'].append(new_entry) 173 | mandatory_parameters['number_of_dests'] = len( 174 | mandatory_parameters['dest_address']) 175 | return True 176 | else: 177 | return False 178 | 179 | 180 | class SM2(SM1): 181 | def __init__(self, command_id, sequence_number, dest_addr_ton=0, dest_addr_npi=0, 182 | destination_addr='', **kwargs): 183 | super(SM2, self).__init__(command_id, sequence_number, **kwargs) 184 | mandatory_parameters = self.obj['body']['mandatory_parameters'] 185 | mandatory_parameters['dest_addr_ton'] = dest_addr_ton 186 | mandatory_parameters['dest_addr_npi'] = dest_addr_npi 187 | mandatory_parameters['destination_addr'] = destination_addr 188 | 189 | 190 | class SubmitSM(SM2): 191 | def __init__(self, sequence_number, **kwargs): 192 | super(SubmitSM, self).__init__('submit_sm', sequence_number, **kwargs) 193 | 194 | 195 | class SubmitSMResp(PDU): 196 | def __init__(self, sequence_number, message_id, command_status='ESME_ROK', **kwargs): 197 | super(SubmitSMResp, self).__init__('submit_sm_resp', command_status, sequence_number, **kwargs) 198 | self.obj['body'] = {} 199 | self.obj['body']['mandatory_parameters'] = {} 200 | self.obj['body']['mandatory_parameters']['message_id'] = message_id 201 | 202 | 203 | class DeliverSM(SM2): 204 | def __init__(self, sequence_number, **kwargs): 205 | super(DeliverSM, self).__init__('deliver_sm', sequence_number, **kwargs) 206 | 207 | 208 | class DeliverSMResp(PDU): 209 | def __init__(self, sequence_number, message_id='', command_status='ESME_ROK', **kwargs): 210 | super(DeliverSMResp, self).__init__('deliver_sm_resp', command_status, sequence_number, **kwargs) 211 | self.obj['body'] = {} 212 | self.obj['body']['mandatory_parameters'] = {} 213 | self.obj['body']['mandatory_parameters']['message_id'] = '' 214 | 215 | 216 | class EnquireLink(PDU): 217 | def __init__(self, sequence_number, **kwargs): 218 | super(EnquireLink, self).__init__('enquire_link', 'ESME_ROK', sequence_number, **kwargs) 219 | 220 | 221 | class EnquireLinkResp(PDU): 222 | def __init__(self, sequence_number, **kwargs): 223 | super(EnquireLinkResp, self).__init__('enquire_link_resp', 'ESME_ROK', sequence_number, **kwargs) 224 | 225 | 226 | class QuerySM(PDU): 227 | def __init__(self, sequence_number, message_id, source_addr='', source_addr_ton=0, source_addr_npi=0, **kwargs): 228 | super(QuerySM, self).__init__('query_sm', 'ESME_ROK', sequence_number, **kwargs) 229 | self.obj['body'] = {} 230 | self.obj['body']['mandatory_parameters'] = {} 231 | self.obj['body']['mandatory_parameters']['message_id'] = message_id 232 | self.obj['body']['mandatory_parameters']['source_addr'] = source_addr 233 | self.obj['body']['mandatory_parameters']['source_addr_ton'] = source_addr_ton 234 | self.obj['body']['mandatory_parameters']['source_addr_npi'] = source_addr_npi 235 | 236 | 237 | # bind = BindTransmitter(system_id='test_id', password='abc123') 238 | # print bind.get_obj() 239 | # print bind.get_hex() 240 | # print bind.get_bin() 241 | # #print json.dumps(bind.get_obj(), indent=4, sort_keys=True) 242 | # #print json.dumps(decode_pdu(bind.get_hex()), indent=4, sort_keys=True) 243 | # print json.dumps(unpack_pdu(bind.get_bin()), indent=4, sort_keys=True) 244 | 245 | # sm = SubmitSM(short_message='testing testing') 246 | # print json.dumps(unpack_pdu(sm.get_bin()), indent=4, sort_keys=True) 247 | # sm.add_message_payload('616263646566676869') 248 | # print json.dumps(unpack_pdu(sm.get_bin()), indent=4, sort_keys=True) 249 | -------------------------------------------------------------------------------- /smpp/pdu_inspector.py: -------------------------------------------------------------------------------- 1 | from pdu import * 2 | 3 | 4 | def detect_multipart(pdu): 5 | to_msisdn = pdu['body']['mandatory_parameters']['destination_addr'] 6 | from_msisdn = pdu['body']['mandatory_parameters']['source_addr'] 7 | short_message = pdu['body']['mandatory_parameters']['short_message'] 8 | optional_parameters = {} 9 | for d in pdu['body'].get('optional_parameters', []): 10 | optional_parameters[d['tag']] = d['value'] 11 | # print repr(pdu) 12 | 13 | try: 14 | mdict = {'multipart_type': 'TLV'} 15 | mdict['to_msisdn'] = to_msisdn 16 | mdict['from_msisdn'] = from_msisdn 17 | mdict['reference_number'] = optional_parameters['sar_msg_ref_num'] 18 | mdict['total_number'] = optional_parameters['sar_total_segments'] 19 | mdict['part_number'] = optional_parameters['sar_segment_seqnum'] 20 | mdict['part_message'] = short_message 21 | return mdict 22 | except: 23 | pass 24 | 25 | # all other multipart types will fail on short_message == None 26 | if short_message is None: 27 | return None 28 | 29 | if short_message[0:1] == '\x00' and \ 30 | short_message[1:2] == '\x03' and \ 31 | len(short_message) >= 5: 32 | mdict = {'multipart_type': 'SAR'} 33 | mdict['to_msisdn'] = to_msisdn 34 | mdict['from_msisdn'] = from_msisdn 35 | mdict['reference_number'] = int(binascii.b2a_hex(short_message[2:3]), 16) 36 | mdict['total_number'] = int(binascii.b2a_hex(short_message[3:4]), 16) 37 | mdict['part_number'] = int(binascii.b2a_hex(short_message[4:5]), 16) 38 | mdict['part_message'] = short_message[5:] 39 | return mdict 40 | 41 | if short_message[0:1] == '\x05' and \ 42 | short_message[1:2] == '\x00' and \ 43 | short_message[2:3] == '\x03' and \ 44 | len(short_message) >= 6: 45 | mdict = {'multipart_type': 'CSM'} 46 | mdict['to_msisdn'] = to_msisdn 47 | mdict['from_msisdn'] = from_msisdn 48 | mdict['reference_number'] = int(binascii.b2a_hex(short_message[3:4]), 16) 49 | mdict['total_number'] = int(binascii.b2a_hex(short_message[4:5]), 16) 50 | mdict['part_number'] = int(binascii.b2a_hex(short_message[5:6]), 16) 51 | mdict['part_message'] = short_message[6:] 52 | return mdict 53 | 54 | if short_message[0:1] == '\x06' and \ 55 | short_message[1:2] == '\x00' and \ 56 | short_message[2:3] == '\x04' and \ 57 | len(short_message) >= 7: 58 | mdict = {'multipart_type': 'CSM16'} 59 | mdict['to_msisdn'] = to_msisdn 60 | mdict['from_msisdn'] = from_msisdn 61 | mdict['reference_number'] = int(binascii.b2a_hex(short_message[3:5]), 16) 62 | mdict['total_number'] = int(binascii.b2a_hex(short_message[5:6]), 16) 63 | mdict['part_number'] = int(binascii.b2a_hex(short_message[6:7]), 16) 64 | mdict['part_message'] = short_message[7:] 65 | return mdict 66 | 67 | return None 68 | 69 | 70 | def multipart_key(multipart, delimiter='_'): 71 | key_list = [] 72 | key_list.append(str(multipart.get('from_msisdn'))) 73 | key_list.append(str(multipart.get('to_msisdn'))) 74 | key_list.append(str(multipart.get('reference_number'))) 75 | key_list.append(str(multipart.get('total_number'))) 76 | return delimiter.join(key_list) 77 | 78 | 79 | class MultipartMessage: 80 | 81 | def __init__(self, array=None): 82 | self.array = {} 83 | for k, v in (array or {}).items(): 84 | self.array.update({int(k): v}) 85 | 86 | def add_pdu(self, pdu): 87 | part = detect_multipart(pdu) 88 | if part: 89 | self.array[part['part_number']] = part 90 | return True 91 | else: 92 | return False 93 | 94 | def get_partial(self): 95 | items = sorted(self.array.items()) 96 | message = ''.join([i[1]['part_message'] for i in items]) 97 | to_msisdn = from_msisdn = '' 98 | if len(items): 99 | to_msisdn = items[0][1].get('to_msisdn') 100 | from_msisdn = items[0][1].get('from_msisdn') 101 | return {'to_msisdn': to_msisdn, 102 | 'from_msisdn': from_msisdn, 103 | 'message': message} 104 | 105 | def get_completed(self): 106 | items = self.array.items() 107 | if len(items) and len(items) == items[0][1].get('total_number'): 108 | return self.get_partial() 109 | return None 110 | 111 | def get_key(self, delimiter='_'): 112 | items = self.array.items() 113 | if len(items): 114 | return multipart_key(items[0][1], delimiter) 115 | return None 116 | 117 | def get_array(self): 118 | return self.array 119 | -------------------------------------------------------------------------------- /smpp/smsc.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from esme import * 4 | 5 | 6 | class SMSC(ESME): 7 | # this is a dummy SMSC, just for testing 8 | 9 | def __init__(self, port=2775, credentials={}): 10 | self.credentials = credentials 11 | self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 12 | self.server.bind(('', port)) 13 | self.server.listen(1) 14 | self.conn, self.addr = self.server.accept() 15 | print 'Connected by', self.addr 16 | while 1: 17 | pdu = self._ESME__recv() 18 | if not pdu: 19 | break 20 | self.conn.send(pack_pdu(self.__response(pdu))) 21 | self.conn.close() 22 | 23 | def __response(self, pdu): 24 | pdu_resp = {} 25 | resp_header = {} 26 | pdu_resp['header'] = resp_header 27 | resp_header['command_length'] = 0 28 | resp_header['command_id'] = 'generic_nack' 29 | resp_header['command_status'] = 'ESME_ROK' 30 | resp_header['sequence_number'] = pdu['header']['sequence_number'] 31 | if pdu['header']['command_id'] in [ 32 | 'bind_transmitter', 33 | 'bind_receiver', 34 | 'bind_transceiver', 35 | 'unbind', 36 | 'submit_sm', 37 | 'submit_multi', 38 | 'deliver_sm', 39 | 'data_sm', 40 | 'query_sm', 41 | 'cancel_sm', 42 | 'replace_sm', 43 | 'enquire_link', 44 | ]: 45 | resp_header['command_id'] = pdu['header']['command_id']+'_resp' 46 | if pdu['header']['command_id'] in [ 47 | 'bind_transmitter', 48 | 'bind_receiver', 49 | 'bind_transceiver', 50 | ]: 51 | resp_body = {} 52 | pdu_resp['body'] = resp_body 53 | resp_mandatory_parameters = {} 54 | resp_body['mandatory_parameters'] = resp_mandatory_parameters 55 | resp_mandatory_parameters['system_id'] = pdu['body']['mandatory_parameters']['system_id'] 56 | if pdu['header']['command_id'] in [ 57 | # 'submit_sm', # message_id is optional in submit_sm 58 | 'submit_multi', 59 | 'deliver_sm', 60 | 'data_sm', 61 | 'query_sm', 62 | ]: 63 | resp_body = {} 64 | pdu_resp['body'] = resp_body 65 | resp_mandatory_parameters = {} 66 | resp_body['mandatory_parameters'] = resp_mandatory_parameters 67 | resp_mandatory_parameters['message_id'] = '' 68 | if pdu['header']['command_id'] == 'submit_multi': 69 | resp_mandatory_parameters['no_unsuccess'] = 0 70 | if pdu['header']['command_id'] == 'query_sm': 71 | resp_mandatory_parameters['final_date'] = '' 72 | resp_mandatory_parameters['message_state'] = 0 73 | resp_mandatory_parameters['error_code'] = 0 74 | return pdu_resp 75 | 76 | 77 | if __name__ == '__main__': 78 | smsc = SMSC(2777) 79 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/praekelt/python-smpp/8a0753fc498ab6bcd6243aed5953cddd69cef2c0/test/__init__.py -------------------------------------------------------------------------------- /test/pdu.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | pdu_objects = [ 4 | { 5 | 'header': { 6 | 'command_length': 0, 7 | 'command_id': 'bind_transmitter', 8 | 'command_status': 'ESME_ROK', 9 | 'sequence_number': 0, 10 | }, 11 | 'body': { 12 | 'mandatory_parameters': { 13 | 'system_id': 'test_system', 14 | 'password': 'abc123', 15 | 'system_type': '', 16 | 'interface_version': '34', 17 | 'addr_ton': 1, 18 | 'addr_npi': 1, 19 | 'address_range': '', 20 | }, 21 | }, 22 | }, 23 | { 24 | 'header': { 25 | 'command_length': 0, 26 | 'command_id': 'bind_transmitter_resp', 27 | 'command_status': 'ESME_ROK', 28 | 'sequence_number': 0, 29 | }, 30 | 'body': { 31 | 'mandatory_parameters': { 32 | 'system_id': 'test_system', 33 | }, 34 | }, 35 | }, 36 | { 37 | 'header': { 38 | 'command_length': 0, 39 | 'command_id': 'bind_receiver', 40 | 'command_status': 'ESME_ROK', 41 | 'sequence_number': 0, 42 | }, 43 | 'body': { 44 | 'mandatory_parameters': { 45 | 'system_id': 'test_system', 46 | 'password': 'abc123', 47 | 'system_type': '', 48 | 'interface_version': '34', 49 | 'addr_ton': 1, 50 | 'addr_npi': 1, 51 | 'address_range': '', 52 | }, 53 | }, 54 | }, 55 | { 56 | 'header': { 57 | 'command_length': 0, 58 | 'command_id': 'bind_receiver_resp', 59 | 'command_status': 'ESME_ROK', 60 | 'sequence_number': 0, 61 | }, 62 | 'body': { 63 | 'mandatory_parameters': { 64 | 'system_id': 'test_system', 65 | }, 66 | }, 67 | }, 68 | { 69 | 'header': { 70 | 'command_length': 0, 71 | 'command_id': 'bind_transceiver', 72 | 'command_status': 'ESME_ROK', 73 | 'sequence_number': 0, 74 | }, 75 | 'body': { 76 | 'mandatory_parameters': { 77 | 'system_id': 'test_system', 78 | 'password': 'abc123', 79 | 'system_type': '', 80 | 'interface_version': '34', 81 | 'addr_ton': 1, 82 | 'addr_npi': 1, 83 | 'address_range': '', 84 | }, 85 | }, 86 | }, 87 | { 88 | 'header': { 89 | 'command_length': 0, 90 | 'command_id': 'bind_transceiver_resp', 91 | 'command_status': 'ESME_ROK', 92 | 'sequence_number': 0, 93 | }, 94 | 'body': { 95 | 'mandatory_parameters': { 96 | 'system_id': 'test_system', 97 | }, 98 | }, 99 | }, 100 | { 101 | 'header': { 102 | 'command_length': 0, 103 | 'command_id': 'outbind', 104 | 'command_status': 'ESME_ROK', 105 | 'sequence_number': 0, 106 | }, 107 | 'body': { 108 | 'mandatory_parameters': { 109 | 'system_id': 'test_system', 110 | 'password': 'abc123', 111 | }, 112 | }, 113 | }, 114 | { 115 | 'header': { 116 | 'command_length': 0, 117 | 'command_id': 'unbind', 118 | 'command_status': 'ESME_ROK', 119 | 'sequence_number': 0, 120 | }, 121 | }, 122 | { 123 | 'header': { 124 | 'command_length': 0, 125 | 'command_id': 'unbind_resp', 126 | 'command_status': 'ESME_ROK', 127 | 'sequence_number': 0, 128 | }, 129 | }, 130 | { 131 | 'header': { 132 | 'command_length': 0, 133 | 'command_id': 'generic_nack', 134 | 'command_status': 'ESME_ROK', 135 | 'sequence_number': 0, 136 | }, 137 | }, 138 | { 139 | 'header': { 140 | 'command_length': 0, 141 | 'command_id': 'submit_sm', 142 | 'command_status': 'ESME_ROK', 143 | 'sequence_number': 0, 144 | }, 145 | 'body': { 146 | 'mandatory_parameters': { 147 | 'service_type': '', 148 | 'source_addr_ton': 1, 149 | 'source_addr_npi': 1, 150 | 'source_addr': '', 151 | 'dest_addr_ton': 1, 152 | 'dest_addr_npi': 1, 153 | 'destination_addr': '', 154 | 'esm_class': 0, 155 | 'protocol_id': 0, 156 | 'priority_flag': 0, 157 | 'schedule_delivery_time': '', 158 | 'validity_period': '', 159 | 'registered_delivery': 0, 160 | 'replace_if_present_flag': 0, 161 | 'data_coding': 0, 162 | 'sm_default_msg_id': 0, 163 | 'sm_length': 1, 164 | 'short_message': 'testing 123', 165 | }, 166 | }, 167 | }, 168 | { 169 | 'header': { 170 | 'command_length': 0, 171 | 'command_id': 'submit_sm', 172 | 'command_status': 'ESME_ROK', 173 | 'sequence_number': 0, 174 | }, 175 | 'body': { 176 | 'mandatory_parameters': { 177 | 'service_type': '', 178 | 'source_addr_ton': 1, 179 | 'source_addr_npi': 1, 180 | 'source_addr': '', 181 | 'dest_addr_ton': 1, 182 | 'dest_addr_npi': 1, 183 | 'destination_addr': '', 184 | 'esm_class': 0, 185 | 'protocol_id': 0, 186 | 'priority_flag': 0, 187 | 'schedule_delivery_time': '', 188 | 'validity_period': '', 189 | 'registered_delivery': 0, 190 | 'replace_if_present_flag': 0, 191 | 'data_coding': 0, 192 | 'sm_default_msg_id': 0, 193 | 'sm_length': 0, 194 | 'short_message': None, 195 | # 'short_message' can be of zero length 196 | }, 197 | 'optional_parameters': [ 198 | { 199 | 'tag': 'message_payload', 200 | 'length': 0, 201 | 'value': '5666', 202 | }, 203 | ], 204 | }, 205 | }, 206 | # ] 207 | # breaker = [ 208 | { 209 | 'header': { 210 | 'command_length': 0, 211 | 'command_id': 'submit_sm_resp', 212 | 'command_status': 'ESME_ROK', 213 | 'sequence_number': 0, 214 | }, 215 | 'body': { 216 | 'mandatory_parameters': { 217 | 'message_id': '', 218 | }, 219 | }, 220 | }, 221 | { 222 | 'header': { 223 | 'command_length': 0, 224 | 'command_id': 'submit_sm_resp', 225 | 'command_status': 'ESME_RSYSERR', 226 | 'sequence_number': 0, 227 | }, 228 | # submit_sm_resp can have no body for failures 229 | }, 230 | { 231 | 'header': { 232 | 'command_length': 0, 233 | 'command_id': 'submit_multi', 234 | 'command_status': 'ESME_ROK', 235 | 'sequence_number': 0, 236 | }, 237 | 'body': { 238 | 'mandatory_parameters': { 239 | 'service_type': '', 240 | 'source_addr_ton': 1, 241 | 'source_addr_npi': 1, 242 | 'source_addr': '', 243 | 'number_of_dests': 0, 244 | 'dest_address': [ 245 | { 246 | 'dest_flag': 1, 247 | 'dest_addr_ton': 1, 248 | 'dest_addr_npi': 1, 249 | 'destination_addr': 'the address' 250 | }, 251 | { 252 | 'dest_flag': 2, 253 | 'dl_name': 'the list', 254 | }, 255 | { 256 | 'dest_flag': 2, 257 | 'dl_name': 'the other list', 258 | }, 259 | # {} 260 | ], 261 | 'esm_class': 0, 262 | 'protocol_id': 0, 263 | 'priority_flag': 0, 264 | 'schedule_delivery_time': '', 265 | 'validity_period': '', 266 | 'registered_delivery': 0, 267 | 'replace_if_present_flag': 0, 268 | 'data_coding': 0, 269 | 'sm_default_msg_id': 0, 270 | 'sm_length': 1, 271 | 'short_message': 'testing 123', 272 | }, 273 | }, 274 | }, 275 | { 276 | 'header': { 277 | 'command_length': 0, 278 | 'command_id': 'submit_multi_resp', 279 | 'command_status': 'ESME_ROK', 280 | 'sequence_number': 0, 281 | }, 282 | 'body': { 283 | 'mandatory_parameters': { 284 | 'message_id': '', 285 | 'no_unsuccess': 5, 286 | 'unsuccess_sme': [ 287 | { 288 | 'dest_addr_ton': 1, 289 | 'dest_addr_npi': 1, 290 | 'destination_addr': '', 291 | 'error_status_code': 0, 292 | }, 293 | { 294 | 'dest_addr_ton': 3, 295 | 'dest_addr_npi': 1, 296 | 'destination_addr': '555', 297 | 'error_status_code': 0, 298 | }, 299 | ], 300 | }, 301 | }, 302 | }, 303 | # ] 304 | # breaker = [ 305 | { 306 | 'header': { 307 | 'command_length': 0, 308 | 'command_id': 'deliver_sm', 309 | 'command_status': 'ESME_ROK', 310 | 'sequence_number': 0, 311 | }, 312 | 'body': { 313 | 'mandatory_parameters': { 314 | 'service_type': '', 315 | 'source_addr_ton': 1, 316 | 'source_addr_npi': 1, 317 | 'source_addr': '', 318 | 'dest_addr_ton': 1, 319 | 'dest_addr_npi': 1, 320 | 'destination_addr': '', 321 | 'esm_class': 0, 322 | 'protocol_id': 0, 323 | 'priority_flag': 0, 324 | 'schedule_delivery_time': '', 325 | 'validity_period': '', 326 | 'registered_delivery': 0, 327 | 'replace_if_present_flag': 0, 328 | 'data_coding': 0, 329 | 'sm_default_msg_id': 0, 330 | 'sm_length': 1, 331 | 'short_message': '', 332 | }, 333 | }, 334 | }, 335 | { 336 | 'header': { 337 | 'command_length': 0, 338 | 'command_id': 'deliver_sm_resp', 339 | 'command_status': 'ESME_ROK', 340 | 'sequence_number': 0, 341 | }, 342 | 'body': { 343 | 'mandatory_parameters': { 344 | 'message_id': '', 345 | }, 346 | }, 347 | }, 348 | { 349 | 'header': { 350 | 'command_length': 0, 351 | 'command_id': 'data_sm', 352 | 'command_status': 'ESME_ROK', 353 | 'sequence_number': 0, 354 | }, 355 | 'body': { 356 | 'mandatory_parameters': { 357 | 'service_type': '', 358 | 'source_addr_ton': 1, 359 | 'source_addr_npi': 1, 360 | 'source_addr': '', 361 | 'dest_addr_ton': 1, 362 | 'dest_addr_npi': 1, 363 | 'destination_addr': '', 364 | 'esm_class': 0, 365 | 'registered_delivery': 0, 366 | 'data_coding': 0, 367 | }, 368 | 'optional_parameters': [ 369 | { 370 | 'tag': 'message_payload', 371 | 'length': 0, 372 | 'value': '', 373 | }, 374 | ], 375 | }, 376 | }, 377 | { 378 | 'header': { 379 | 'command_length': 0, 380 | 'command_id': 'data_sm_resp', 381 | 'command_status': 'ESME_ROK', 382 | 'sequence_number': 0, 383 | }, 384 | 'body': { 385 | 'mandatory_parameters': { 386 | 'message_id': '', 387 | }, 388 | }, 389 | }, 390 | { 391 | 'header': { 392 | 'command_length': 0, 393 | 'command_id': 'query_sm', 394 | 'command_status': 'ESME_ROK', 395 | 'sequence_number': 0, 396 | }, 397 | 'body': { 398 | 'mandatory_parameters': { 399 | 'message_id': '', 400 | 'source_addr_ton': 1, 401 | 'source_addr_npi': 1, 402 | 'source_addr': '', 403 | }, 404 | }, 405 | }, 406 | { 407 | 'header': { 408 | 'command_length': 0, 409 | 'command_id': 'query_sm_resp', 410 | 'command_status': 'ESME_ROK', 411 | 'sequence_number': 0, 412 | }, 413 | 'body': { 414 | 'mandatory_parameters': { 415 | 'message_id': '', 416 | 'final_date': '', 417 | 'message_state': 0, 418 | 'error_code': 0, 419 | }, 420 | }, 421 | }, 422 | { 423 | 'header': { 424 | 'command_length': 0, 425 | 'command_id': 'cancel_sm', 426 | 'command_status': 'ESME_ROK', 427 | 'sequence_number': 0, 428 | }, 429 | 'body': { 430 | 'mandatory_parameters': { 431 | 'service_type': '', 432 | 'message_id': '', 433 | 'source_addr_ton': 1, 434 | 'source_addr_npi': 1, 435 | 'source_addr': '', 436 | 'dest_addr_ton': 1, 437 | 'dest_addr_npi': 1, 438 | 'destination_addr': '', 439 | }, 440 | }, 441 | }, 442 | { 443 | 'header': { 444 | 'command_length': 0, 445 | 'command_id': 'cancel_sm_resp', 446 | 'command_status': 'ESME_ROK', 447 | 'sequence_number': 0, 448 | }, 449 | }, 450 | { 451 | 'header': { 452 | 'command_length': 0, 453 | 'command_id': 'replace_sm', 454 | 'command_status': 'ESME_ROK', 455 | 'sequence_number': 0, 456 | }, 457 | 'body': { 458 | 'mandatory_parameters': { 459 | 'message_id': '', 460 | 'source_addr_ton': 1, 461 | 'source_addr_npi': 1, 462 | 'source_addr': '', 463 | 'schedule_delivery_time': '', 464 | 'validity_period': '', 465 | 'registered_delivery': 0, 466 | 'replace_if_present_flag': 0, 467 | 'data_coding': 0, 468 | 'sm_default_msg_id': 0, 469 | 'sm_length': 1, 470 | 'short_message': 'is this an = sign?', 471 | }, 472 | }, 473 | }, 474 | { 475 | 'header': { 476 | 'command_length': 0, 477 | 'command_id': 'replace_sm_resp', 478 | 'command_status': 'ESME_ROK', 479 | 'sequence_number': 0, 480 | }, 481 | }, 482 | { 483 | 'header': { 484 | 'command_length': 0, 485 | 'command_id': 'enquire_link', 486 | 'command_status': 'ESME_ROK', 487 | 'sequence_number': 0, 488 | }, 489 | }, 490 | { 491 | 'header': { 492 | 'command_length': 0, 493 | 'command_id': 'enquire_link_resp', 494 | 'command_status': 'ESME_ROK', 495 | 'sequence_number': 0, 496 | }, 497 | }, 498 | { 499 | 'header': { 500 | 'command_length': 0, 501 | 'command_id': 'alert_notification', 502 | 'command_status': 'ESME_ROK', 503 | 'sequence_number': 0, 504 | }, 505 | 'body': { 506 | 'mandatory_parameters': { 507 | 'source_addr_ton': 'international', 508 | 'source_addr_npi': 1, 509 | 'source_addr': '', 510 | 'esme_addr_ton': 9, 511 | 'esme_addr_npi': '', 512 | 'esme_addr': '', 513 | }, 514 | }, 515 | }, 516 | ] 517 | -------------------------------------------------------------------------------- /test/pdu_asserts.py: -------------------------------------------------------------------------------- 1 | 2 | ######################################## 3 | pdu_json_0000000001 = '''{ 4 | "body": { 5 | "mandatory_parameters": { 6 | "addr_npi": "ISDN", 7 | "addr_ton": "international", 8 | "address_range": "", 9 | "interface_version": "34", 10 | "password": "abc123", 11 | "system_id": "test_system", 12 | "system_type": "" 13 | } 14 | }, 15 | "header": { 16 | "command_id": "bind_transmitter", 17 | "command_length": 40, 18 | "command_status": "ESME_ROK", 19 | "sequence_number": 0 20 | } 21 | }''' 22 | 23 | ######################################## 24 | pdu_json_0000000002 = '''{ 25 | "body": { 26 | "mandatory_parameters": { 27 | "system_id": "test_system" 28 | } 29 | }, 30 | "header": { 31 | "command_id": "bind_transmitter_resp", 32 | "command_length": 28, 33 | "command_status": "ESME_ROK", 34 | "sequence_number": 0 35 | } 36 | }''' 37 | 38 | ######################################## 39 | pdu_json_0000000003 = '''{ 40 | "body": { 41 | "mandatory_parameters": { 42 | "addr_npi": "ISDN", 43 | "addr_ton": "international", 44 | "address_range": "", 45 | "interface_version": "34", 46 | "password": "abc123", 47 | "system_id": "test_system", 48 | "system_type": "" 49 | } 50 | }, 51 | "header": { 52 | "command_id": "bind_receiver", 53 | "command_length": 40, 54 | "command_status": "ESME_ROK", 55 | "sequence_number": 0 56 | } 57 | }''' 58 | 59 | ######################################## 60 | pdu_json_0000000004 = '''{ 61 | "body": { 62 | "mandatory_parameters": { 63 | "system_id": "test_system" 64 | } 65 | }, 66 | "header": { 67 | "command_id": "bind_receiver_resp", 68 | "command_length": 28, 69 | "command_status": "ESME_ROK", 70 | "sequence_number": 0 71 | } 72 | }''' 73 | 74 | ######################################## 75 | pdu_json_0000000005 = '''{ 76 | "body": { 77 | "mandatory_parameters": { 78 | "addr_npi": "ISDN", 79 | "addr_ton": "international", 80 | "address_range": "", 81 | "interface_version": "34", 82 | "password": "abc123", 83 | "system_id": "test_system", 84 | "system_type": "" 85 | } 86 | }, 87 | "header": { 88 | "command_id": "bind_transceiver", 89 | "command_length": 40, 90 | "command_status": "ESME_ROK", 91 | "sequence_number": 0 92 | } 93 | }''' 94 | 95 | ######################################## 96 | pdu_json_0000000006 = '''{ 97 | "body": { 98 | "mandatory_parameters": { 99 | "system_id": "test_system" 100 | } 101 | }, 102 | "header": { 103 | "command_id": "bind_transceiver_resp", 104 | "command_length": 28, 105 | "command_status": "ESME_ROK", 106 | "sequence_number": 0 107 | } 108 | }''' 109 | 110 | ######################################## 111 | pdu_json_0000000007 = '''{ 112 | "body": { 113 | "mandatory_parameters": { 114 | "password": "abc123", 115 | "system_id": "test_system" 116 | } 117 | }, 118 | "header": { 119 | "command_id": "outbind", 120 | "command_length": 35, 121 | "command_status": "ESME_ROK", 122 | "sequence_number": 0 123 | } 124 | }''' 125 | 126 | ######################################## 127 | pdu_json_0000000008 = '''{ 128 | "header": { 129 | "command_id": "unbind", 130 | "command_length": 16, 131 | "command_status": "ESME_ROK", 132 | "sequence_number": 0 133 | } 134 | }''' 135 | 136 | ######################################## 137 | pdu_json_0000000009 = '''{ 138 | "header": { 139 | "command_id": "unbind_resp", 140 | "command_length": 16, 141 | "command_status": "ESME_ROK", 142 | "sequence_number": 0 143 | } 144 | }''' 145 | 146 | ######################################## 147 | pdu_json_0000000010 = '''{ 148 | "header": { 149 | "command_id": "generic_nack", 150 | "command_length": 16, 151 | "command_status": "ESME_ROK", 152 | "sequence_number": 0 153 | } 154 | }''' 155 | 156 | ######################################## 157 | pdu_json_0000000011 = '''{ 158 | "body": { 159 | "mandatory_parameters": { 160 | "data_coding": 0, 161 | "dest_addr_npi": "ISDN", 162 | "dest_addr_ton": "international", 163 | "destination_addr": "", 164 | "esm_class": 0, 165 | "priority_flag": 0, 166 | "protocol_id": 0, 167 | "registered_delivery": 0, 168 | "replace_if_present_flag": 0, 169 | "schedule_delivery_time": "", 170 | "service_type": "", 171 | "short_message": "testing 123", 172 | "sm_default_msg_id": 0, 173 | "sm_length": 11, 174 | "source_addr": "", 175 | "source_addr_npi": "ISDN", 176 | "source_addr_ton": "international", 177 | "validity_period": "" 178 | } 179 | }, 180 | "header": { 181 | "command_id": "submit_sm", 182 | "command_length": 44, 183 | "command_status": "ESME_ROK", 184 | "sequence_number": 0 185 | } 186 | }''' 187 | 188 | ######################################## 189 | pdu_json_0000000012 = '''{ 190 | "body": { 191 | "mandatory_parameters": { 192 | "data_coding": 0, 193 | "dest_addr_npi": "ISDN", 194 | "dest_addr_ton": "international", 195 | "destination_addr": "", 196 | "esm_class": 0, 197 | "priority_flag": 0, 198 | "protocol_id": 0, 199 | "registered_delivery": 0, 200 | "replace_if_present_flag": 0, 201 | "schedule_delivery_time": "", 202 | "service_type": "", 203 | "short_message": null, 204 | "sm_default_msg_id": 0, 205 | "sm_length": 0, 206 | "source_addr": "", 207 | "source_addr_npi": "ISDN", 208 | "source_addr_ton": "international", 209 | "validity_period": "" 210 | }, 211 | "optional_parameters": [ 212 | { 213 | "length": 2, 214 | "tag": "message_payload", 215 | "value": "5666" 216 | } 217 | ] 218 | }, 219 | "header": { 220 | "command_id": "submit_sm", 221 | "command_length": 39, 222 | "command_status": "ESME_ROK", 223 | "sequence_number": 0 224 | } 225 | }''' 226 | 227 | ######################################## 228 | pdu_json_0000000013 = '''{ 229 | "body": { 230 | "mandatory_parameters": { 231 | "message_id": "" 232 | } 233 | }, 234 | "header": { 235 | "command_id": "submit_sm_resp", 236 | "command_length": 17, 237 | "command_status": "ESME_ROK", 238 | "sequence_number": 0 239 | } 240 | }''' 241 | 242 | ######################################## 243 | pdu_json_0000000014 = '''{ 244 | "header": { 245 | "command_id": "submit_sm_resp", 246 | "command_length": 16, 247 | "command_status": "ESME_RSYSERR", 248 | "sequence_number": 0 249 | } 250 | }''' 251 | 252 | ######################################## 253 | pdu_json_0000000015 = '''{ 254 | "body": { 255 | "mandatory_parameters": { 256 | "data_coding": 0, 257 | "dest_address": [ 258 | { 259 | "dest_addr_npi": "ISDN", 260 | "dest_addr_ton": "international", 261 | "dest_flag": 1, 262 | "destination_addr": "the address" 263 | }, 264 | { 265 | "dest_flag": 2, 266 | "dl_name": "the list" 267 | }, 268 | { 269 | "dest_flag": 2, 270 | "dl_name": "the other list" 271 | } 272 | ], 273 | "esm_class": 0, 274 | "number_of_dests": 3, 275 | "priority_flag": 0, 276 | "protocol_id": 0, 277 | "registered_delivery": 0, 278 | "replace_if_present_flag": 0, 279 | "schedule_delivery_time": "", 280 | "service_type": "", 281 | "short_message": "testing 123", 282 | "sm_default_msg_id": 0, 283 | "sm_length": 11, 284 | "source_addr": "", 285 | "source_addr_npi": "ISDN", 286 | "source_addr_ton": "international", 287 | "validity_period": "" 288 | } 289 | }, 290 | "header": { 291 | "command_id": "submit_multi", 292 | "command_length": 83, 293 | "command_status": "ESME_ROK", 294 | "sequence_number": 0 295 | } 296 | }''' 297 | 298 | ######################################## 299 | pdu_json_0000000016 = '''{ 300 | "body": { 301 | "mandatory_parameters": { 302 | "message_id": "", 303 | "no_unsuccess": 2, 304 | "unsuccess_sme": [ 305 | { 306 | "dest_addr_npi": "ISDN", 307 | "dest_addr_ton": "international", 308 | "destination_addr": "", 309 | "error_status_code": 0 310 | }, 311 | { 312 | "dest_addr_npi": "ISDN", 313 | "dest_addr_ton": "network_specific", 314 | "destination_addr": "555", 315 | "error_status_code": 0 316 | } 317 | ] 318 | } 319 | }, 320 | "header": { 321 | "command_id": "submit_multi_resp", 322 | "command_length": 35, 323 | "command_status": "ESME_ROK", 324 | "sequence_number": 0 325 | } 326 | }''' 327 | 328 | ######################################## 329 | pdu_json_0000000017 = '''{ 330 | "body": { 331 | "mandatory_parameters": { 332 | "data_coding": 0, 333 | "dest_addr_npi": "ISDN", 334 | "dest_addr_ton": "international", 335 | "destination_addr": "", 336 | "esm_class": 0, 337 | "priority_flag": 0, 338 | "protocol_id": 0, 339 | "registered_delivery": 0, 340 | "replace_if_present_flag": 0, 341 | "schedule_delivery_time": "", 342 | "service_type": "", 343 | "short_message": null, 344 | "sm_default_msg_id": 0, 345 | "sm_length": 0, 346 | "source_addr": "", 347 | "source_addr_npi": "ISDN", 348 | "source_addr_ton": "international", 349 | "validity_period": "" 350 | } 351 | }, 352 | "header": { 353 | "command_id": "deliver_sm", 354 | "command_length": 33, 355 | "command_status": "ESME_ROK", 356 | "sequence_number": 0 357 | } 358 | }''' 359 | 360 | ######################################## 361 | pdu_json_0000000018 = '''{ 362 | "body": { 363 | "mandatory_parameters": { 364 | "message_id": "" 365 | } 366 | }, 367 | "header": { 368 | "command_id": "deliver_sm_resp", 369 | "command_length": 17, 370 | "command_status": "ESME_ROK", 371 | "sequence_number": 0 372 | } 373 | }''' 374 | 375 | ######################################## 376 | pdu_json_0000000019 = '''{ 377 | "body": { 378 | "mandatory_parameters": { 379 | "data_coding": 0, 380 | "dest_addr_npi": "ISDN", 381 | "dest_addr_ton": "international", 382 | "destination_addr": "", 383 | "esm_class": 0, 384 | "registered_delivery": 0, 385 | "service_type": "", 386 | "source_addr": "", 387 | "source_addr_npi": "ISDN", 388 | "source_addr_ton": "international" 389 | }, 390 | "optional_parameters": [ 391 | { 392 | "length": 0, 393 | "tag": "message_payload", 394 | "value": null 395 | } 396 | ] 397 | }, 398 | "header": { 399 | "command_id": "data_sm", 400 | "command_length": 30, 401 | "command_status": "ESME_ROK", 402 | "sequence_number": 0 403 | } 404 | }''' 405 | 406 | ######################################## 407 | pdu_json_0000000020 = '''{ 408 | "body": { 409 | "mandatory_parameters": { 410 | "message_id": "" 411 | } 412 | }, 413 | "header": { 414 | "command_id": "data_sm_resp", 415 | "command_length": 17, 416 | "command_status": "ESME_ROK", 417 | "sequence_number": 0 418 | } 419 | }''' 420 | 421 | ######################################## 422 | pdu_json_0000000021 = '''{ 423 | "body": { 424 | "mandatory_parameters": { 425 | "message_id": "", 426 | "source_addr": "", 427 | "source_addr_npi": "ISDN", 428 | "source_addr_ton": "international" 429 | } 430 | }, 431 | "header": { 432 | "command_id": "query_sm", 433 | "command_length": 20, 434 | "command_status": "ESME_ROK", 435 | "sequence_number": 0 436 | } 437 | }''' 438 | 439 | ######################################## 440 | pdu_json_0000000022 = '''{ 441 | "body": { 442 | "mandatory_parameters": { 443 | "error_code": 0, 444 | "final_date": "", 445 | "message_id": "", 446 | "message_state": 0 447 | } 448 | }, 449 | "header": { 450 | "command_id": "query_sm_resp", 451 | "command_length": 20, 452 | "command_status": "ESME_ROK", 453 | "sequence_number": 0 454 | } 455 | }''' 456 | 457 | ######################################## 458 | pdu_json_0000000023 = '''{ 459 | "body": { 460 | "mandatory_parameters": { 461 | "dest_addr_npi": "ISDN", 462 | "dest_addr_ton": "international", 463 | "destination_addr": "", 464 | "message_id": "", 465 | "service_type": "", 466 | "source_addr": "", 467 | "source_addr_npi": "ISDN", 468 | "source_addr_ton": "international" 469 | } 470 | }, 471 | "header": { 472 | "command_id": "cancel_sm", 473 | "command_length": 24, 474 | "command_status": "ESME_ROK", 475 | "sequence_number": 0 476 | } 477 | }''' 478 | 479 | ######################################## 480 | pdu_json_0000000024 = '''{ 481 | "header": { 482 | "command_id": "cancel_sm_resp", 483 | "command_length": 16, 484 | "command_status": "ESME_ROK", 485 | "sequence_number": 0 486 | } 487 | }''' 488 | 489 | ######################################## 490 | pdu_json_0000000025 = '''{ 491 | "body": { 492 | "mandatory_parameters": { 493 | "data_coding": 0, 494 | "message_id": "", 495 | "registered_delivery": 0, 496 | "replace_if_present_flag": 0, 497 | "schedule_delivery_time": "", 498 | "short_message": "is this an = sign?", 499 | "sm_default_msg_id": 0, 500 | "sm_length": 18, 501 | "source_addr": "", 502 | "source_addr_npi": "ISDN", 503 | "source_addr_ton": "international", 504 | "validity_period": "" 505 | } 506 | }, 507 | "header": { 508 | "command_id": "replace_sm", 509 | "command_length": 45, 510 | "command_status": "ESME_ROK", 511 | "sequence_number": 0 512 | } 513 | }''' 514 | 515 | ######################################## 516 | pdu_json_0000000026 = '''{ 517 | "header": { 518 | "command_id": "replace_sm_resp", 519 | "command_length": 16, 520 | "command_status": "ESME_ROK", 521 | "sequence_number": 0 522 | } 523 | }''' 524 | 525 | ######################################## 526 | pdu_json_0000000027 = '''{ 527 | "header": { 528 | "command_id": "enquire_link", 529 | "command_length": 16, 530 | "command_status": "ESME_ROK", 531 | "sequence_number": 0 532 | } 533 | }''' 534 | 535 | ######################################## 536 | pdu_json_0000000028 = '''{ 537 | "header": { 538 | "command_id": "enquire_link_resp", 539 | "command_length": 16, 540 | "command_status": "ESME_ROK", 541 | "sequence_number": 0 542 | } 543 | }''' 544 | 545 | ######################################## 546 | pdu_json_0000000029 = '''{ 547 | "body": { 548 | "mandatory_parameters": { 549 | "esme_addr": "", 550 | "esme_addr_npi": "unknown", 551 | "esme_addr_ton": 9, 552 | "source_addr": "", 553 | "source_addr_npi": "ISDN", 554 | "source_addr_ton": "international" 555 | } 556 | }, 557 | "header": { 558 | "command_id": "alert_notification", 559 | "command_length": 22, 560 | "command_status": "ESME_ROK", 561 | "sequence_number": 0 562 | } 563 | }''' 564 | -------------------------------------------------------------------------------- /test/pdu_hex.py: -------------------------------------------------------------------------------- 1 | 2 | pdu_hex_strings = [ 3 | ''' 4 | 0000003C # command_length 5 | 00000004 # command_id 6 | 00000000 # command_status 7 | 00000005 # sequence_number 8 | 00 9 | 02 10 | 08 11 | 35353500 12 | 01 13 | 01 14 | 35353535353535353500 15 | 00 16 | 00 17 | 00 18 | 00 19 | 00 20 | 00 21 | 00 22 | 00 23 | 00 24 | 0F 25 | 48656C6C6F2077696B697065646961 26 | 00000000 27 | 001d00026566 28 | ''', 29 | 30 | ''' 31 | 00000000 # command_length 32 | 00000021 # command_id 33 | 00000000 # command_status 34 | 00000000 # sequence_number 35 | 00 36 | 00 37 | 00 38 | 00 39 | 02 40 | 01 01 01 6500 41 | 02 6600 42 | 00 43 | 00 44 | 00 45 | 00 46 | 00 47 | 00 48 | 00 49 | 00 50 | 00 51 | 00 52 | 0005 0002 0000 53 | 0000 0004 00000000 54 | ''', 55 | 56 | ''' 57 | 00000000 58 | 80000021 59 | 00000000 60 | 00000000 61 | 00 62 | 02 63 | 01016565650000000000 64 | 01016666660000000000 65 | ''', 66 | 67 | ] 68 | -------------------------------------------------------------------------------- /test/pdu_hex_asserts.py: -------------------------------------------------------------------------------- 1 | 2 | ######################################## 3 | pdu_json_0000000001 = '''{ 4 | "body": { 5 | "mandatory_parameters": { 6 | "data_coding": 0, 7 | "dest_addr_npi": "ISDN", 8 | "dest_addr_ton": "international", 9 | "destination_addr": "555555555", 10 | "esm_class": 0, 11 | "priority_flag": 0, 12 | "protocol_id": 0, 13 | "registered_delivery": 0, 14 | "replace_if_present_flag": 0, 15 | "schedule_delivery_time": "", 16 | "service_type": "", 17 | "short_message": "Hello wikipedia", 18 | "sm_default_msg_id": 0, 19 | "sm_length": 15, 20 | "source_addr": "555", 21 | "source_addr_npi": "national", 22 | "source_addr_ton": "national", 23 | "validity_period": "" 24 | }, 25 | "optional_parameters": [ 26 | { 27 | "length": 0, 28 | "tag": "0000", 29 | "value": null 30 | }, 31 | { 32 | "length": 2, 33 | "tag": "additional_status_info_text", 34 | "value": "ef" 35 | } 36 | ] 37 | }, 38 | "header": { 39 | "command_id": "submit_sm", 40 | "command_length": 60, 41 | "command_status": "ESME_ROK", 42 | "sequence_number": 5 43 | } 44 | }''' 45 | 46 | ######################################## 47 | pdu_json_0000000002 = '''{ 48 | "body": { 49 | "mandatory_parameters": { 50 | "data_coding": 0, 51 | "dest_address": [ 52 | { 53 | "dest_addr_npi": "ISDN", 54 | "dest_addr_ton": "international", 55 | "dest_flag": 1, 56 | "destination_addr": "e" 57 | }, 58 | { 59 | "dest_flag": 2, 60 | "dl_name": "f" 61 | } 62 | ], 63 | "esm_class": 0, 64 | "number_of_dests": 2, 65 | "priority_flag": 0, 66 | "protocol_id": 0, 67 | "registered_delivery": 0, 68 | "replace_if_present_flag": 0, 69 | "schedule_delivery_time": "", 70 | "service_type": "", 71 | "short_message": null, 72 | "sm_default_msg_id": 0, 73 | "sm_length": 0, 74 | "source_addr": "", 75 | "source_addr_npi": "unknown", 76 | "source_addr_ton": "unknown", 77 | "validity_period": "" 78 | }, 79 | "optional_parameters": [ 80 | { 81 | "length": 2, 82 | "tag": "dest_addr_subunit", 83 | "value": 0 84 | }, 85 | { 86 | "length": 4, 87 | "tag": "0000", 88 | "value": "00000000" 89 | } 90 | ] 91 | }, 92 | "header": { 93 | "command_id": "submit_multi", 94 | "command_length": 0, 95 | "command_status": "ESME_ROK", 96 | "sequence_number": 0 97 | } 98 | }''' 99 | 100 | ######################################## 101 | pdu_json_0000000003 = '''{ 102 | "body": { 103 | "mandatory_parameters": { 104 | "message_id": "", 105 | "no_unsuccess": 2, 106 | "unsuccess_sme": [ 107 | { 108 | "dest_addr_npi": "ISDN", 109 | "dest_addr_ton": "international", 110 | "destination_addr": "eee", 111 | "error_status_code": 0 112 | }, 113 | { 114 | "dest_addr_npi": "ISDN", 115 | "dest_addr_ton": "international", 116 | "destination_addr": "fff", 117 | "error_status_code": 0 118 | } 119 | ] 120 | } 121 | }, 122 | "header": { 123 | "command_id": "submit_multi_resp", 124 | "command_length": 0, 125 | "command_status": "ESME_ROK", 126 | "sequence_number": 0 127 | } 128 | }''' 129 | -------------------------------------------------------------------------------- /test/test_multipart.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | from smpp.pdu_builder import * 5 | from smpp.pdu_inspector import * 6 | 7 | 8 | class MultipartTestCase(unittest.TestCase): 9 | 10 | def setUp(self): 11 | pass 12 | 13 | def tearDown(self): 14 | pass 15 | 16 | def test_formats(self): 17 | """ 18 | Testing TLV vs SAR vs CSM vs CSM16 19 | """ 20 | tlv = DeliverSM(1, short_message='the first message part') 21 | tlv.set_sar_msg_ref_num(65017) 22 | tlv.set_sar_total_segments(2) 23 | tlv.set_sar_segment_seqnum(1) 24 | sar = DeliverSM(1, short_message='\x00\x03\xff\x02\x01the first message part') 25 | csm = DeliverSM(1, short_message='\x05\x00\x03\xff\x02\x01the first message part') 26 | csm16 = DeliverSM(1, short_message='\x06\x00\x04\xff\xff\x02\x01the first message part') 27 | non_multi = DeliverSM(1, short_message='whatever') 28 | none_short_message = DeliverSM(1, short_message=None) 29 | 30 | self.assertEquals(detect_multipart(unpack_pdu(tlv.get_bin()))['multipart_type'], 'TLV') 31 | self.assertEquals(detect_multipart(unpack_pdu(sar.get_bin()))['multipart_type'], 'SAR') 32 | self.assertEquals(detect_multipart(unpack_pdu(csm.get_bin()))['multipart_type'], 'CSM') 33 | self.assertEquals(detect_multipart(unpack_pdu(csm16.get_bin()))['multipart_type'], 'CSM16') 34 | self.assertEquals(detect_multipart(unpack_pdu(none_short_message.get_bin())), None) 35 | 36 | def test_ordering(self): 37 | """ 38 | Out of order pieces must be re-assembled in-order 39 | """ 40 | sar_1 = DeliverSM(1, short_message='\x00\x03\xff\x04\x01There she was just a') 41 | sar_2 = DeliverSM(1, short_message='\x00\x03\xff\x04\x02 walking down the street,') 42 | sar_3 = DeliverSM(1, short_message='\x00\x03\xff\x04\x03 singing doo wa diddy') 43 | sar_4 = DeliverSM(1, short_message='\x00\x03\xff\x04\x04 diddy dum diddy do') 44 | 45 | multi = MultipartMessage() 46 | multi.add_pdu(sar_3.get_obj()) 47 | multi.add_pdu(sar_4.get_obj()) 48 | multi.add_pdu(sar_2.get_obj()) 49 | multi.add_pdu(sar_1.get_obj()) 50 | self.assertEquals(multi.get_completed()['message'], 51 | 'There she was just a walking down the street, singing doo wa diddy diddy dum diddy do') 52 | 53 | def test_real_csm_data(self): 54 | """ 55 | Test with real-world data which uses the CSM format. 56 | """ 57 | 58 | asif_1 = {'body': {'mandatory_parameters': {'priority_flag': 0, 'source_addr': '261xxx720371', 'protocol_id': 0, 'replace_if_present_flag': 0, 'registered_delivery': 0, 'dest_addr_ton': 'international', 'source_addr_npi': 'ISDN', 'schedule_delivery_time': '', 'dest_addr_npi': 'ISDN', 'sm_length': 159, 'esm_class': 64, 'data_coding': 0, 'service_type': '', 'source_addr_ton': 'international', 'sm_default_msg_id': 0, 'validity_period': '', 'destination_addr': '261xxx782943', 'short_message': '\x05\x00\x03\x1a\x02\x01I try to send sms testing vumi sms sms sms sms msm sms sms sms sms sms sms sms sms sms ssms sms smS sms sms sms sms sms sms sms sns sns sms sms sms sms s'}, 'optional_parameters': [{'length': 2, 'tag': 'user_message_reference', 'value': 91}, {'length': 16, 'tag': 'dest_subaddress', 'value': 'a0000410020601030303070802090403'}]}, 'header': {'command_status': 'ESME_ROK', 'command_length': 242, 'sequence_number': 23, 'command_id': 'deliver_sm'}} 59 | 60 | asif_2 = {'body': {'mandatory_parameters': {'priority_flag': 1, 'source_addr': '261xxx720371', 'protocol_id': 0, 'replace_if_present_flag': 0, 'registered_delivery': 0, 'dest_addr_ton': 'international', 'source_addr_npi': 'ISDN', 'schedule_delivery_time': '', 'dest_addr_npi': 'ISDN', 'sm_length': 78, 'esm_class': 64, 'data_coding': 0, 'service_type': '', 'source_addr_ton': 'international', 'sm_default_msg_id': 0, 'validity_period': '', 'destination_addr': '261xxx782943', 'short_message': '\x05\x00\x03\x1a\x02\x02mns again again again again again again again again sms sms sms sms sms '}, 'optional_parameters': [{'length': 2, 'tag': 'user_message_reference', 'value': 92}, {'length': 16, 'tag': 'dest_subaddress', 'value': 'a0000410020601030303070802090403'}]}, 'header': {'command_status': 'ESME_ROK', 'command_length': 161, 'sequence_number': 24, 'command_id': 'deliver_sm'}} 61 | 62 | multi = MultipartMessage() 63 | self.assertEquals(multi.get_partial(), {'to_msisdn': '', 'from_msisdn': '', 'message': ''}) 64 | self.assertEquals(multi.get_completed(), None) 65 | self.assertEquals(multi.get_key(), None) 66 | multi.add_pdu(asif_2) 67 | self.assertEquals(multi.get_partial(), {'to_msisdn': '261xxx782943', 'from_msisdn': '261xxx720371', 'message': 'mns again again again again again again again again sms sms sms sms sms '}) 68 | self.assertEquals(multi.get_completed(), None) 69 | self.assertEquals(multi.get_key(), '261xxx720371_261xxx782943_26_2') 70 | multi.add_pdu(asif_1) 71 | self.assertEquals(multi.get_completed()['message'], 'I try to send sms testing vumi sms sms sms sms msm sms sms sms sms sms sms sms sms sms ssms sms smS sms sms sms sms sms sms sms sns sns sms sms sms sms smns again again again again again again again again sms sms sms sms sms ') 72 | self.assertEquals(multi.get_key(), '261xxx720371_261xxx782943_26_2') 73 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import collections 4 | from datetime import datetime, timedelta 5 | 6 | from smpp.esme import ESME 7 | from smpp.clickatell import clickatell_defaults 8 | from smpp import pdu 9 | from smpp.pdu_builder import SubmitSM 10 | import credentials_test 11 | try: 12 | import credentials_priv 13 | except: 14 | pass 15 | import binascii 16 | import re 17 | try: 18 | import json 19 | except: 20 | import simplejson as json 21 | 22 | from test.pdu import pdu_objects 23 | from test.pdu_hex import pdu_hex_strings 24 | from test import pdu_asserts 25 | from test import pdu_hex_asserts 26 | 27 | 28 | def unpack_hex(pdu_hex): 29 | """Unpack PDU hex string and return it as a dictionary""" 30 | return pdu.unpack_pdu(binascii.a2b_hex(hexclean(pdu_hex))) 31 | 32 | 33 | def hexclean(dirtyhex): 34 | """Remove whitespace, comments & newlines from hex string""" 35 | return re.sub(r'\s', '', re.sub(r'#.*\n', '\n', dirtyhex)) 36 | 37 | 38 | def prettydump(pdu_obj): 39 | """Unpack PDU dictionary and dump it as a JSON formatted string""" 40 | return json.dumps(pdu_obj, indent=4, sort_keys=True) 41 | 42 | 43 | def hex_to_named(dictionary): 44 | """ 45 | Recursive function to convert values in test dictionaries to 46 | their named counterparts that unpack_pdu returns 47 | """ 48 | clone = dictionary.copy() 49 | for key, value in clone.items(): 50 | if isinstance(value, collections.Mapping): 51 | clone[key] = hex_to_named(value) 52 | else: 53 | lookup_table = pdu.maps.get('%s_by_hex' % key) 54 | if lookup_table: 55 | # overwrite with mapped value or keep using 56 | # default if the dictionary key doesn't exist 57 | clone[key] = lookup_table.get("%.2d" % value, value) 58 | return clone 59 | 60 | 61 | def create_pdu_asserts(): 62 | pdu_index = 0 63 | for pdu_object in pdu_objects: 64 | pdu_index += 1 65 | pstr = "\n########################################\n" 66 | pstr += "pdu_json_" 67 | pstr += ('%010d' % pdu_index) 68 | pstr += " = '''" 69 | pstr += prettydump(pdu.unpack_pdu(pdu.pack_pdu(pdu_object))) 70 | pstr += "'''" 71 | print pstr 72 | 73 | 74 | def create_pdu_hex_asserts(): 75 | pdu_index = 0 76 | for pdu_hex in pdu_hex_strings: 77 | pdu_index += 1 78 | pstr = "\n########################################\n" 79 | pstr += "pdu_json_" 80 | pstr += ('%010d' % pdu_index) 81 | pstr += " = '''" 82 | pstr += prettydump(pdu.unpack_hex(pdu_hex)) 83 | pstr += "'''" 84 | print pstr 85 | 86 | 87 | # # :w|!python % > test/pdu_asserts.py 88 | # create_pdu_asserts() 89 | # quit() 90 | 91 | # # :w|!python % > test/pdu_hex_asserts.py 92 | # create_pdu_hex_asserts() 93 | # quit() 94 | 95 | 96 | class PduTestCase(unittest.TestCase): 97 | 98 | def setUp(self): 99 | pass 100 | 101 | def tearDown(self): 102 | pass 103 | 104 | def assertDictEquals(self, dictionary1, dictionary2, depth=[]): 105 | """ 106 | Recursive dictionary comparison, will fail if any keys and values 107 | in the two dictionaries don't match. Displays the key chain / depth 108 | and which parts of the two dictionaries didn't match. 109 | """ 110 | d1_keys = dictionary1.keys() 111 | d1_keys.sort() 112 | 113 | d2_keys = dictionary2.keys() 114 | d2_keys.sort() 115 | 116 | self.failUnlessEqual(d1_keys, d2_keys, 117 | "Dictionary keys do not match, %s vs %s" % ( 118 | d1_keys, d2_keys)) 119 | for key, value in dictionary1.items(): 120 | if isinstance(value, collections.Mapping): 121 | # go recursive 122 | depth.append(key) 123 | self.assertDictEquals(value, dictionary2[key], depth) 124 | else: 125 | self.failUnlessEqual(value, dictionary2[key], 126 | "Dictionary values do not match for key '%s' " 127 | "(%s vs %s) at depth: %s.\nDictionary 1: %s\n" 128 | "Dictionary 2: %s\n" % ( 129 | key, value, dictionary2[key], ".".join(depth), 130 | prettydump(dictionary1), prettydump(dictionary2))) 131 | 132 | def test_pack_unpack_pdu_objects(self): 133 | print '' 134 | """ 135 | Take a dictionary, pack and unpack it and dump it as JSON correctly 136 | """ 137 | pdu_index = 0 138 | for pdu_object in pdu_objects: 139 | pdu_index += 1 140 | padded_index = '%010d' % pdu_index 141 | print '...', padded_index 142 | str_eval = re.sub('null', 'None', getattr(pdu_asserts, 'pdu_json_' + padded_index)) 143 | self.assertEquals( 144 | pdu.unpack_pdu(pdu.pack_pdu(pdu_object)), 145 | eval(str_eval) 146 | ) 147 | 148 | def test_pack_unpack_pdu_hex_strings(self): 149 | print '' 150 | """ 151 | Read the hex data, clean it, and unpack it to JSON correctly 152 | """ 153 | pdu_index = 0 154 | for pdu_hex in pdu_hex_strings: 155 | pdu_index += 1 156 | padded_index = '%010d' % pdu_index 157 | print '...', padded_index 158 | str_eval = re.sub('null', 'None', eval('pdu_hex_asserts.pdu_json_' + padded_index)) 159 | self.assertEquals( 160 | unpack_hex(pdu_hex), 161 | eval(str_eval) 162 | ) 163 | 164 | def test_pack_unpack_performance(self): 165 | import platform 166 | if platform.python_implementation() == "PyPy": 167 | # Skip this test on pypy, because the JIT warmup time dominates. 168 | return 169 | print '' 170 | """ 171 | Pack & unpack 500 submit_sm PDUs in under 1 second 172 | """ 173 | submit_sm = { 174 | 'header': { 175 | 'command_length': 0, 176 | 'command_id': 'submit_sm', 177 | 'command_status': 'ESME_ROK', 178 | 'sequence_number': 0, 179 | }, 180 | 'body': { 181 | 'mandatory_parameters': { 182 | 'service_type': '', 183 | 'source_addr_ton': 1, 184 | 'source_addr_npi': 1, 185 | 'source_addr': '', 186 | 'dest_addr_ton': 1, 187 | 'dest_addr_npi': 1, 188 | 'destination_addr': '', 189 | 'esm_class': 0, 190 | 'protocol_id': 0, 191 | 'priority_flag': 0, 192 | 'schedule_delivery_time': '', 193 | 'validity_period': '', 194 | 'registered_delivery': 0, 195 | 'replace_if_present_flag': 0, 196 | 'data_coding': 0, 197 | 'sm_default_msg_id': 0, 198 | 'sm_length': 1, 199 | 'short_message': '', 200 | }, 201 | }, 202 | } 203 | start = datetime.now() 204 | for x in range(500): 205 | x += 1 206 | submit_sm['header']['sequence_number'] = x 207 | sm = 'testing: x = '+str(x)+'' 208 | submit_sm['body']['mandatory_parameters']['short_message'] = sm 209 | u = pdu.unpack_pdu(pdu.pack_pdu(submit_sm)) 210 | delta = datetime.now() - start 211 | print '... 500 pack & unpacks in:', delta 212 | self.assertTrue(delta < timedelta(seconds=1)) 213 | 214 | def test_pack_unpack_of_unicode(self): 215 | """ 216 | SMPP module should be able to pack & unpack unicode characters 217 | without a problem 218 | """ 219 | submit_sm = { 220 | 'header': { 221 | 'command_length': 67, 222 | 'command_id': 'submit_sm', 223 | 'command_status': 'ESME_ROK', 224 | 'sequence_number': 0, 225 | }, 226 | 'body': { 227 | 'mandatory_parameters': { 228 | 'service_type': '', 229 | 'source_addr_ton': 'international', 230 | 'source_addr_npi': 'unknown', 231 | 'source_addr': '', 232 | 'dest_addr_ton': 'international', 233 | 'dest_addr_npi': 'unknown', 234 | 'destination_addr': '', 235 | 'esm_class': 0, 236 | 'protocol_id': 0, 237 | 'priority_flag': 0, 238 | 'schedule_delivery_time': '', 239 | 'validity_period': '', 240 | 'registered_delivery': 0, 241 | 'replace_if_present_flag': 0, 242 | 'data_coding': 0, 243 | 'sm_default_msg_id': 0, 244 | 'sm_length': 34, 245 | 'short_message': u'Vumi says: أبن الشرموطة'.encode('utf-8'), 246 | }, 247 | }, 248 | } 249 | self.assertDictEquals( 250 | hex_to_named(submit_sm), 251 | pdu.unpack_pdu(pdu.pack_pdu(submit_sm)) 252 | ) 253 | 254 | def test_pack_unpack_of_ascii_and_unicode_8_16_32(self): 255 | """ 256 | SMPP module should be able to pack & unpack unicode characters 257 | without a problem 258 | """ 259 | submit_sm = { 260 | 'header': { 261 | 'command_length': 65, 262 | 'command_id': 'submit_sm', 263 | 'command_status': 'ESME_ROK', 264 | 'sequence_number': 0, 265 | }, 266 | 'body': { 267 | 'mandatory_parameters': { 268 | 'service_type': '', 269 | 'source_addr_ton': 'international', 270 | 'source_addr_npi': 'unknown', 271 | 'source_addr': '', 272 | 'dest_addr_ton': 'international', 273 | 'dest_addr_npi': 'unknown', 274 | 'destination_addr': '', 275 | 'esm_class': 0, 276 | 'protocol_id': 0, 277 | 'priority_flag': 0, 278 | 'schedule_delivery_time': '', 279 | 'validity_period': '', 280 | 'registered_delivery': 0, 281 | 'replace_if_present_flag': 0, 282 | 'data_coding': 0, 283 | 'sm_default_msg_id': 0, 284 | 'sm_length': 32, 285 | 'short_message': u'a \xf0\x20\u0373\u0020\u0433\u0020\u0533\u0020\u05f3\u0020\u0633\u0020\u13a3\u0020\u16a3 \U0001f090'.encode('utf-8'), 286 | }, 287 | }, 288 | } 289 | self.assertDictEquals( 290 | hex_to_named(submit_sm), 291 | pdu.unpack_pdu(pdu.pack_pdu(submit_sm)) 292 | ) 293 | 294 | def test_optional_param_length(self): 295 | # Variable length hex string. 296 | self.assertEqual( 297 | '04240000', pdu.encode_optional_parameter('message_payload', '')) 298 | self.assertEqual( 299 | '04240004deadbeef', 300 | pdu.encode_optional_parameter('message_payload', 'deadbeef')) 301 | 302 | # Fixed length integer. 303 | self.assertEqual( 304 | '020400020000', 305 | pdu.encode_optional_parameter('user_message_reference', 0)) 306 | self.assertEqual( 307 | '0204000201ff', 308 | pdu.encode_optional_parameter('user_message_reference', 511)) 309 | 310 | def test_encode_param_type_no_value(self): 311 | self.assertEqual(pdu.encode_param_type(None, 'integer'), None) 312 | self.assertEqual(pdu.encode_param_type(None, 'string'), None) 313 | self.assertEqual(pdu.encode_param_type(None, 'xstring'), None) 314 | self.assertEqual(pdu.encode_param_type(None, 'bitmask'), None) 315 | self.assertEqual(pdu.encode_param_type(None, 'hex'), None) 316 | 317 | def test_encode_param_type_integer(self): 318 | self.assertEqual(pdu.encode_param_type(0, 'integer'), '00') 319 | self.assertEqual(pdu.encode_param_type(1, 'integer'), '01') 320 | self.assertEqual(pdu.encode_param_type(255, 'integer'), 'ff') 321 | self.assertEqual(pdu.encode_param_type(256, 'integer'), '0100') 322 | 323 | self.assertEqual(pdu.encode_param_type(0, 'integer', min=2), '0000') 324 | self.assertEqual(pdu.encode_param_type(255, 'integer', min=2), '00ff') 325 | self.assertEqual(pdu.encode_param_type(256, 'integer', min=2), '0100') 326 | 327 | self.assertEqual(pdu.encode_param_type(255, 'integer', max=1), 'ff') 328 | self.assertRaises(ValueError, pdu.encode_param_type, 256, 'integer', max=1) 329 | 330 | def test_encode_param_type_string(self): 331 | self.assertEqual(pdu.encode_param_type('', 'string'), '00') 332 | self.assertEqual(pdu.encode_param_type('ABC', 'string'), '41424300') 333 | self.assertEqual(pdu.encode_param_type('ABC', 'string', max=4), '41424300') 334 | self.assertRaises( 335 | ValueError, pdu.encode_param_type, 'ABC', 'string', max=3) 336 | 337 | def test_encode_param_type_xstring(self): 338 | self.assertEqual(pdu.encode_param_type('', 'xstring'), '') 339 | self.assertEqual(pdu.encode_param_type('ABC', 'xstring'), '414243') 340 | self.assertEqual(pdu.encode_param_type('ABC', 'xstring', max=3), '414243') 341 | self.assertRaises( 342 | ValueError, pdu.encode_param_type, 'ABC', 'xstring', max=2) 343 | 344 | def test_ignore_invalid_null_after_short_message_field(self): 345 | """ 346 | At least one provider sends us an invalid deliver_sm PDU with a null 347 | byte after the short_message field. 348 | """ 349 | deliver_sm = { 350 | 'header': { 351 | 'command_length': 0, 352 | 'command_id': 'deliver_sm', 353 | 'command_status': 'ESME_ROK', 354 | 'sequence_number': 0, 355 | }, 356 | 'body': { 357 | 'mandatory_parameters': { 358 | 'service_type': '', 359 | 'source_addr_ton': 1, 360 | 'source_addr_npi': 1, 361 | 'source_addr': '', 362 | 'dest_addr_ton': 1, 363 | 'dest_addr_npi': 1, 364 | 'destination_addr': '', 365 | 'esm_class': 0, 366 | 'protocol_id': 0, 367 | 'priority_flag': 0, 368 | 'schedule_delivery_time': '', 369 | 'validity_period': '', 370 | 'registered_delivery': 0, 371 | 'replace_if_present_flag': 0, 372 | 'data_coding': 0, 373 | 'sm_default_msg_id': 0, 374 | 'sm_length': 1, 375 | 'short_message': 'test', 376 | }, 377 | }, 378 | } 379 | packed_pdu = pdu.pack_pdu(deliver_sm) 380 | unpacked_pdu = pdu.unpack_pdu(packed_pdu) 381 | unpacked_dodgy_pdu = pdu.unpack_pdu(packed_pdu + '\x00') 382 | self.assertEqual(unpacked_pdu, unpacked_dodgy_pdu) 383 | 384 | def test_validity_period(self): 385 | """ 386 | Should be able to pack and unpack a PDU with a valid validity_period. 387 | """ 388 | submit_sm = { 389 | 'header': { 390 | 'command_length': 67, 391 | 'command_id': 'submit_sm', 392 | 'command_status': 'ESME_ROK', 393 | 'sequence_number': 0, 394 | }, 395 | 'body': { 396 | 'mandatory_parameters': { 397 | 'service_type': '', 398 | 'source_addr_ton': 'international', 399 | 'source_addr_npi': 'unknown', 400 | 'source_addr': '', 401 | 'dest_addr_ton': 'international', 402 | 'dest_addr_npi': 'unknown', 403 | 'destination_addr': '', 404 | 'esm_class': 0, 405 | 'protocol_id': 0, 406 | 'priority_flag': 0, 407 | 'schedule_delivery_time': '', 408 | 'validity_period': '000001234567800R', 409 | 'registered_delivery': 0, 410 | 'replace_if_present_flag': 0, 411 | 'data_coding': 0, 412 | 'sm_default_msg_id': 0, 413 | 'sm_length': 18, 414 | 'short_message': 'Test Short Message', 415 | }, 416 | }, 417 | } 418 | self.assertEqual(pdu.unpack_pdu(pdu.pack_pdu(submit_sm)), submit_sm) 419 | 420 | 421 | class PduBuilderTestCase(unittest.TestCase): 422 | def test_submit_sm_message_too_long(self): 423 | short_message = '1234567890' * 26 424 | submit_sm = SubmitSM(5, short_message=short_message) 425 | self.assertRaises(ValueError, submit_sm.get_hex) 426 | 427 | 428 | if __name__ == '__main__': 429 | print '\n##########################################################\n' 430 | # deliv_sm_resp = DeliverSMResp(23) 431 | # print deliv_sm_resp.get_obj() 432 | # print deliv_sm_resp.get_hex() 433 | # enq_lnk = EnquireLink(7) 434 | # print enq_lnk.get_obj() 435 | # print enq_lnk.get_hex() 436 | # sub_sm = SubmitSM(5, short_message='testing testing') 437 | # print sub_sm.get_obj() 438 | # print sub_sm.get_hex() 439 | # sub_sm.add_message_payload('01020304') 440 | # print sub_sm.get_obj() 441 | # print sub_sm.get_hex() 442 | # print unpack_pdu(sub_sm.get_bin()) 443 | print '\n##########################################################\n' 444 | 445 | esme = ESME() 446 | esme.loadDefaults(clickatell_defaults) 447 | esme.loadDefaults(credentials_test.logica) 448 | print esme.defaults 449 | esme.bind_transmitter() 450 | print esme.state 451 | start = datetime.now() 452 | for x in range(1): 453 | esme.submit_sm( 454 | short_message='gobbledygook', 455 | destination_addr='555', 456 | ) 457 | print esme.state 458 | for x in range(1): 459 | esme.submit_multi( 460 | short_message='gobbledygook', 461 | dest_address=['444', '333'], 462 | ) 463 | print esme.state 464 | for x in range(1): 465 | esme.submit_multi( 466 | short_message='gobbledygook', 467 | dest_address=[ 468 | {'dest_flag': 1, 'destination_addr': '111'}, 469 | {'dest_flag': 2, 'dl_name': 'list22222'}, 470 | ], 471 | ) 472 | print esme.state 473 | delta = datetime.now() - start 474 | esme.disconnect() 475 | print esme.state 476 | print 'excluding binding ... time to send messages =', delta 477 | --------------------------------------------------------------------------------