├── messaging ├── test │ ├── __init__.py │ ├── mms-data │ │ ├── m.mms │ │ ├── BTMMS.MMS │ │ ├── NOWMMS.MMS │ │ ├── SIMPLE.MMS │ │ ├── iPhone.mms │ │ ├── TOMSLOT.MMS │ │ ├── openwave.mms │ │ ├── SEC-SGHS300M.mms │ │ ├── gallery2test.mms │ │ ├── projekt_exempel.mms │ │ ├── SonyEricssonT310-R201.mms │ │ ├── images_are_cut_off_debug.mms │ │ └── 27d0a048cd79555de05283a22372b0eb.mms │ ├── test_udh.py │ ├── test_wap.py │ ├── test_gsm_encoding.py │ ├── test_sms.py │ └── test_mms.py ├── __init__.py ├── sms │ ├── __init__.py │ ├── pdu.py │ ├── base.py │ ├── consts.py │ ├── wap.py │ ├── udh.py │ ├── deliver.py │ ├── submit.py │ └── gsm0338.py ├── mms │ ├── iterator.py │ ├── __init__.py │ └── message.py └── utils.py ├── packaging └── debian │ └── generic │ └── debian │ ├── compat │ ├── rules │ ├── README.Debian │ ├── copyright │ ├── control │ └── changelog ├── .gitignore ├── .travis.yml ├── doc ├── modules │ ├── sms │ │ ├── pdu.rst │ │ ├── base.rst │ │ ├── gsm0338.rst │ │ ├── submit.rst │ │ ├── deliver.rst │ │ ├── wap.rst │ │ └── udh.rst │ ├── mms │ │ ├── iterator.rst │ │ ├── mms_pdu.rst │ │ ├── message.rst │ │ └── wsp_pdu.rst │ └── utils.rst ├── index.rst ├── glossary.rst ├── tutorial │ ├── sms.rst │ └── mms.rst ├── Makefile └── conf.py ├── LICENSE ├── setup.py ├── Makefile ├── README ├── python-messaging.spec ├── CHANGELOG ├── resources └── pydump.py └── COPYING /messaging/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packaging/debian/generic/debian/compat: -------------------------------------------------------------------------------- 1 | 6 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | /build 4 | *.egg-info 5 | -------------------------------------------------------------------------------- /messaging/__init__.py: -------------------------------------------------------------------------------- 1 | # see LICENSE 2 | 3 | VERSION = (0, 5, 12) 4 | -------------------------------------------------------------------------------- /messaging/test/mms-data/m.mms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/m.mms -------------------------------------------------------------------------------- /messaging/test/mms-data/BTMMS.MMS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/BTMMS.MMS -------------------------------------------------------------------------------- /messaging/test/mms-data/NOWMMS.MMS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/NOWMMS.MMS -------------------------------------------------------------------------------- /messaging/test/mms-data/SIMPLE.MMS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/SIMPLE.MMS -------------------------------------------------------------------------------- /messaging/test/mms-data/iPhone.mms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/iPhone.mms -------------------------------------------------------------------------------- /messaging/test/mms-data/TOMSLOT.MMS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/TOMSLOT.MMS -------------------------------------------------------------------------------- /messaging/test/mms-data/openwave.mms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/openwave.mms -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - pip install . 6 | # command to run tests 7 | script: nosetests 8 | -------------------------------------------------------------------------------- /messaging/test/mms-data/SEC-SGHS300M.mms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/SEC-SGHS300M.mms -------------------------------------------------------------------------------- /messaging/test/mms-data/gallery2test.mms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/gallery2test.mms -------------------------------------------------------------------------------- /messaging/test/mms-data/projekt_exempel.mms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/projekt_exempel.mms -------------------------------------------------------------------------------- /messaging/test/mms-data/SonyEricssonT310-R201.mms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/SonyEricssonT310-R201.mms -------------------------------------------------------------------------------- /messaging/test/mms-data/images_are_cut_off_debug.mms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/images_are_cut_off_debug.mms -------------------------------------------------------------------------------- /messaging/test/mms-data/27d0a048cd79555de05283a22372b0eb.mms: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmarti/python-messaging/HEAD/messaging/test/mms-data/27d0a048cd79555de05283a22372b0eb.mms -------------------------------------------------------------------------------- /doc/modules/sms/pdu.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.sms.pdu` 2 | ========================== 3 | 4 | .. automodule:: messaging.sms.pdu 5 | 6 | Classes 7 | -------- 8 | 9 | .. autoclass:: Pdu 10 | :members: 11 | -------------------------------------------------------------------------------- /packaging/debian/generic/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | DEB_PYTHON_SYSTEM = pycentral 4 | 5 | include /usr/share/cdbs/1/rules/debhelper.mk 6 | include /usr/share/cdbs/1/class/python-distutils.mk 7 | -------------------------------------------------------------------------------- /doc/modules/sms/base.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.sms.base` 2 | ========================= 3 | 4 | .. automodule:: messaging.sms.base 5 | 6 | Classes 7 | -------- 8 | 9 | .. autoclass:: SmsBase 10 | :members: 11 | -------------------------------------------------------------------------------- /doc/modules/sms/gsm0338.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.sms.gsm0338` 2 | ============================ 3 | 4 | .. automodule:: messaging.sms.gsm0338 5 | 6 | Functions 7 | --------- 8 | 9 | .. autofunction:: is_gsm_text 10 | -------------------------------------------------------------------------------- /doc/modules/mms/iterator.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.mms.iterator` 2 | ============================= 3 | 4 | .. automodule:: messaging.mms.iterator 5 | 6 | Classes 7 | -------- 8 | 9 | .. autoclass:: PreviewIterator 10 | :members: 11 | -------------------------------------------------------------------------------- /doc/modules/sms/submit.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.sms.submit` 2 | =========================== 3 | 4 | .. automodule:: messaging.sms.submit 5 | 6 | Classes 7 | -------- 8 | 9 | .. autoclass:: SmsSubmit 10 | :show-inheritance: 11 | :members: 12 | -------------------------------------------------------------------------------- /messaging/sms/__init__.py: -------------------------------------------------------------------------------- 1 | # See LICENSE 2 | 3 | from messaging.sms.submit import SmsSubmit 4 | from messaging.sms.deliver import SmsDeliver 5 | from messaging.sms.gsm0338 import is_gsm_text 6 | 7 | __all__ = ["SmsSubmit", "SmsDeliver", "is_gsm_text"] 8 | -------------------------------------------------------------------------------- /doc/modules/sms/deliver.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.sms.deliver` 2 | ============================ 3 | 4 | .. automodule:: messaging.sms.deliver 5 | 6 | Classes 7 | -------- 8 | 9 | .. autoclass:: SmsDeliver 10 | :show-inheritance: 11 | :members: 12 | -------------------------------------------------------------------------------- /packaging/debian/generic/debian/README.Debian: -------------------------------------------------------------------------------- 1 | python-messaging for Debian 2 | --------------------------- 3 | 4 | Please see ./README for a description of the python-messaging software. 5 | 6 | -- Pablo Martí Gamboa Thu, 29 Jan 2009 08:37:28 +0200 7 | -------------------------------------------------------------------------------- /doc/modules/sms/wap.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.sms.wap` 2 | ======================== 3 | 4 | .. automodule:: messaging.sms.wap 5 | 6 | Functions 7 | --------- 8 | 9 | .. autofunction:: is_a_wap_push_notification 10 | 11 | .. autofunction:: extract_push_notification 12 | -------------------------------------------------------------------------------- /messaging/sms/pdu.py: -------------------------------------------------------------------------------- 1 | # see LICENSE 2 | 3 | 4 | class Pdu(object): 5 | 6 | def __init__(self, pdu, len_smsc, cnt=1, seq=1): 7 | self.pdu = pdu.upper() 8 | self.length = len(pdu) / 2 - len_smsc 9 | self.cnt = cnt 10 | self.seq = seq 11 | -------------------------------------------------------------------------------- /doc/modules/mms/mms_pdu.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.mms.mms_pdu` 2 | ============================ 3 | 4 | .. automodule:: messaging.mms.mms_pdu 5 | 6 | Classes 7 | -------- 8 | 9 | .. autoclass:: MMSEncoder 10 | :show-inheritance: 11 | :members: 12 | 13 | .. autoclass:: MMSDecoder 14 | :show-inheritance: 15 | :members: 16 | -------------------------------------------------------------------------------- /doc/modules/mms/message.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.mms.message` 2 | ============================ 3 | 4 | .. automodule:: messaging.mms.message 5 | 6 | Classes 7 | -------- 8 | 9 | .. autoclass:: MMSMessage 10 | :members: 11 | 12 | .. autoclass:: MMSMessagePage 13 | :members: 14 | 15 | .. autoclass:: DataPart 16 | :members: 17 | -------------------------------------------------------------------------------- /doc/modules/sms/udh.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.sms.udh` 2 | ======================== 3 | 4 | .. automodule:: messaging.sms.udh 5 | 6 | Classes 7 | -------- 8 | 9 | .. autoclass:: PortAddress 10 | :members: 11 | 12 | .. autoclass:: ConcatReference 13 | :members: 14 | 15 | .. autoclass:: UserDataHeader 16 | :members: 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /messaging/sms/base.py: -------------------------------------------------------------------------------- 1 | # see LICENSE 2 | 3 | 4 | class SmsBase(object): 5 | 6 | def __init__(self): 7 | self.udh = None 8 | self.number = None 9 | self.text = None 10 | self.fmt = None 11 | self.dcs = None 12 | self.pid = None 13 | self.csca = None 14 | self.type = None 15 | -------------------------------------------------------------------------------- /messaging/sms/consts.py: -------------------------------------------------------------------------------- 1 | # see LICENSE 2 | SEVENBIT_SIZE = 160 3 | EIGHTBIT_SIZE = 140 4 | UCS2_SIZE = 70 5 | SEVENBIT_MP_SIZE = SEVENBIT_SIZE - 7 6 | EIGHTBIT_MP_SIZE = EIGHTBIT_SIZE - 6 7 | UCS2_MP_SIZE = UCS2_SIZE - 3 8 | 9 | # address type 10 | UNKNOWN = 0 11 | INTERNATIONAL = 1 12 | NATIONAL = 2 13 | NETWORK_SPECIFIC = 3 14 | SUBSCRIBER = 4 15 | ALPHANUMERIC = 5 16 | ABBREVIATED = 6 17 | RESERVED = 7 18 | -------------------------------------------------------------------------------- /packaging/debian/generic/debian/copyright: -------------------------------------------------------------------------------- 1 | This package was debianized by Pablo Martí Gamboa on 2 | Thu, 22 Jan 2009 13:45:38 +0100. 3 | 4 | Upstream Author(s): 5 | 6 | 7 | 8 | 9 | License: 10 | 11 | GPLv2 12 | 13 | The Debian packaging is (C) 2009, Pablo Martí Gamboa and 14 | is licensed under the GPL, see `/usr/share/common-licenses/GPL'. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2004 Paul Hardwick 2 | Copyright (C) 2008 Warp Networks S.L. 3 | Copyright (C) 2008 Telefonica I+D 4 | Copyright (C) 2008 Francois Aucamp 5 | 6 | Imported for the wader project on 5 June 2008 by Pablo Martí 7 | Imported for the mobile-manager on 1 Oct 2008 by Roberto Majadas 8 | 9 | Copyright (C) 2008-2010 python-messaging developers 10 | 11 | See the COPYING file for the gory details of the GPLv2. 12 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to python-messaging's documentation! 2 | ============================================ 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | tutorial/sms 10 | tutorial/mms 11 | glossary 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`glossary` 19 | * :ref:`search` 20 | 21 | .. toctree:: 22 | :hidden: 23 | :glob: 24 | 25 | modules/* 26 | modules/sms/* 27 | modules/mms/* 28 | 29 | -------------------------------------------------------------------------------- /packaging/debian/generic/debian/control: -------------------------------------------------------------------------------- 1 | Source: python-messaging 2 | Section: net 3 | Priority: extra 4 | Maintainer: Pablo Martí Gamboa 5 | Build-Depends: cdbs (>= 0.4.49), debhelper (>= 5.0.38), python-central (>= 0.5.6) 6 | Standards-Version: 3.8.3 7 | XS-Python-Version: >= 2.5 8 | 9 | Package: python-messaging 10 | Architecture: all 11 | Depends: python (>= 2.5), ${python:Depends}, ${misc:Depends}, ${shlibs:Depends} 12 | XB-Python-Version: >= 2.5 13 | Description: A SMS encoding/decoding library 14 | -------------------------------------------------------------------------------- /doc/modules/mms/wsp_pdu.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.mms.wsp_pdu` 2 | ============================ 3 | 4 | .. automodule:: messaging.mms.wsp_pdu 5 | 6 | Functions 7 | --------- 8 | 9 | .. autofunction:: get_header_field_names 10 | 11 | .. autofunction:: get_well_known_parameters 12 | 13 | Classes 14 | -------- 15 | 16 | .. autoclass:: DecodeError 17 | :show-inheritance: 18 | 19 | .. autoclass:: EncodeError 20 | :show-inheritance: 21 | 22 | .. autoclass:: Decoder 23 | :members: 24 | 25 | .. autoclass:: Encoder 26 | :members: 27 | -------------------------------------------------------------------------------- /doc/modules/utils.rst: -------------------------------------------------------------------------------- 1 | :mod:`messaging.utils` 2 | ========================= 3 | 4 | .. automodule:: messaging.utils 5 | 6 | Classes 7 | ------- 8 | 9 | .. autoclass:: FixedOffset 10 | :members: 11 | 12 | 13 | Functions 14 | --------- 15 | 16 | .. autofunction:: bytes_to_str 17 | 18 | .. autofunction:: to_array 19 | 20 | .. autofunction:: to_bytes 21 | 22 | .. autofunction:: swap 23 | 24 | .. autofunction:: swap_number 25 | 26 | .. autofunction:: clean_number 27 | 28 | .. autofunction:: encode_str 29 | 30 | .. autofunction:: pack_8bits_to_7bits 31 | 32 | .. autofunction:: pack_8bits_to_8bit 33 | 34 | .. autofunction:: pack_8bits_to_ucs2 35 | 36 | .. autofunction:: unpack_msg 37 | 38 | .. autofunction:: timedelta_to_relative_validity 39 | 40 | .. autofunction:: datetime_to_absolute_validity 41 | 42 | -------------------------------------------------------------------------------- /messaging/test/test_udh.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from messaging.sms.udh import UserDataHeader 4 | from messaging.utils import to_array 5 | 6 | 7 | class TestUserDataHeader(unittest.TestCase): 8 | 9 | def test_user_data_header(self): 10 | data = to_array("08049f8e020105040b8423f0") 11 | udh = UserDataHeader.from_bytes(data) 12 | 13 | self.assertEqual(udh.concat.seq, 1) 14 | self.assertEqual(udh.concat.cnt, 2) 15 | self.assertEqual(udh.concat.ref, 40846) 16 | self.assertEqual(udh.ports.dest_port, 2948) 17 | self.assertEqual(udh.ports.orig_port, 9200) 18 | 19 | data = to_array("0003190201") 20 | udh = UserDataHeader.from_bytes(data) 21 | 22 | self.assertEqual(udh.concat.seq, 1) 23 | self.assertEqual(udh.concat.cnt, 2) 24 | self.assertEqual(udh.concat.ref, 25) 25 | -------------------------------------------------------------------------------- /messaging/sms/wap.py: -------------------------------------------------------------------------------- 1 | # See LICENSE 2 | 3 | from array import array 4 | 5 | from messaging.mms.mms_pdu import MMSDecoder 6 | 7 | 8 | def is_a_wap_push_notification(s): 9 | if not isinstance(s, str): 10 | raise TypeError("data must be an array.array serialised to string") 11 | 12 | data = array("B", s) 13 | 14 | try: 15 | return data[1] == 0x06 16 | except IndexError: 17 | return False 18 | 19 | 20 | def extract_push_notification(s): 21 | data = array("B", s) 22 | 23 | wap_push, offset = data[1:3] 24 | assert wap_push == 0x06 25 | 26 | offset += 3 27 | data = data[offset:] 28 | 29 | # XXX: Not all WAP pushes are MMS 30 | return MMSDecoder().decode_data(data) 31 | 32 | 33 | def is_mms_notification(push): 34 | # XXX: Pretty poor, but until we decode generic WAP pushes 35 | # it will have to suffice. Ideally we would read the 36 | # content-type from the WAP push header and test 37 | return (push.headers.get('From') is not None and 38 | push.headers.get('Content-Location') is not None) 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import sys 3 | from messaging import VERSION 4 | 5 | extra = {} 6 | if sys.version_info >= (3,): 7 | extra['use_2to3'] = True 8 | 9 | setup(name="python-messaging", 10 | version='%s.%s.%s' % VERSION, 11 | description='SMS/MMS encoder/decoder', 12 | license=open('COPYING').read(), 13 | packages=find_packages(), 14 | install_requires=['nose'], 15 | zip_safe=True, 16 | test_suite='nose.collector', 17 | classifiers=[ 18 | 'Development Status :: 4 - Beta', 19 | 'License :: OSI Approved :: GNU General Public License (GPL)', 20 | 'Natural Language :: English', 21 | 'Operating System :: POSIX :: Linux', 22 | 'Programming Language :: Python', 23 | 'Programming Language :: Python :: 2.5', 24 | 'Programming Language :: Python :: 2.6', 25 | 'Programming Language :: Python :: 2.7', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.1', 28 | 'Programming Language :: Python :: 3.2', 29 | 'Topic :: Communications :: Telephony', 30 | ], 31 | **extra 32 | ) 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | 3 | VERSION := $(shell python -c 'from messaging import VERSION; print "%s.%s.%s" % VERSION') 4 | SOURCES := $(shell rpmbuild --eval '%{_topdir}' 2>/dev/null)/SOURCES 5 | PMV := python-messaging-$(VERSION) 6 | 7 | all: 8 | @echo Usage: make deb \[TARGET=ubuntu-lucid\] \| rpm 9 | 10 | test: 11 | nosetests -v -w . messaging/test 12 | 13 | rpm: 14 | @if [ ! -d $(SOURCES) ] ;\ 15 | then\ 16 | echo 'SOURCES does not exist, are you running on a non RPM based system?';\ 17 | exit 1;\ 18 | fi 19 | 20 | tar -zcvf $(SOURCES)/$(PMV).tar.gz --exclude=.git --transform="s/^\./$(PMV)/" . 21 | rpmbuild -ba python-messaging.spec 22 | 23 | deb: 24 | @if [ ! -d /var/lib/dpkg ] ;\ 25 | then\ 26 | echo 'Debian package directory does not exist, are you running on a non Debian based system?';\ 27 | exit 1;\ 28 | fi 29 | 30 | @if [ -d packaging/debian/$(TARGET)/debian ] ;\ 31 | then\ 32 | PKGSOURCE=$(TARGET);\ 33 | else\ 34 | PKGSOURCE=generic;\ 35 | fi;\ 36 | tar -C packaging/debian/$$PKGSOURCE -cf - debian | tar -xf - 37 | 38 | @if ! head -1 debian/changelog | grep -q $(VERSION) ;\ 39 | then\ 40 | echo Changelog and package version are different;\ 41 | exit 1;\ 42 | fi 43 | 44 | dpkg-buildpackage -rfakeroot 45 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | What is python-messaging? 2 | ========================= 3 | 4 | A SMS/MMS encoder/decoder written 100% in Python. 5 | 6 | History 7 | ======= 8 | 9 | Originally written by Paul Hardwick with some bits from Dave Berkeley's 10 | pysms. It was imported in the Wader and MobileManager projects and quickly 11 | became clear that a joint effort would be more beneficial for everyone. 12 | 13 | In 2010, the superb python-mms package by Francois Aucamp was merged into 14 | python-messaging to provide a complete SMS/MMS encoder/decoder. 15 | 16 | In 2011, as part of a license clarification, Francis Aucamp was contacted 17 | and asked if it were possible to relicence his MMS portions as GPLv2, he 18 | responded: 19 | "feel free to re-license the parts of the python-mms code you are using in 20 | your python-messaging project to GPL v2; you have my full consent." 21 | 22 | SMS Features 23 | ============ 24 | 25 | * Supports 7bit, 8bit and UCS2 encodings 26 | * Multipart encoding/decoding 27 | * Status report encoding/decoding 28 | * Relative validity 29 | * Alphanumeric address decoding 30 | * Supports python 2.5 up to 3.2 31 | * Tests 32 | 33 | 34 | MMS Features 35 | ============ 36 | 37 | * SMIL support 38 | * Main formats supported: jpg, gif, arm, 3gp, midi, etc. 39 | * Supports MMS 1.0-1.4 decoding/encoding 40 | * Supports python 2.5 up to 3.2 41 | * Tests 42 | -------------------------------------------------------------------------------- /doc/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | ======== 4 | Glossary 5 | ======== 6 | 7 | .. glossary:: 8 | :sorted: 9 | 10 | MMS 11 | Multimedia Messaging Service, or MMS, is a standard way to send 12 | messages that include multimedia content to and from mobile 13 | phones. It extends the core SMS (Short Message Service) capability 14 | which only allowed exchange of text messages up to 160 characters 15 | in length. 16 | 17 | MMSC 18 | A "store and forward" server for MMS. If the recipient is in another 19 | network, the MMS will be forwarded to the recipient's carrier using 20 | the Internet. 21 | 22 | SMS 23 | Short Message Service (SMS) is the text communication service 24 | component of phone, web or mobile communication systems, using 25 | standardized communications protocols that allow the exchange 26 | of short text messages (up to 160 characters) between fixed line 27 | or mobile phone devices. 28 | 29 | WAP 30 | Wireless Application Protocol (WAP) is an open international standard 31 | for application-layer network communications in a wireless-communication 32 | environment. Most use of WAP involves accessing the mobile web from a 33 | mobile phone or from a PDA. 34 | 35 | PDU 36 | A PDU is a sequence of bytes used in telecommunications to convey 37 | information from one host/device to another. 38 | -------------------------------------------------------------------------------- /python-messaging.spec: -------------------------------------------------------------------------------- 1 | %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} 2 | 3 | Name: python-messaging 4 | Version: %(%{__python} -c 'from messaging import VERSION; print "%s.%s.%s" % VERSION') 5 | Release: 1%{?dist} 6 | Summary: SMS encoder/decoder library 7 | License: GPL 8 | Group: Development 9 | Source: %{name}-%{version}.tar.gz 10 | BuildRoot: %{_tmppath}/%{name}-buildroot 11 | BuildArch: noarch 12 | 13 | BuildRequires: python-devel 14 | %if 0%{?fedora} >= 8 15 | BuildRequires: python-setuptools-devel 16 | %else 17 | BuildRequires: python-setuptools 18 | %endif 19 | 20 | %description 21 | Pure python SMS encoder/decoder library 22 | 23 | %prep 24 | %setup -q -n %{name}-%{version} 25 | 26 | %build 27 | %{__python} -c 'import setuptools; execfile("setup.py")' build 28 | 29 | %install 30 | [ "%{buildroot}" != "/" ] && rm -rf %{buildroot} 31 | %{__python} -c 'import setuptools; execfile("setup.py")' install -O1 --skip-build --root %{buildroot} --prefix=%{_prefix} 32 | 33 | %clean 34 | [ "%{buildroot}" != "/" ] && rm -rf %{buildroot} 35 | 36 | %files 37 | %{python_sitelib} 38 | %defattr(-,root,root,-) 39 | %doc README 40 | 41 | %changelog 42 | 43 | * Tue Jan 24 2012 - Andrew Bird - 0.5.12 44 | - New release 45 | 46 | * Tue Aug 30 2011 - Andrew Bird - 0.5.11 47 | - New release 48 | 49 | * Mon Jun 06 2011 - Andrew Bird - 0.5.10 50 | - Initial release - Spec file tested on Fedora 14 / 15 and OpenSUSE 11.4 51 | -------------------------------------------------------------------------------- /packaging/debian/generic/debian/changelog: -------------------------------------------------------------------------------- 1 | python-messaging (0.5.12-1) lucid; urgency=low 2 | 3 | * New upstream version 4 | 5 | -- Andrew Bird Tue, 24 Jan 2012 13:50:24 +0000 6 | 7 | python-messaging (0.5.11-1) lucid; urgency=low 8 | 9 | * New upstream version 10 | 11 | -- Andrew Bird Tue, 30 Aug 2011 19:12:02 +0100 12 | 13 | python-messaging (0.5.10-1) lucid; urgency=low 14 | 15 | * New upstream version 16 | 17 | -- Andrew Bird Tue, 01 Mar 2011 12:52:00 +0000 18 | 19 | python-messaging (0.5.9-1) lucid; urgency=low 20 | 21 | * New upstream version 22 | 23 | -- Pablo Martí Gamboa Wed, 28 Jul 2010 09:12:45 +0200 24 | 25 | python-messaging (0.5-1) lucid; urgency=low 26 | 27 | * New upstream version 28 | 29 | -- Andrew Bird Fri, 18 Jun 2010 14:36:15 +0100 30 | 31 | python-messaging (0.4-2) lucid; urgency=low 32 | 33 | * Packaging tweak 34 | 35 | -- Andrew Bird Tue, 08 Jun 2010 14:19:45 +0100 36 | 37 | python-messaging (0.4-1) lucid; urgency=low 38 | 39 | * New upstream version 40 | 41 | -- Andrew Bird Mon, 07 Jun 2010 18:25:48 +0100 42 | 43 | python-messaging (0.3-1) lucid; urgency=low 44 | 45 | * New upstream version 46 | 47 | -- Pablo Martí Gamboa Sun, 18 Apr 2010 10:02:45 +0200 48 | 49 | python-messaging (0.2-1) lucid; urgency=low 50 | 51 | * New upstream version 52 | 53 | -- Pablo Martí Gamboa Tue, 01 Sep 2009 12:52:23 +0200 54 | 55 | python-messaging (0.1-1) intrepid; urgency=low 56 | 57 | * New upstream version 58 | 59 | -- Pablo Martí Gamboa Tue, 03 Jan 2009 14:49:18 +0100 60 | 61 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | ++++++++++++++++++++++++++++++++++++++++++++++ 2 | python-messaging-0.5.9 3 | Overview of changes since python-messaging-0.5 4 | ++++++++++++++++++++++++++++++++++++++++++++++ 5 | 6 | This is an interim release of python-messaging. List of changes: 7 | 8 | * See Git log for details 9 | 10 | 11 | ++++++++++++++++++++++++++++++++++++++++++++++ 12 | python-messaging-0.5 13 | Overview of changes since python-messaging-0.4 14 | ++++++++++++++++++++++++++++++++++++++++++++++ 15 | 16 | This is a new major stable release of python-messaging. List of changes: 17 | 18 | * Py3K fixes 19 | * GSM 7bit encoding fixes 20 | * Additional testing 21 | 22 | 23 | ++++++++++++++++++++++++++++++++++++++++++++++ 24 | python-messaging-0.4 25 | Overview of changes since python-messaging-0.3 26 | ++++++++++++++++++++++++++++++++++++++++++++++ 27 | 28 | This is a new major stable release of python-messaging. List of changes: 29 | 30 | * Py3K support 31 | * GSM 7bit decoding fixes 32 | 33 | 34 | ++++++++++++++++++++++++++++++++++++++++++++++ 35 | python-messaging-0.3 36 | Overview of changes since python-messaging-0.2 37 | ++++++++++++++++++++++++++++++++++++++++++++++ 38 | 39 | This is a new major stable release of python-messaging. List of changes: 40 | 41 | * API has changed, decode_pdu returns a dictionary now 42 | * SMS status report encoding 43 | * SMS status report decoding by the MobileManager project 44 | * Decoder completely revamped (code way cleaner) 45 | * Support for alphanumeric addresses contributed by Andrew Bird 46 | * Time offset fix by Andrew Bird 47 | * Debian package migrated to pycentral 48 | * Switched to distribute 49 | 50 | 51 | ++++++++++++++++++++++++++++++++++++++++++++++ 52 | python-messaging-0.2 53 | Overview of changes since python-messaging-0.1 54 | ++++++++++++++++++++++++++++++++++++++++++++++ 55 | 56 | This is a new major stable release of python-messaging. List of changes: 57 | 58 | * Fix the case where there is no SMSC 59 | * Some multi-part fixes pulled from mobile-manager 60 | * Test coverage increased 61 | * Docstrings updated to rst 62 | * Some packaging fixes 63 | * Some small code simplifications 64 | 65 | python-messaging-0.1 66 | ++++++++++++++++++++ 67 | 68 | * Initial release 69 | -------------------------------------------------------------------------------- /messaging/sms/udh.py: -------------------------------------------------------------------------------- 1 | # See LICENSE 2 | 3 | 4 | class PortAddress(object): 5 | 6 | def __init__(self, dest_port, orig_port, eight_bits): 7 | self.dest_port = dest_port 8 | self.orig_port = orig_port 9 | self.eight_bits = eight_bits 10 | 11 | def __repr__(self): 12 | args = (self.dest_port, self.orig_port) 13 | return "" % args 14 | 15 | 16 | class ConcatReference(object): 17 | 18 | def __init__(self, ref, cnt, seq, eight_bits): 19 | self.ref = ref 20 | self.cnt = cnt 21 | self.seq = seq 22 | self.eight_bits = eight_bits 23 | 24 | def __repr__(self): 25 | args = (self.ref, self.cnt, self.seq) 26 | return "" % args 27 | 28 | 29 | class UserDataHeader(object): 30 | 31 | def __init__(self): 32 | self.concat = None 33 | self.ports = None 34 | self.headers = {} 35 | 36 | def __repr__(self): 37 | args = (self.headers, self.concat, self.ports) 38 | return "" % args 39 | 40 | @classmethod 41 | def from_status_report_ref(cls, ref): 42 | udh = cls() 43 | udh.concat = ConcatReference(ref, 0, 0, True) 44 | return udh 45 | 46 | @classmethod 47 | def from_bytes(cls, data): 48 | udh = cls() 49 | while len(data): 50 | iei = data.pop(0) 51 | ie_len = data.pop(0) 52 | ie_data = data[:ie_len] 53 | data = data[ie_len:] 54 | udh.headers[iei] = ie_data 55 | 56 | if iei == 0x00: 57 | # process SM concatenation 8bit ref. 58 | ref, cnt, seq = ie_data 59 | udh.concat = ConcatReference(ref, cnt, seq, True) 60 | 61 | if iei == 0x08: 62 | # process SM concatenation 16bit ref. 63 | ref = ie_data[0] << 8 | ie_data[1] 64 | cnt = ie_data[2] 65 | seq = ie_data[3] 66 | udh.concat = ConcatReference(ref, cnt, seq, False) 67 | 68 | elif iei == 0x04: 69 | # process App port addressing 8bit 70 | dest_port, orig_port = ie_data 71 | udh.ports = PortAddress(dest_port, orig_port, False) 72 | 73 | elif iei == 0x05: 74 | # process App port addressing 16bit 75 | dest_port = ie_data[0] << 8 | ie_data[1] 76 | orig_port = ie_data[2] << 8 | ie_data[3] 77 | udh.ports = PortAddress(dest_port, orig_port, False) 78 | 79 | return udh 80 | -------------------------------------------------------------------------------- /messaging/mms/iterator.py: -------------------------------------------------------------------------------- 1 | # This library is free software. 2 | # 3 | # It was originally distributed under the terms of the GNU Lesser 4 | # General Public License Version 2. 5 | # 6 | # python-messaging opts to apply the terms of the ordinary GNU 7 | # General Public License v2, as permitted by section 3 of the LGPL 8 | # v2.1. This re-licensing allows the entirety of python-messaging to 9 | # be distributed according to the terms of GPL-2. 10 | # 11 | # See the COPYING file included in this archive 12 | # 13 | # The docstrings in this module contain epytext markup; API documentation 14 | # may be created by processing this file with epydoc: http://epydoc.sf.net 15 | """Iterator with "value preview" capability.""" 16 | 17 | 18 | class PreviewIterator(object): 19 | """An ``iter`` wrapper class providing a "previewable" iterator. 20 | 21 | This "preview" functionality allows the iterator to return successive 22 | values from its ``iterable`` object, without actually mvoving forward 23 | itself. This is very usefuly if the next item(s) in an iterator must 24 | be used for something, after which the iterator should "undo" those 25 | read operations, so that they can be read again by another function. 26 | 27 | From the user point of view, this class supersedes the builtin iter() 28 | function: like iter(), it is called as PreviewIter(iterable). 29 | """ 30 | def __init__(self, data): 31 | self._it = iter(data) 32 | self._cached_values = [] 33 | self._preview_pos = 0 34 | 35 | def __iter__(self): 36 | return self 37 | 38 | def next(self): 39 | self.reset_preview() 40 | if len(self._cached_values) > 0: 41 | return self._cached_values.pop(0) 42 | else: 43 | return self._it.next() 44 | 45 | def preview(self): 46 | """ 47 | Return the next item in the ``iteratable`` object 48 | 49 | But it does not modify the actual iterator (i.e. do not 50 | intefere with :func:`next`. 51 | 52 | Successive calls to :func:`preview` will return successive values from 53 | the ``iterable`` object, exactly in the same way :func:`next` does. 54 | 55 | However, :func:`preview` will always return the next item from 56 | ``iterable`` after the item returned by the previous :func:`preview` 57 | or :func:`next` call, whichever was called the most recently. 58 | To force the "preview() iterator" to synchronize with the "next() 59 | iterator" (without calling :func:`next`), use :func:`reset_preview`. 60 | """ 61 | if self._preview_pos < len(self._cached_values): 62 | value = self._cached_values[self._preview_pos] 63 | else: 64 | value = self._it.next() 65 | self._cached_values.append(value) 66 | 67 | self._preview_pos += 1 68 | return value 69 | 70 | def reset_preview(self): 71 | self._preview_pos = 0 72 | -------------------------------------------------------------------------------- /messaging/mms/__init__.py: -------------------------------------------------------------------------------- 1 | # This library is free software. 2 | # 3 | # It was originally distributed under the terms of the GNU Lesser 4 | # General Public License Version 2. 5 | # 6 | # python-messaging opts to apply the terms of the ordinary GNU 7 | # General Public License v2, as permitted by section 3 of the LGPL 8 | # v2.1. This re-licensing allows the entirety of python-messaging to 9 | # be distributed according to the terms of GPL-2. 10 | # 11 | # See the COPYING file included in this archive 12 | # 13 | # Copyright (C) 2007 Francois Aucamp 14 | # 15 | """ 16 | Multimedia Messaging Service (MMS) library 17 | 18 | The :mod:`messaging.mms` module provides several classes for the creation 19 | and manipulation of MMS messages (multimedia messages) used in mobile 20 | devices such as cellular telephones. 21 | 22 | Multimedia Messaging Service (MMS) is a messaging service for the mobile 23 | environment standardized by the WAP Forum and 3GPP. To the end-user MMS is 24 | very similar to the text-based Short Message Service (SMS): it provides 25 | automatic immediate delivery for user-created content from device to device. 26 | 27 | In addition to text, however, MMS messages can contain multimedia content such 28 | as still images, audio clips and video clips, which are binded together 29 | into a "mini presentation" (or slideshow) that controls for example, the order 30 | in which images are to appear on the screen, how long they will be displayed, 31 | when an audio clip should be played, etc. Furthermore, MMS messages do not have 32 | the 160-character limit of SMS messages. 33 | 34 | An MMS message is a multimedia presentation in one entity; it is not a text 35 | file with attachments. 36 | 37 | This library enables the creation of MMS messages with full support for 38 | presentation layout, and multimedia data parts such as JPEG, GIF, AMR, MIDI, 39 | 3GP, etc. It also allows the decoding and unpacking of received MMS messages. 40 | 41 | @version: 0.2 42 | @author: Francois Aucamp C{} 43 | @license: GNU General Public License, version 2 44 | @note: References used in the code and this document: 45 | 46 | .. [1] MMS Conformance Document version 2.0.0, 6 February 2002 47 | U{www.bogor.net/idkf/bio2/mobile-docs/mms_conformance_v2_0_0.pdf} 48 | 49 | .. [2] Forum Nokia, "How To Create MMS Services, Version 4.0" 50 | U{http://forum.nokia.com/info/sw.nokia.com/id/a57a4f20-b7f2-475b-b426-19eff18a5afb/How_To_Create_MMS_Services_v4_0_en2.pdf.html} 51 | 52 | .. [3] Wap Forum/Open Mobile ALliance, "WAP-206 MMS Client Transactions" 53 | U{http://www.openmobilealliance.org/tech/affiliates/LicenseAgreement.asp?DocName=/wap/wap-206-mmsctr-20020115-a.pdf} 54 | 55 | .. [4] Wap Forum/Open Mobile Alliance, "WAP-209 MMS Encapsulation Protocol" 56 | U{http://www.openmobilealliance.org/tech/affiliates/LicenseAgreement.asp?DocName=/wap/wap-209-mmsencapsulation-20020105-a.pdf} 57 | 58 | .. [5] Wap Forum/Open Mobile Alliance, "WAP-230 Wireless Session Protocol Specification" 59 | U{http://www.openmobilealliance.org/tech/affiliates/LicenseAgreement.asp?DocName=/wap/wap-230-wsp-20010705-a.pdf} 60 | 61 | .. [6] IANA: "Character Sets" 62 | U{http://www.iana.org/assignments/character-sets} 63 | """ 64 | -------------------------------------------------------------------------------- /doc/tutorial/sms.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | SMS tutorial 3 | ============ 4 | 5 | Features 6 | ++++++++ 7 | 8 | python-messaging contains in :mod:`messaging.sms` a full featured 9 | :term:`SMS` encoder/decoder that should fulfill your needs. Some of 10 | its features: 11 | 12 | - 7bit/8bit/16bit encoding/decoding: Decode messages from your Chinese 13 | friends, decode a :term:`WAP` PUSH notification or encode a UCS2 14 | message with a `Haiku`_ for your Japanese relatives. 15 | - Encode/decode `concatenated SMS`_ 16 | - Set SMS validity: (relative, absolute) 17 | - Set SMS class: (0-3) 18 | - Encode/decode read reports 19 | - Decode alphanumeric senders 20 | - Pythonic API: We have strived to design an API that will make feel 21 | Pythonistas right at home! 22 | 23 | .. _Haiku: http://en.wikipedia.org/wiki/Haiku 24 | .. _concatenated SMS: http://en.wikipedia.org/wiki/Concatenated_SMS 25 | 26 | Encoding 27 | ++++++++ 28 | 29 | Single part vs Multipart 30 | ~~~~~~~~~~~~~~~~~~~~~~~~ 31 | 32 | How to encode a single part SMS ready to be sent:: 33 | 34 | from messaging.sms import SmsSubmit 35 | 36 | sms = SmsSubmit("+44123231231", "hey how's it going?") 37 | pdu = sms.to_pdu()[0] 38 | 39 | print pdu.length, pdu.pdu 40 | 41 | 42 | How to encode a concatenated SMS ready to be sent:: 43 | 44 | from messaging.sms import SmsSubmit 45 | 46 | sms = SmsSubmit("+44123231231", "hey " * 50) 47 | for pdu in sms.to_pdu(): 48 | print pdu.length, pdu.pdu 49 | 50 | 51 | Setting class 52 | ~~~~~~~~~~~~~ 53 | 54 | Setting the SMS class (0-3) is a no brainer:: 55 | 56 | from messaging.sms import SmsSubmit 57 | 58 | sms = SmsSubmit("+44123231231", "hey how's it going?") 59 | sms.class = 0 60 | pdu = sms.to_pdu()[0] 61 | 62 | print pdu.length, pdu.pdu 63 | 64 | 65 | Setting validity 66 | ~~~~~~~~~~~~~~~~ 67 | 68 | Validity can be either absolute, or relative. In order to provide 69 | a pythonic API, we are using :class:`datetime.datetime` and 70 | :class:`datetime.timedelta` objects respectively. 71 | 72 | Setting absolute validity:: 73 | 74 | from datetime import datetime 75 | from messaging.sms import SmsSubmit 76 | 77 | sms = SmsSubmit("+44123231231", "this SMS will auto-destroy in 4 months) 78 | sms.validity = datetime(2010, 12, 31, 23, 59, 59) 79 | pdu = sms.to_pdu()[0] 80 | 81 | print pdu.length, pdu.pdu 82 | 83 | 84 | Setting relative validity:: 85 | 86 | from datetime import timedelta 87 | from messaging.sms import SmsSubmit 88 | 89 | sms = SmsSubmit("+44123231231", "this SMS will auto-destroy in 5 hours") 90 | sms.validity = timedelta(hours=5) 91 | pdu = sms.to_pdu()[0] 92 | 93 | print pdu.length, pdu.pdu 94 | 95 | 96 | Decoding 97 | ++++++++ 98 | 99 | term:`PDU` decoding is really simple with :class:`~messaging.sms.SmsDeliver`:: 100 | 101 | from messaging.sms import SmsDeliver 102 | 103 | pdu = "0791447758100650040C914497726247010000909010711423400A2050EC468B81C4733A" 104 | sms = SmsDeliver(pdu) 105 | 106 | print sms.data 107 | # {'csca': '+447785016005', 'type': None, 108 | # 'date': datetime.datetime(2009, 9, 1, 16, 41, 32), 109 | # 'text': u' 1741 bst', 'fmt': 0, 'pid': 0, 110 | # 'dcs': 0, 'number': '+447927267410'} 111 | 112 | Apart from the pdu, the :py:meth:`messaging.sms.SmsDeliver.__init__` accepts a 113 | second parameter (`strict`, which defaults to True). If False, it will decode 114 | incomplete (odd size) PDUs. 115 | 116 | Sending 117 | +++++++ 118 | 119 | This is how you would send a SMS with a modem or a 3G device on Linux, the 120 | following code assumes that the device is already authenticated and 121 | registered:: 122 | 123 | import serial 124 | 125 | from messaging.sms import SmsSubmit 126 | 127 | def send_text(number, text, path='/dev/ttyUSB0'): 128 | # encode the SMS 129 | # note how I get the first returned element, this does 130 | # not deal with concatenated SMS. 131 | pdu = SmsSubmit(number, text).to_pdu()[0] 132 | # open the modem port (assumes Linux) 133 | ser = serial.Serial(path, timeout=1) 134 | # write the PDU length and wait 1 second till the 135 | # prompt appears (a more robust implementation 136 | # would wait till the prompt appeared) 137 | ser.write('AT+CMGS=%d\r' % pdu.length) 138 | print ser.readlines() 139 | # write the PDU and send a Ctrl+z escape 140 | ser.write('%s\x1a' % pdu.pdu) 141 | ser.close() 142 | 143 | send_text('655234567', 'hey how are you?') 144 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-messaging.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-messaging.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-messaging" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-messaging" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /resources/pydump.py: -------------------------------------------------------------------------------- 1 | # Use this file to dump a python string into a format that wireshark's 2 | # text2pcap can interpret and convert to a pcap that wireshark can read 3 | # 4 | # Suggested workflow: 5 | # edit 'data' string to contain the interesting data 6 | # python pydump.py > wsp.dmp 7 | # text2pcap -o hex -u9200,50 wsp.dmp wsp.pcap 8 | # wireshark wsp.pcap 9 | # export plain text format from wireshark 10 | 11 | from struct import unpack 12 | 13 | # generic WAP Push 14 | data = '\x01\x06\x0b\x03\xae\x81\xea\xc3\x95\x8d\x01\xa2\xb4\x84\x03\x05j\n Vodafone\x00E\xc6\x0c\x03wap.meincallya.de/\x00\x08\x01\x03Zum kostenlosen Portal "Mein\x00\x83\x00\x03" - einfach auf den folgenden Link klicken oder die Seite direkt aufrufen. Ihr\x00\x83\x00\x03 Team\x00\x01\x01' 15 | 16 | """ 17 | No. Time Source Destination Protocol Info 18 | 1 0.000000 1.1.1.1 2.2.2.2 WSP WSP Push (0x06) (WBXML 1.3, Public ID: "-//WAPFORUM//DTD SI 1.0//EN (Service Indication 1.0)") 19 | 20 | Frame 1: 218 bytes on wire (1744 bits), 218 bytes captured (1744 bits) 21 | Ethernet II, Src: Private_01:01:01 (01:01:01:01:01:01), Dst: MS-NLB-PhysServer-02_02:02:02:02 (02:02:02:02:02:02) 22 | Internet Protocol, Src: 1.1.1.1 (1.1.1.1), Dst: 2.2.2.2 (2.2.2.2) 23 | User Datagram Protocol, Src Port: wap-wsp (9200), Dst Port: re-mail-ck (50) 24 | Wireless Session Protocol, Method: Push (0x06), Content-Type: application/vnd.wap.sic 25 | Transaction ID: 0x01 26 | PDU Type: Push (0x06) 27 | Headers Length: 11 28 | Content-Type: application/vnd.wap.sic; charset=utf-8 29 | Charset: utf-8 30 | Headers 31 | Encoding-Version: 1.5 32 | Content-Length: 162 33 | Push-Flag: (Last push message) 34 | .... ...0 = Initiator URI is authenticated: False (0) 35 | .... ..0. = Content is trusted: False (0) 36 | .... .1.. = Last push message: True (1) 37 | WAP Binary XML, Version: 1.3, Public ID: "-//WAPFORUM//DTD SI 1.0//EN (Service Indication 1.0)" 38 | Version: 1.3 (0x03) 39 | Public Identifier (known): -//WAPFORUM//DTD SI 1.0//EN (Service Indication 1.0) (0x00000005) 40 | Character Set: utf-8 (0x0000006a) 41 | String table: 10 bytes 42 | Start | Length | String 43 | 0 | 10 | ' Vodafone' 44 | Data representation 45 | Level | State | Codepage | WBXML Token Description | Rendering 46 | 0 | Tag | T 0 | Known Tag 0x05 (.C) | 47 | 1 | Tag | T 0 | Known Tag 0x06 (AC) | 52 | 1 | Tag | T 0 | STR_I (Inline string) | 'Zum kostenlosen Portal "Mein' 53 | 1 | Tag | T 0 | STR_T (Tableref string) | ' Vodafone' 54 | 1 | Tag | T 0 | STR_I (Inline string) | '" - einfach auf den folgenden Link klicken oder die Seite direkt aufrufen. Ihr' 55 | 1 | Tag | T 0 | STR_T (Tableref string) | ' Vodafone' 56 | 1 | Tag | T 0 | STR_I (Inline string) | ' Team' 57 | 1 | Tag | T 0 | END (Known Tag 0x06) | 58 | 0 | Tag | T 0 | END (Known Tag 0x05) | 59 | """ 60 | 61 | # MMS WAP Push 62 | data = '\x01\x06"application/vnd.wap.mms-message\x00\xaf\x84\x8c\x82\x98NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA\x00\x8d\x90\x89\x19\x80+447717275049/TYPE=PLMN\x00\x8a\x80\x8e\x02t\x00\x88\x05\x81\x03\x03\xf4\x7f\x83http://promms/servlets/NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA\x00' 63 | 64 | """ 65 | No. Time Source Destination Protocol Info 66 | 1 0.000000 1.1.1.1 2.2.2.2 MMSE MMS m-notification-ind 67 | 68 | Frame 1: 224 bytes on wire (1792 bits), 224 bytes captured (1792 bits) 69 | Ethernet II, Src: Private_01:01:01 (01:01:01:01:01:01), Dst: MS-NLB-PhysServer-02_02:02:02:02 (02:02:02:02:02:02) 70 | Internet Protocol, Src: 1.1.1.1 (1.1.1.1), Dst: 2.2.2.2 (2.2.2.2) 71 | User Datagram Protocol, Src Port: wap-wsp (9200), Dst Port: re-mail-ck (50) 72 | Wireless Session Protocol, Method: Push (0x06), Content-Type: application/vnd.wap.mms-message 73 | Transaction ID: 0x01 74 | PDU Type: Push (0x06) 75 | Headers Length: 34 76 | Content-Type: application/vnd.wap.mms-message 77 | Headers 78 | X-Wap-Application-Id: x-wap-application:mms.ua 79 | MMS Message Encapsulation, Type: m-notification-ind 80 | X-Mms-Message-Type: m-notification-ind (0x82) 81 | X-Mms-Transaction-ID: NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA 82 | X-Mms-MMS-Version: 1.0 83 | From: +447717275049/TYPE=PLMN 84 | X-Mms-Message-Class: Personal (0x80) 85 | X-Mms-Message-Size: 29696 86 | X-Mms-Expiry: 259199.000000000 seconds 87 | X-Mms-Content-Location: http://promms/servlets/NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA 88 | """ 89 | 90 | offset = 0 91 | length = len(data) 92 | perline = 8 93 | 94 | while True: 95 | if offset >= length: 96 | break 97 | 98 | end = offset + perline 99 | if end > length: 100 | end = length 101 | line = data[offset:end] 102 | 103 | s = '' 104 | for c in line: 105 | s += " %02x" % unpack('B', c) 106 | 107 | # 000000 00 e0 1e a7 05 6f 00 10 108 | print "%06x%s" % (offset, s) 109 | 110 | offset += perline 111 | -------------------------------------------------------------------------------- /doc/tutorial/mms.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | MMS tutorial 3 | ============ 4 | 5 | Features 6 | ======== 7 | 8 | * Full featured MMS encoder/decoder 9 | * Supports MMS 1.0-1.4 10 | * Supports presentation layout 11 | * Handles well known formats: JPEG, GIF, AMR, MIDI, 3GP, etc. 12 | * Tested with WAP 2.0 gateways 13 | 14 | 15 | Encoding 16 | ======== 17 | 18 | How to encode a MMS:: 19 | 20 | from messaging.mms.message import MMSMessage, MMSMessagePage 21 | 22 | mms = MMSMessage() 23 | mms.headers['To'] = '+34231342234/TYPE=PLMN' 24 | mms.headers['Message-Type'] = 'm-send-req' 25 | mms.headers['Subject'] = 'Test python-messaging.mms' 26 | 27 | slide1 = MMSMessagePage() 28 | slide1.add_image('image1.jpg') 29 | slide1.add_text('This is the first slide, with a static image and some text.') 30 | 31 | slide2 = MMSMessagePage() 32 | slide2.set_duration(4500) 33 | slide2.add_image('image2.jpg', 1500) 34 | slide2.add_text('This second slide has some timing effects.', 500, 3500) 35 | 36 | mms.add_page(slide1) 37 | mms.add_page(slide2) 38 | 39 | payload = mms.encode() 40 | 41 | 42 | The above snippet binary encodes a MMS for '+34231342234' and subject 'Test 43 | python-messaging.mms' with two slides. The first slide is just an static 44 | image with some text, the second one has timing effects and will last 4.5s. 45 | 46 | Sending a MMS 47 | +++++++++++++ 48 | 49 | In a WAP2.0 gateway, the binary message (payload) will be used as an argument 50 | for a plain HTTP POST:: 51 | 52 | from cStringIO import StringIO 53 | import socket 54 | 55 | gw_host, gw_port = "212.11.23.23", 7899 56 | 57 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 58 | s.connect((gw_host, gw_port)) 59 | s.send("POST %s HTTP/1.0\r\n" % mmsc) 60 | s.send("Content-Type: application/vnd.wap.mms-message\r\n") 61 | s.send("Content-Length: %d\r\n\r\n" % len(payload)) 62 | 63 | s.sendall(payload) 64 | 65 | buf = StringIO() 66 | 67 | while True: 68 | data = s.recv(4096) 69 | if not data: 70 | break 71 | 72 | buf.write(data) 73 | 74 | s.close() 75 | data = buf.getvalue() 76 | buf.close() 77 | 78 | print "PROXY RESPONSE", data 79 | 80 | 81 | Encoding a m-notifyresp-ind PDU 82 | +++++++++++++++++++++++++++++++ 83 | 84 | In order to send a m-notifyresp-ind, you will need to know the transaction 85 | id of the MMS you want to acknowledge, once you have that you just need 86 | to:: 87 | 88 | mms = MMSMessage() 89 | mms.headers['Transaction-Id'] = 'T2132112322' 90 | mms.headers['Message-Type'] = 'm-notifyresp-ind' 91 | mms.headers['Status'] = 'Retrieved' 92 | 93 | payload = mms.encode() 94 | 95 | And POST the resulting payload to the :term:`MMSC` proxy using the very same 96 | code used for sending a MMS. 97 | 98 | 99 | Decoding 100 | ======== 101 | 102 | Decoding from binary data 103 | +++++++++++++++++++++++++ 104 | 105 | Decoding a MMS could not be any easier, once you have the binary data of the 106 | MMS, you just need to:: 107 | 108 | from messaging.mms.message import MMSMessage 109 | 110 | # data is an array.array("B") instance 111 | mms = MMSMessage.from_data(data) 112 | 113 | print mms.headers['Message-Type'] # m-send-req 114 | print mms.headers['To'] # '+34231342234/TYPE=PLMN' 115 | 116 | 117 | Decoding from a file 118 | ++++++++++++++++++++ 119 | 120 | Decoding a MMS serialised to a file is pretty straightforward too, you just 121 | need the path to the file and:: 122 | 123 | from messaging.mms.message import MMSMessage 124 | 125 | path = '/tmp/binary-mms.bin' 126 | mms = MMSMessage.from_file(path) 127 | 128 | print mms.headers['Message-Type'] # m-send-req 129 | print mms.headers['To'] # '+34231342234/TYPE=PLMN' 130 | 131 | 132 | Obtaining a MMS from a WAP push notification 133 | ++++++++++++++++++++++++++++++++++++++++++++ 134 | 135 | A WAP push notification conveys all the necessary information to retrieve 136 | the MMS from the MMSC. Once you have the different PDUs of the WAP push, 137 | you need to decode it and obtain the `Content-Location` value of the 138 | headers:: 139 | 140 | from messaging.sms import SmsDeliver 141 | from messaging.sms.wap import extract_push_notification 142 | 143 | pdus = [ 144 | "0791447758100650400E80885810000000810004016082415464408C0C08049F8E020105040B8423F00106226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82984E4F4B3543694B636F544D595347344D4253774141734B7631344655484141414141414141008D908919802B3434373738353334323734392F545950453D504C4D4E008A808E0274008805810301194083687474703A2F", 145 | "0791447758100650440E8088581000000081000401608241547440440C08049F8E020205040B8423F02F70726F6D6D732F736572766C6574732F4E4F4B3543694B636F544D595347344D4253774141734B763134465548414141414141414100", 146 | ] 147 | data = "" 148 | 149 | sms = SmsDeliver(pdus[0]) 150 | data += sms.text 151 | 152 | sms = SmsDeliver(pdus[1]) 153 | data += sms.text 154 | 155 | mms = extract_push_notification(data) 156 | url = mms.headers['Content-Location'] 157 | print url 158 | 159 | 160 | Once you have the content location, you need to do a HTTP GET to retrieve 161 | the MMS payload:: 162 | 163 | import socket 164 | from cStringIO import StringIO 165 | 166 | from messaging.mms.message import MMSMessage 167 | 168 | gw_host, gw_port = "212.11.23.23", 7899 169 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 170 | s.connect((gw_host, gw_port)) 171 | s.send("GET %s HTTP/1.0\r\n\r\n" % url) 172 | 173 | buf = StringIO() 174 | 175 | while True: 176 | data = s.recv(4096) 177 | if not data: 178 | break 179 | 180 | buf.write(data) 181 | 182 | s.close() 183 | data = buf.getvalue() 184 | buf.close() 185 | 186 | mms = MMSMessage.from_data(data) 187 | print mms 188 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # If your documentation needs a minimal Sphinx version, state it here. 4 | #needs_sphinx = '1.0' 5 | 6 | # Add any Sphinx extension module names here, as strings. They can be extensions 7 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 8 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 9 | 10 | # Add any paths that contain templates here, relative to this directory. 11 | templates_path = ['_templates'] 12 | 13 | # The suffix of source filenames. 14 | source_suffix = '.rst' 15 | 16 | # The encoding of source files. 17 | #source_encoding = 'utf-8-sig' 18 | 19 | # The master toctree document. 20 | master_doc = 'index' 21 | 22 | # General information about the project. 23 | project = u'python-messaging' 24 | copyright = u'2010, Pablo Martí' 25 | 26 | # The short X.Y version. 27 | version = '0.5.9' 28 | # The full version, including alpha/beta/rc tags. 29 | release = '0.5.9' 30 | 31 | # The language for content autogenerated by Sphinx. Refer to documentation 32 | # for a list of supported languages. 33 | #language = None 34 | 35 | today_fmt = '%B %d, %Y' 36 | 37 | # List of patterns, relative to source directory, that match files and 38 | # directories to ignore when looking for source files. 39 | exclude_patterns = ['_build'] 40 | 41 | # The reST default role (used for this markup: `text`) to use for all documents. 42 | #default_role = None 43 | 44 | # If true, '()' will be appended to :func: etc. cross-reference text. 45 | add_function_parentheses = True 46 | 47 | # If true, the current module name will be prepended to all description 48 | # unit titles (such as .. function::). 49 | add_module_names = False 50 | 51 | # If true, sectionauthor and moduleauthor directives will be shown in the 52 | # output. They are ignored by default. 53 | #show_authors = False 54 | 55 | # The name of the Pygments (syntax highlighting) style to use. 56 | pygments_style = 'sphinx' 57 | 58 | # A list of ignored prefixes for module index sorting. 59 | #modindex_common_prefix = [] 60 | 61 | 62 | # -- Options for HTML output --------------------------------------------------- 63 | 64 | # The theme to use for HTML and HTML Help pages. See the documentation for 65 | # a list of builtin themes. 66 | html_theme = 'default' 67 | 68 | # Theme options are theme-specific and customize the look and feel of a theme 69 | # further. For a list of options available for each theme, see the 70 | # documentation. 71 | #html_theme_options = {} 72 | 73 | # Add any paths that contain custom themes here, relative to this directory. 74 | #html_theme_path = [] 75 | 76 | # The name for this set of Sphinx documents. If None, it defaults to 77 | # " v documentation". 78 | #html_title = None 79 | 80 | # A shorter title for the navigation bar. Default is the same as html_title. 81 | #html_short_title = None 82 | 83 | # The name of an image file (relative to this directory) to place at the top 84 | # of the sidebar. 85 | #html_logo = None 86 | 87 | # The name of an image file (within the static path) to use as favicon of the 88 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 89 | # pixels large. 90 | #html_favicon = None 91 | 92 | # Add any paths that contain custom static files (such as style sheets) here, 93 | # relative to this directory. They are copied after the builtin static files, 94 | # so a file named "default.css" will overwrite the builtin "default.css". 95 | html_static_path = ['_static'] 96 | 97 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 98 | # using the given strftime format. 99 | #html_last_updated_fmt = '%b %d, %Y' 100 | 101 | # If true, SmartyPants will be used to convert quotes and dashes to 102 | # typographically correct entities. 103 | html_use_smartypants = True 104 | 105 | # Custom sidebar templates, maps document names to template names. 106 | #html_sidebars = {} 107 | 108 | # Additional templates that should be rendered to pages, maps page names to 109 | # template names. 110 | #html_additional_pages = {} 111 | 112 | # If false, no module index is generated. 113 | #html_domain_indices = True 114 | 115 | # If false, no index is generated. 116 | #html_use_index = True 117 | 118 | # If true, the index is split into individual pages for each letter. 119 | #html_split_index = False 120 | 121 | # If true, links to the reST sources are added to the pages. 122 | html_show_sourcelink = True 123 | 124 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 125 | html_show_sphinx = True 126 | 127 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 128 | html_show_copyright = True 129 | 130 | # If true, an OpenSearch description file will be output, and all pages will 131 | # contain a tag referring to it. The value of this option must be the 132 | # base URL from which the finished HTML is served. 133 | #html_use_opensearch = '' 134 | 135 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 136 | #html_file_suffix = None 137 | 138 | # Output file base name for HTML help builder. 139 | htmlhelp_basename = 'python-messagingdoc' 140 | 141 | 142 | # -- Options for LaTeX output -------------------------------------------------- 143 | 144 | # The paper size ('letter' or 'a4'). 145 | #latex_paper_size = 'letter' 146 | 147 | # The font size ('10pt', '11pt' or '12pt'). 148 | #latex_font_size = '10pt' 149 | 150 | # Grouping the document tree into LaTeX files. List of tuples 151 | # (source start file, target name, title, author, documentclass [howto/manual]). 152 | latex_documents = [ 153 | ('index', 'python-messaging.tex', u'python-messaging Documentation', 154 | u'Pablo Martí', 'manual'), 155 | ] 156 | 157 | # The name of an image file (relative to this directory) to place at the top of 158 | # the title page. 159 | #latex_logo = None 160 | 161 | # For "manual" documents, if this is true, then toplevel headings are parts, 162 | # not chapters. 163 | #latex_use_parts = False 164 | 165 | # If true, show page references after internal links. 166 | #latex_show_pagerefs = False 167 | 168 | # If true, show URL addresses after external links. 169 | #latex_show_urls = False 170 | 171 | # Additional stuff for the LaTeX preamble. 172 | #latex_preamble = '' 173 | 174 | # Documents to append as an appendix to all manuals. 175 | #latex_appendices = [] 176 | 177 | # If false, no module index is generated. 178 | #latex_domain_indices = True 179 | 180 | 181 | # -- Options for manual page output -------------------------------------------- 182 | 183 | # One entry per manual page. List of tuples 184 | # (source start file, name, description, authors, manual section). 185 | man_pages = [ 186 | ('index', 'python-messaging', u'python-messaging Documentation', 187 | [u'Pablo Martí'], 1) 188 | ] 189 | -------------------------------------------------------------------------------- /messaging/utils.py: -------------------------------------------------------------------------------- 1 | from array import array 2 | from datetime import timedelta, tzinfo 3 | from math import floor 4 | import sys 5 | 6 | 7 | class FixedOffset(tzinfo): 8 | """Fixed offset in minutes east from UTC.""" 9 | 10 | def __init__(self, offset, name): 11 | if isinstance(offset, timedelta): 12 | self.offset = offset 13 | else: 14 | self.offset = timedelta(minutes=offset) 15 | 16 | self.__name = name 17 | 18 | @classmethod 19 | def from_timezone(cls, tz_str, name): 20 | # no timezone, GMT+3, GMT-3 21 | # '', '+0330', '-0300' 22 | if not tz_str: 23 | return cls(timedelta(0), name) 24 | 25 | sign = 1 if '+' in tz_str else -1 26 | offset = tz_str.replace('+', '').replace('-', '') 27 | hours, minutes = int(offset[:2]), int(offset[2:]) 28 | minutes += hours * 60 29 | 30 | if sign == 1: 31 | td = timedelta(minutes=minutes) 32 | elif sign == -1: 33 | td = timedelta(days=-1, minutes=minutes) 34 | 35 | return cls(td, name) 36 | 37 | def utcoffset(self, dt): 38 | return self.offset 39 | 40 | def tzname(self, dt): 41 | return self.__name 42 | 43 | def dst(self, dt): 44 | return timedelta(0) 45 | 46 | 47 | def bytes_to_str(b): 48 | if sys.version_info >= (3,): 49 | return b.decode('latin1') 50 | 51 | return b 52 | 53 | 54 | def to_array(pdu): 55 | return array('B', [int(pdu[i:i + 2], 16) for i in range(0, len(pdu), 2)]) 56 | 57 | 58 | def to_bytes(s): 59 | if sys.version_info >= (3,): 60 | return bytes(s) 61 | 62 | return ''.join(map(unichr, s)) 63 | 64 | 65 | def debug(s): 66 | # set this to True if you want to poke at PDU encoding/decoding 67 | if False: 68 | print s 69 | 70 | 71 | def swap(s): 72 | """Swaps ``s`` according to GSM 23.040""" 73 | what = s[:] 74 | for n in range(1, len(what), 2): 75 | what[n - 1], what[n] = what[n], what[n - 1] 76 | 77 | return what 78 | 79 | 80 | def swap_number(n): 81 | data = swap(list(n.replace('f', ''))) 82 | return ''.join(data) 83 | 84 | 85 | def clean_number(n): 86 | return n.strip().replace(' ', '') 87 | 88 | 89 | def encode_str(s): 90 | """Returns the hexadecimal representation of ``s``""" 91 | return ''.join(["%02x" % ord(n) for n in s]) 92 | 93 | 94 | def encode_bytes(b): 95 | return ''.join(["%02x" % n for n in b]) 96 | 97 | 98 | def pack_8bits_to_7bits(message, udh=None): 99 | pdu = "" 100 | txt = bytes_to_str(message) 101 | 102 | if udh is None: 103 | tl = len(txt) 104 | txt += '\x00' 105 | msgl = int(len(txt) * 7 / 8) 106 | op = [-1] * msgl 107 | c = shift = 0 108 | 109 | for n in range(msgl): 110 | if shift == 6: 111 | c += 1 112 | 113 | shift = n % 7 114 | lb = ord(txt[c]) >> shift 115 | hb = (ord(txt[c + 1]) << (7 - shift) & 255) 116 | op[n] = lb + hb 117 | c += 1 118 | 119 | pdu = chr(tl) + ''.join(map(chr, op)) 120 | else: 121 | txt = "\x00\x00\x00\x00\x00\x00" + txt 122 | tl = len(txt) 123 | 124 | txt += '\x00' 125 | msgl = int(len(txt) * 7 / 8) 126 | op = [-1] * msgl 127 | c = shift = 0 128 | 129 | for n in range(msgl): 130 | if shift == 6: 131 | c += 1 132 | 133 | shift = n % 7 134 | lb = ord(txt[c]) >> shift 135 | hb = (ord(txt[c + 1]) << (7 - shift) & 255) 136 | op[n] = lb + hb 137 | c += 1 138 | 139 | for i, char in enumerate(udh): 140 | op[i] = ord(char) 141 | 142 | pdu = chr(tl) + ''.join(map(chr, op)) 143 | 144 | return encode_str(pdu) 145 | 146 | 147 | def pack_8bits_to_8bit(message, udh=None): 148 | text = message 149 | if udh is not None: 150 | text = udh + text 151 | 152 | mlen = len(text) 153 | message = chr(mlen) + message 154 | return encode_str(message) 155 | 156 | 157 | def pack_8bits_to_ucs2(message, udh=None): 158 | # XXX: This does not control the size respect to UDH 159 | text = message 160 | nmesg = '' 161 | 162 | if udh is not None: 163 | text = udh + text 164 | 165 | for n in text: 166 | nmesg += chr(ord(n) >> 8) + chr(ord(n) & 0xFF) 167 | 168 | mlen = len(text) * 2 169 | message = chr(mlen) + nmesg 170 | return encode_str(message) 171 | 172 | 173 | def unpack_msg(pdu): 174 | """Unpacks ``pdu`` into septets and returns the decoded string""" 175 | # Taken/modified from Dave Berkeley's pysms package 176 | count = last = 0 177 | result = [] 178 | 179 | for i in range(0, len(pdu), 2): 180 | byte = int(pdu[i:i + 2], 16) 181 | mask = 0x7F >> count 182 | out = ((byte & mask) << count) + last 183 | last = byte >> (7 - count) 184 | result.append(out) 185 | 186 | if len(result) >= 0xa0: 187 | break 188 | 189 | if count == 6: 190 | result.append(last) 191 | last = 0 192 | 193 | count = (count + 1) % 7 194 | 195 | return to_bytes(result) 196 | 197 | 198 | def unpack_msg2(pdu): 199 | """Unpacks ``pdu`` into septets and returns the decoded string""" 200 | # Taken/modified from Dave Berkeley's pysms package 201 | count = last = 0 202 | result = [] 203 | 204 | for byte in pdu: 205 | mask = 0x7F >> count 206 | out = ((byte & mask) << count) + last 207 | last = byte >> (7 - count) 208 | result.append(out) 209 | 210 | if len(result) >= 0xa0: 211 | break 212 | 213 | if count == 6: 214 | result.append(last) 215 | last = 0 216 | 217 | count = (count + 1) % 7 218 | 219 | return to_bytes(result) 220 | 221 | 222 | def timedelta_to_relative_validity(t): 223 | """ 224 | Convert ``t`` to its relative validity period 225 | 226 | In case the resolution of ``t`` is too small for a time unit, 227 | it will be floor-rounded to the previous sane value 228 | 229 | :type t: datetime.timedelta 230 | 231 | :return int 232 | """ 233 | if t < timedelta(minutes=5): 234 | raise ValueError("Min resolution is five minutes") 235 | 236 | if t > timedelta(weeks=63): 237 | raise ValueError("Max validity is 63 weeks") 238 | 239 | if t <= timedelta(hours=12): 240 | return int(floor(t.seconds / (60 * 5))) - 1 241 | 242 | if t <= timedelta(hours=24): 243 | t -= timedelta(hours=12) 244 | return int(floor(t.seconds / (60 * 30))) + 143 245 | 246 | if t <= timedelta(days=30): 247 | return t.days + 166 248 | 249 | if t <= timedelta(weeks=63): 250 | return int(floor(t.days / 7)) + 192 251 | 252 | 253 | def datetime_to_absolute_validity(d, tzname='Unknown'): 254 | """Convert ``d`` to its integer representation""" 255 | n = d.strftime("%y %m %d %H %M %S %z").split(" ") 256 | # compute offset 257 | offset = FixedOffset.from_timezone(n[-1], tzname).offset 258 | # one unit is 15 minutes 259 | s = "%02d" % int(floor(offset.seconds / (60 * 15))) 260 | 261 | if offset.days < 0: 262 | # set MSB to 1 263 | s = "%02x" % ((int(s[0]) << 4) | int(s[1]) | 0x80) 264 | 265 | n[-1] = s 266 | 267 | return [int(c[::-1], 16) for c in n] 268 | -------------------------------------------------------------------------------- /messaging/test/test_wap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from array import array 3 | import unittest 4 | 5 | from messaging.sms import SmsDeliver 6 | from messaging.sms.wap import (is_a_wap_push_notification as is_push, 7 | is_mms_notification, 8 | extract_push_notification) 9 | 10 | 11 | def list_to_str(l): 12 | a = array("B", l) 13 | return a.tostring() 14 | 15 | 16 | class TestSmsWapPush(unittest.TestCase): 17 | 18 | data = [1, 6, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111, 19 | 110, 47, 118, 110, 100, 46, 119, 97, 112, 46, 109, 109, 115, 45, 20 | 109, 101, 115, 115, 97, 103, 101, 0, 175, 132, 140, 130, 152, 78, 21 | 79, 75, 53, 67, 105, 75, 99, 111, 84, 77, 89, 83, 71, 52, 77, 66, 22 | 83, 119, 65, 65, 115, 75, 118, 49, 52, 70, 85, 72, 65, 65, 65, 65, 23 | 65, 65, 65, 65, 0, 141, 144, 137, 25, 128, 43, 52, 52, 55, 55, 56, 24 | 53, 51, 52, 50, 55, 52, 57, 47, 84, 89, 80, 69, 61, 80, 76, 77, 78, 25 | 0, 138, 128, 142, 2, 116, 0, 136, 5, 129, 3, 1, 25, 64, 131, 104, 26 | 116, 116, 112, 58, 47, 47, 112, 114, 111, 109, 109, 115, 47, 115, 27 | 101, 114, 118, 108, 101, 116, 115, 47, 78, 79, 75, 53, 67, 105, 75, 28 | 99, 111, 84, 77, 89, 83, 71, 52, 77, 66, 83, 119, 65, 65, 115, 75, 29 | 118, 49, 52, 70, 85, 72, 65, 65, 65, 65, 65, 65, 65, 65, 0] 30 | 31 | def test_is_a_wap_push_notification(self): 32 | self.assertTrue(is_push(list_to_str(self.data))) 33 | self.assertTrue(is_push(list_to_str([1, 6, 57, 92, 45]))) 34 | self.assertFalse(is_push(list_to_str([4, 5, 57, 92, 45]))) 35 | self.assertRaises(TypeError, is_push, 1) 36 | 37 | def test_decoding_m_notification_ind(self): 38 | pdus = [ 39 | "0791447758100650400E80885810000000810004016082415464408C0C08049F8E020105040B8423F00106226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82984E4F4B3543694B636F544D595347344D4253774141734B7631344655484141414141414141008D908919802B3434373738353334323734392F545950453D504C4D4E008A808E0274008805810301194083687474703A2F", 40 | "0791447758100650440E8088581000000081000401608241547440440C08049F8E020205040B8423F02F70726F6D6D732F736572766C6574732F4E4F4B3543694B636F544D595347344D4253774141734B763134465548414141414141414100", 41 | ] 42 | number = '3838383530313030303030303138'.decode('hex') 43 | csca = "+447785016005" 44 | data = "" 45 | 46 | sms = SmsDeliver(pdus[0]) 47 | self.assertEqual(sms.udh.concat.ref, 40846) 48 | self.assertEqual(sms.udh.concat.cnt, 2) 49 | self.assertEqual(sms.udh.concat.seq, 1) 50 | self.assertEqual(sms.number, number) 51 | self.assertEqual(sms.csca, csca) 52 | data += sms.text 53 | 54 | sms = SmsDeliver(pdus[1]) 55 | self.assertEqual(sms.udh.concat.ref, 40846) 56 | self.assertEqual(sms.udh.concat.cnt, 2) 57 | self.assertEqual(sms.udh.concat.seq, 2) 58 | self.assertEqual(sms.number, number) 59 | data += sms.text 60 | 61 | mms = extract_push_notification(data) 62 | self.assertEqual(is_mms_notification(mms), True) 63 | 64 | self.assertEqual(mms.headers['Message-Type'], 'm-notification-ind') 65 | self.assertEqual(mms.headers['Transaction-Id'], 66 | 'NOK5CiKcoTMYSG4MBSwAAsKv14FUHAAAAAAAA') 67 | self.assertEqual(mms.headers['MMS-Version'], '1.0') 68 | self.assertEqual(mms.headers['From'], 69 | '2b3434373738353334323734392f545950453d504c4d4e'.decode('hex')) 70 | self.assertEqual(mms.headers['Message-Class'], 'Personal') 71 | self.assertEqual(mms.headers['Message-Size'], 29696) 72 | self.assertEqual(mms.headers['Expiry'], 72000) 73 | self.assertEqual(mms.headers['Content-Location'], 74 | 'http://promms/servlets/NOK5CiKcoTMYSG4MBSwAAsKv14FUHAAAAAAAA') 75 | 76 | pdus = [ 77 | "0791447758100650400E80885810000000800004017002314303408C0C0804DFD3020105040B8423F00106226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82984E4F4B3541315A6446544D595347344F3356514141734A763934476F4E4141414141414141008D908919802B3434373731373237353034392F545950453D504C4D4E008A808E0274008805810303F47F83687474703A2F", 78 | "0791447758100650440E8088581000000080000401700231431340440C0804DFD3020205040B8423F02F70726F6D6D732F736572766C6574732F4E4F4B3541315A6446544D595347344F3356514141734A763934476F4E414141414141414100", 79 | ] 80 | 81 | number = "88850100000008" 82 | data = "" 83 | 84 | sms = SmsDeliver(pdus[0]) 85 | self.assertEqual(sms.udh.concat.ref, 57299) 86 | self.assertEqual(sms.udh.concat.cnt, 2) 87 | self.assertEqual(sms.udh.concat.seq, 1) 88 | self.assertEqual(sms.number, number) 89 | data += sms.text 90 | 91 | sms = SmsDeliver(pdus[1]) 92 | self.assertEqual(sms.udh.concat.ref, 57299) 93 | self.assertEqual(sms.udh.concat.cnt, 2) 94 | self.assertEqual(sms.udh.concat.seq, 2) 95 | self.assertEqual(sms.number, number) 96 | data += sms.text 97 | 98 | mms = extract_push_notification(data) 99 | self.assertEqual(is_mms_notification(mms), True) 100 | 101 | self.assertEqual(mms.headers['Message-Type'], 'm-notification-ind') 102 | self.assertEqual(mms.headers['Transaction-Id'], 103 | 'NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA') 104 | self.assertEqual(mms.headers['MMS-Version'], '1.0') 105 | self.assertEqual(mms.headers['From'], 106 | '2b3434373731373237353034392f545950453d504c4d4e'.decode('hex')) 107 | self.assertEqual(mms.headers['Message-Class'], 'Personal') 108 | self.assertEqual(mms.headers['Message-Size'], 29696) 109 | self.assertEqual(mms.headers['Expiry'], 259199) 110 | self.assertEqual(mms.headers['Content-Location'], 111 | 'http://promms/servlets/NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA') 112 | 113 | def test_decoding_generic_wap_push(self): 114 | pdus = [ 115 | "0791947122725014440C8500947122921105F5112042519582408C0B05040B8423F0000396020101060B03AE81EAC3958D01A2B48403056A0A20566F6461666F6E650045C60C037761702E6D65696E63616C6C79612E64652F000801035A756D206B6F7374656E6C6F73656E20506F7274616C20224D65696E0083000322202D2065696E66616368206175662064656E20666F6C67656E64656E204C696E6B206B6C69636B656E", 116 | "0791947122725014440C8500947122921105F5112042519592403C0B05040B8423F00003960202206F6465722064696520536569746520646972656B7420617566727566656E2E2049687200830003205465616D000101", 117 | ] 118 | number = '303034393137323232393131'.decode('hex') 119 | csca = "+491722270541" 120 | data = "" 121 | 122 | sms = SmsDeliver(pdus[0]) 123 | self.assertEqual(sms.udh.concat.ref, 150) 124 | self.assertEqual(sms.udh.concat.cnt, 2) 125 | self.assertEqual(sms.udh.concat.seq, 1) 126 | self.assertEqual(sms.number, number) 127 | self.assertEqual(sms.csca, csca) 128 | data += sms.text 129 | 130 | sms = SmsDeliver(pdus[1]) 131 | self.assertEqual(sms.udh.concat.ref, 150) 132 | self.assertEqual(sms.udh.concat.cnt, 2) 133 | self.assertEqual(sms.udh.concat.seq, 2) 134 | self.assertEqual(sms.number, number) 135 | data += sms.text 136 | 137 | self.assertEqual(data, '\x01\x06\x0b\x03\xae\x81\xea\xc3\x95\x8d\x01\xa2\xb4\x84\x03\x05j\n Vodafone\x00E\xc6\x0c\x03wap.meincallya.de/\x00\x08\x01\x03Zum kostenlosen Portal "Mein\x00\x83\x00\x03" - einfach auf den folgenden Link klicken oder die Seite direkt aufrufen. Ihr\x00\x83\x00\x03 Team\x00\x01\x01') 138 | 139 | push = extract_push_notification(data) 140 | self.assertEqual(is_mms_notification(push), False) 141 | -------------------------------------------------------------------------------- /messaging/sms/deliver.py: -------------------------------------------------------------------------------- 1 | # see LICENSE 2 | """Classes for processing received SMS""" 3 | 4 | from datetime import datetime, timedelta 5 | 6 | from messaging.utils import (swap, swap_number, encode_bytes, debug, 7 | unpack_msg, unpack_msg2, to_array) 8 | from messaging.sms import consts 9 | from messaging.sms.base import SmsBase 10 | from messaging.sms.udh import UserDataHeader 11 | 12 | 13 | class SmsDeliver(SmsBase): 14 | """I am a delivered SMS in your Inbox""" 15 | 16 | def __init__(self, pdu, strict=True): 17 | super(SmsDeliver, self).__init__() 18 | self._pdu = None 19 | self._strict = strict 20 | self.date = None 21 | self.mtype = None 22 | self.sr = None 23 | 24 | self.pdu = pdu 25 | 26 | @property 27 | def data(self): 28 | """ 29 | Returns a dict populated with the SMS attributes 30 | 31 | It mimics the old API to ease the port to the new API 32 | """ 33 | ret = { 34 | 'text': self.text, 35 | 'pid': self.pid, 36 | 'dcs': self.dcs, 37 | 'csca': self.csca, 38 | 'number': self.number, 39 | 'type': self.type, 40 | 'date': self.date, 41 | 'fmt': self.fmt, 42 | 'sr': self.sr, 43 | } 44 | 45 | if self.udh is not None: 46 | if self.udh.concat is not None: 47 | ret.update({ 48 | 'ref': self.udh.concat.ref, 49 | 'cnt': self.udh.concat.cnt, 50 | 'seq': self.udh.concat.seq, 51 | }) 52 | 53 | return ret 54 | 55 | def _set_pdu(self, pdu): 56 | if not self._strict and len(pdu) % 2: 57 | # if not strict and PDU-length is odd, remove the last character 58 | # and make it even. See the discussion of this bug at 59 | # http://github.com/pmarti/python-messaging/issues#issue/7 60 | pdu = pdu[:-1] 61 | 62 | if len(pdu) % 2: 63 | raise ValueError("Can not decode an odd-length pdu") 64 | 65 | # XXX: Should we keep the original PDU or the modified one? 66 | self._pdu = pdu 67 | 68 | data = to_array(self._pdu) 69 | 70 | # Service centre address 71 | smscl = data.pop(0) 72 | if smscl > 0: 73 | smscertype = data.pop(0) 74 | smscl -= 1 75 | self.csca = swap_number(encode_bytes(data[:smscl])) 76 | if (smscertype >> 4) & 0x07 == consts.INTERNATIONAL: 77 | self.csca = '+%s' % self.csca 78 | data = data[smscl:] 79 | else: 80 | self.csca = None 81 | 82 | # 1 byte(octet) == 2 char 83 | # Message type TP-MTI bits 0,1 84 | # More messages to send/deliver bit 2 85 | # Status report request indicated bit 5 86 | # User Data Header Indicator bit 6 87 | # Reply path set bit 7 88 | try: 89 | self.mtype = data.pop(0) 90 | except TypeError: 91 | raise ValueError("Decoding this type of SMS is not supported yet") 92 | 93 | mtype = self.mtype & 0x03 94 | 95 | if mtype == 0x02: 96 | return self._decode_status_report_pdu(data) 97 | 98 | if mtype == 0x01: 99 | raise ValueError("Cannot decode a SmsSubmitReport message yet") 100 | 101 | sndlen = data.pop(0) 102 | if sndlen % 2: 103 | sndlen += 1 104 | sndlen = int(sndlen / 2.0) 105 | 106 | sndtype = (data.pop(0) >> 4) & 0x07 107 | if sndtype == consts.ALPHANUMERIC: 108 | # coded according to 3GPP TS 23.038 [9] GSM 7-bit default alphabet 109 | sender = unpack_msg2(data[:sndlen]).decode("gsm0338") 110 | else: 111 | # Extract phone number of sender 112 | sender = swap_number(encode_bytes(data[:sndlen])) 113 | if sndtype == consts.INTERNATIONAL: 114 | sender = '+%s' % sender 115 | 116 | self.number = sender 117 | data = data[sndlen:] 118 | 119 | # 1 byte TP-PID (Protocol IDentifier) 120 | self.pid = data.pop(0) 121 | # 1 byte TP-DCS (Data Coding Scheme) 122 | self.dcs = data.pop(0) 123 | if self.dcs & (0x04 | 0x08) == 0: 124 | self.fmt = 0x00 125 | elif self.dcs & 0x04: 126 | self.fmt = 0x04 127 | elif self.dcs & 0x08: 128 | self.fmt = 0x08 129 | 130 | datestr = '' 131 | # Get date stamp (sender's local time) 132 | date = list(encode_bytes(data[:6])) 133 | for n in range(1, len(date), 2): 134 | date[n - 1], date[n] = date[n], date[n - 1] 135 | 136 | data = data[6:] 137 | 138 | # Get sender's offset from GMT (TS 23.040 TP-SCTS) 139 | tz = data.pop(0) 140 | 141 | offset = ((tz & 0x07) * 10 + ((tz & 0xf0) >> 4)) * 15 142 | if (tz & 0x08): 143 | offset = offset * -1 144 | 145 | # 02/08/26 19:37:41 146 | datestr = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date) 147 | outputfmt = '%y/%m/%d %H:%M:%S' 148 | 149 | sndlocaltime = datetime.strptime(datestr, outputfmt) 150 | sndoffset = timedelta(minutes=offset) 151 | # date as UTC 152 | self.date = sndlocaltime - sndoffset 153 | 154 | self._process_message(data) 155 | 156 | def _process_message(self, data): 157 | # Now get message body 158 | msgl = data.pop(0) 159 | msg = encode_bytes(data[:msgl]) 160 | # check for header 161 | headlen = ud_len = 0 162 | 163 | if self.mtype & 0x40: # UDHI present 164 | ud_len = data.pop(0) 165 | self.udh = UserDataHeader.from_bytes(data[:ud_len]) 166 | headlen = (ud_len + 1) * 8 167 | if self.fmt == 0x00: 168 | while headlen % 7: 169 | headlen += 1 170 | headlen /= 7 171 | 172 | headlen = int(headlen) 173 | 174 | if self.fmt == 0x00: 175 | # XXX: Use unpack_msg2 176 | data = data[ud_len:].tolist() 177 | #self.text = unpack_msg2(data).decode("gsm0338") 178 | self.text = unpack_msg(msg)[headlen:msgl].decode("gsm0338") 179 | 180 | elif self.fmt == 0x04: 181 | self.text = data[ud_len:].tostring() 182 | 183 | elif self.fmt == 0x08: 184 | data = data[ud_len:].tolist() 185 | _bytes = [int("%02X%02X" % (data[i], data[i + 1]), 16) 186 | for i in range(0, len(data), 2)] 187 | self.text = u''.join(list(map(unichr, _bytes))) 188 | 189 | pdu = property(lambda self: self._pdu, _set_pdu) 190 | 191 | def _decode_status_report_pdu(self, data): 192 | self.udh = UserDataHeader.from_status_report_ref(data.pop(0)) 193 | 194 | sndlen = data.pop(0) 195 | if sndlen % 2: 196 | sndlen += 1 197 | sndlen = int(sndlen / 2.0) 198 | 199 | sndtype = data.pop(0) 200 | recipient = swap_number(encode_bytes(data[:sndlen])) 201 | if (sndtype >> 4) & 0x07 == consts.INTERNATIONAL: 202 | recipient = '+%s' % recipient 203 | 204 | data = data[sndlen:] 205 | 206 | date = swap(list(encode_bytes(data[:7]))) 207 | try: 208 | scts_str = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date[0:12]) 209 | self.date = datetime.strptime(scts_str, "%y/%m/%d %H:%M:%S") 210 | except (ValueError, TypeError): 211 | scts_str = '' 212 | debug('Could not decode scts: %s' % date) 213 | 214 | data = data[7:] 215 | 216 | date = swap(list(encode_bytes(data[:7]))) 217 | try: 218 | dt_str = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date[0:12]) 219 | dt = datetime.strptime(dt_str, "%y/%m/%d %H:%M:%S") 220 | except (ValueError, TypeError): 221 | dt_str = '' 222 | dt = None 223 | debug('Could not decode date: %s' % date) 224 | 225 | data = data[7:] 226 | 227 | msg_l = [recipient, scts_str] 228 | try: 229 | status = data.pop(0) 230 | except IndexError: 231 | # Yes it is entirely possible that a status report comes 232 | # with no status at all! I'm faking for now the values and 233 | # set it to SR-UNKNOWN as that's all we can do 234 | _status = None 235 | status = 0x1 236 | sender = 'SR-UNKNOWN' 237 | msg_l.append(dt_str) 238 | else: 239 | _status = status 240 | if status == 0x00: 241 | msg_l.append(dt_str) 242 | else: 243 | msg_l.append('') 244 | if status == 0x00: 245 | sender = "SR-OK" 246 | elif status == 0x1: 247 | sender = "SR-UNKNOWN" 248 | elif status == 0x30: 249 | sender = "SR-STORED" 250 | else: 251 | sender = "SR-UNKNOWN" 252 | 253 | self.number = sender 254 | self.text = "|".join(msg_l) 255 | self.fmt = 0x08 # UCS2 256 | self.type = 0x03 # status report 257 | 258 | self.sr = { 259 | 'recipient': recipient, 260 | 'scts': self.date, 261 | 'dt': dt, 262 | 'status': _status 263 | } 264 | 265 | -------------------------------------------------------------------------------- /messaging/test/test_gsm_encoding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 2011 Sphere Systems Ltd 3 | # Author: Andrew Bird 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with this program; if not, write to the Free Software Foundation, Inc., 17 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | """Unittests for the gsm encoding/decoding module""" 19 | 20 | import unittest 21 | import messaging.sms.gsm0338 # imports GSM7 codec 22 | 23 | # Reversed from: ftp://ftp.unicode.org/Public/MAPPINGS/ETSI/GSM0338.TXT 24 | MAP = { 25 | # unichr(0x0000): (0x0000, 0x00), # Null 26 | u'@': (0x0040, 0x00), 27 | u'£': (0x00a3, 0x01), 28 | u'$': (0x0024, 0x02), 29 | u'¥': (0x00a5, 0x03), 30 | u'è': (0x00e8, 0x04), 31 | u'é': (0x00e9, 0x05), 32 | u'ù': (0x00f9, 0x06), 33 | u'ì': (0x00ec, 0x07), 34 | u'ò': (0x00f2, 0x08), 35 | u'Ç': (0x00c7, 0x09), # LATIN CAPITAL LETTER C WITH CEDILLA 36 | unichr(0x000a): (0x000a, 0x0a), # Linefeed 37 | u'Ø': (0x00d8, 0x0b), 38 | u'ø': (0x00f8, 0x0c), 39 | unichr(0x000d): (0x000d, 0x0d), # Carriage return 40 | u'Å': (0x00c5, 0x0e), 41 | u'å': (0x00e5, 0x0f), 42 | u'Δ': (0x0394, 0x10), 43 | u'_': (0x005f, 0x11), 44 | u'Φ': (0x03a6, 0x12), 45 | u'Γ': (0x0393, 0x13), 46 | u'Λ': (0x039b, 0x14), 47 | u'Ω': (0x03a9, 0x15), 48 | u'Π': (0x03a0, 0x16), 49 | u'Ψ': (0x03a8, 0x17), 50 | u'Σ': (0x03a3, 0x18), 51 | u'Θ': (0x0398, 0x19), 52 | u'Ξ': (0x039e, 0x1a), 53 | unichr(0x00a0): (0x00a0, 0x1b), # Escape to extension table (displayed 54 | # as NBSP, on decode of invalid escape 55 | # sequence) 56 | u'Æ': (0x00c6, 0x1c), 57 | u'æ': (0x00e6, 0x1d), 58 | u'ß': (0x00df, 0x1e), 59 | u'É': (0x00c9, 0x1f), 60 | u' ': (0x0020, 0x20), 61 | u'!': (0x0021, 0x21), 62 | u'"': (0x0022, 0x22), 63 | u'#': (0x0023, 0x23), 64 | u'¤': (0x00a4, 0x24), 65 | u'%': (0x0025, 0x25), 66 | u'&': (0x0026, 0x26), 67 | u'\'': (0x0027, 0x27), 68 | u'{': (0x007b, 0x1b28), 69 | u'}': (0x007d, 0x1b29), 70 | u'*': (0x002a, 0x2a), 71 | u'+': (0x002b, 0x2b), 72 | u',': (0x002c, 0x2c), 73 | u'-': (0x002d, 0x2d), 74 | u'.': (0x002e, 0x2e), 75 | u'\\': (0x005c, 0x1b2f), 76 | u'0': (0x0030, 0x30), 77 | u'1': (0x0031, 0x31), 78 | u'2': (0x0032, 0x32), 79 | u'3': (0x0033, 0x33), 80 | u'4': (0x0034, 0x34), 81 | u'5': (0x0035, 0x35), 82 | u'6': (0x0036, 0x36), 83 | u'7': (0x0037, 0x37), 84 | u'8': (0x0038, 0x38), 85 | u'9': (0x0039, 0x39), 86 | u':': (0x003a, 0x3a), 87 | u';': (0x003b, 0x3b), 88 | u'[': (0x005b, 0x1b3c), 89 | unichr(0x000c): (0x000c, 0x1b0a), # Formfeed 90 | u']': (0x005d, 0x1b3e), 91 | u'?': (0x003f, 0x3f), 92 | u'|': (0x007c, 0x1b40), 93 | u'A': (0x0041, 0x41), 94 | u'B': (0x0042, 0x42), 95 | u'C': (0x0043, 0x43), 96 | u'D': (0x0044, 0x44), 97 | u'E': (0x0045, 0x45), 98 | u'F': (0x0046, 0x46), 99 | u'G': (0x0047, 0x47), 100 | u'H': (0x0048, 0x48), 101 | u'I': (0x0049, 0x49), 102 | u'J': (0x004a, 0x4a), 103 | u'K': (0x004b, 0x4b), 104 | u'L': (0x004c, 0x4c), 105 | u'M': (0x004d, 0x4d), 106 | u'N': (0x004e, 0x4e), 107 | u'O': (0x004f, 0x4f), 108 | u'P': (0x0050, 0x50), 109 | u'Q': (0x0051, 0x51), 110 | u'R': (0x0052, 0x52), 111 | u'S': (0x0053, 0x53), 112 | u'T': (0x0054, 0x54), 113 | u'U': (0x0055, 0x55), 114 | u'V': (0x0056, 0x56), 115 | u'W': (0x0057, 0x57), 116 | u'X': (0x0058, 0x58), 117 | u'Y': (0x0059, 0x59), 118 | u'Z': (0x005a, 0x5a), 119 | u'Ä': (0x00c4, 0x5b), 120 | u'Ö': (0x00d6, 0x5c), 121 | u'Ñ': (0x00d1, 0x5d), 122 | u'Ü': (0x00dc, 0x5e), 123 | u'§': (0x00a7, 0x5f), 124 | u'¿': (0x00bf, 0x60), 125 | u'a': (0x0061, 0x61), 126 | u'b': (0x0062, 0x62), 127 | u'c': (0x0063, 0x63), 128 | u'd': (0x0064, 0x64), 129 | u'€': (0x20ac, 0x1b65), 130 | u'f': (0x0066, 0x66), 131 | u'g': (0x0067, 0x67), 132 | u'h': (0x0068, 0x68), 133 | u'<': (0x003c, 0x3c), 134 | u'j': (0x006a, 0x6a), 135 | u'k': (0x006b, 0x6b), 136 | u'l': (0x006c, 0x6c), 137 | u'm': (0x006d, 0x6d), 138 | u'n': (0x006e, 0x6e), 139 | u'~': (0x007e, 0x1b3d), 140 | u'p': (0x0070, 0x70), 141 | u'q': (0x0071, 0x71), 142 | u'r': (0x0072, 0x72), 143 | u's': (0x0073, 0x73), 144 | u't': (0x0074, 0x74), 145 | u'>': (0x003e, 0x3e), 146 | u'v': (0x0076, 0x76), 147 | u'i': (0x0069, 0x69), 148 | u'x': (0x0078, 0x78), 149 | u'^': (0x005e, 0x1b14), 150 | u'z': (0x007a, 0x7a), 151 | u'ä': (0x00e4, 0x7b), 152 | u'ö': (0x00f6, 0x7c), 153 | u'ñ': (0x00f1, 0x7d), 154 | u'ü': (0x00fc, 0x7e), 155 | u'à': (0x00e0, 0x7f), 156 | u'¡': (0x00a1, 0x40), 157 | u'/': (0x002f, 0x2f), 158 | u'o': (0x006f, 0x6f), 159 | u'u': (0x0075, 0x75), 160 | u'w': (0x0077, 0x77), 161 | u'y': (0x0079, 0x79), 162 | u'e': (0x0065, 0x65), 163 | u'=': (0x003d, 0x3d), 164 | u'(': (0x0028, 0x28), 165 | u')': (0x0029, 0x29), 166 | } 167 | 168 | GREEK_MAP = { # Note: these might look like Latin uppercase, but they aren't 169 | u'Α': (0x0391, 0x41), 170 | u'Β': (0x0392, 0x42), 171 | u'Ε': (0x0395, 0x45), 172 | u'Η': (0x0397, 0x48), 173 | u'Ι': (0x0399, 0x49), 174 | u'Κ': (0x039a, 0x4b), 175 | u'Μ': (0x039c, 0x4d), 176 | u'Ν': (0x039d, 0x4e), 177 | u'Ο': (0x039f, 0x4f), 178 | u'Ρ': (0x03a1, 0x50), 179 | u'Τ': (0x03a4, 0x54), 180 | u'Χ': (0x03a7, 0x58), 181 | u'Υ': (0x03a5, 0x59), 182 | u'Ζ': (0x0396, 0x5a), 183 | } 184 | 185 | QUIRK_MAP = { 186 | u'ç': (0x00e7, 0x09), 187 | } 188 | 189 | BAD = -1 190 | 191 | 192 | class TestEncodingFunctions(unittest.TestCase): 193 | 194 | def test_encoding_supported_unicode_gsm(self): 195 | 196 | for key in MAP.keys(): 197 | # Use 'ignore' so that we see the code tested, not an exception 198 | s_gsm = key.encode('gsm0338', 'ignore') 199 | 200 | if len(s_gsm) == 1: 201 | i_gsm = ord(s_gsm) 202 | elif len(s_gsm) == 2: 203 | i_gsm = (ord(s_gsm[0]) << 8) + ord(s_gsm[1]) 204 | else: 205 | i_gsm = BAD # so we see the comparison, not an exception 206 | 207 | # We shouldn't generate an invalid escape sequence 208 | if key == unichr(0x00a0): 209 | self.assertEqual(BAD, i_gsm) 210 | else: 211 | self.assertEqual(MAP[key][1], i_gsm) 212 | 213 | def test_encoding_supported_greek_unicode_gsm(self): 214 | # Note: Conversion is one way, hence no corresponding decode test 215 | 216 | for key in GREEK_MAP.keys(): 217 | # Use 'replace' so that we trigger the mapping 218 | s_gsm = key.encode('gsm0338', 'replace') 219 | 220 | if len(s_gsm) == 1: 221 | i_gsm = ord(s_gsm) 222 | else: 223 | i_gsm = BAD # so we see the comparison, not an exception 224 | 225 | self.assertEqual(GREEK_MAP[key][1], i_gsm) 226 | 227 | def test_encoding_supported_quirk_unicode_gsm(self): 228 | # Note: Conversion is one way, hence no corresponding decode test 229 | 230 | for key in QUIRK_MAP.keys(): 231 | # Use 'replace' so that we trigger the mapping 232 | s_gsm = key.encode('gsm0338', 'replace') 233 | 234 | if len(s_gsm) == 1: 235 | i_gsm = ord(s_gsm) 236 | else: 237 | i_gsm = BAD # so we see the comparison, not an exception 238 | 239 | self.assertEqual(QUIRK_MAP[key][1], i_gsm) 240 | 241 | def test_decoding_supported_unicode_gsm(self): 242 | for key in MAP.keys(): 243 | i_gsm = MAP[key][1] 244 | if i_gsm <= 0xff: 245 | s_gsm = chr(i_gsm) 246 | elif i_gsm <= 0xffff: 247 | s_gsm = chr((i_gsm & 0xff00) >> 8) 248 | s_gsm += chr(i_gsm & 0x00ff) 249 | 250 | s_unicode = s_gsm.decode('gsm0338', 'strict') 251 | self.assertEqual(MAP[key][0], ord(s_unicode)) 252 | 253 | def test_is_gsm_text_true(self): 254 | for key in MAP.keys(): 255 | if key == unichr(0x00a0): 256 | continue 257 | self.assertEqual(messaging.sms.gsm0338.is_gsm_text(key), True) 258 | 259 | def test_is_gsm_text_false(self): 260 | self.assertEqual( 261 | messaging.sms.gsm0338.is_gsm_text(unichr(0x00a0)), False) 262 | 263 | for i in xrange(1, 0xffff + 1): 264 | if unichr(i) not in MAP: 265 | # Note: it's a little odd, but on error we want to see values 266 | if messaging.sms.gsm0338.is_gsm_text(unichr(i)) is not False: 267 | self.assertEqual(BAD, i) 268 | -------------------------------------------------------------------------------- /messaging/sms/submit.py: -------------------------------------------------------------------------------- 1 | # See LICENSE 2 | """Classes for sending SMS""" 3 | 4 | from datetime import datetime, timedelta 5 | import re 6 | 7 | from messaging.sms import consts 8 | from messaging.utils import (debug, encode_str, clean_number, 9 | pack_8bits_to_ucs2, pack_8bits_to_7bits, 10 | pack_8bits_to_8bit, 11 | timedelta_to_relative_validity, 12 | datetime_to_absolute_validity) 13 | from messaging.sms.base import SmsBase 14 | from messaging.sms.gsm0338 import is_gsm_text 15 | from messaging.sms.pdu import Pdu 16 | 17 | VALID_NUMBER = re.compile("^\+?\d{3,20}$") 18 | 19 | 20 | class SmsSubmit(SmsBase): 21 | """I am a SMS ready to be sent""" 22 | 23 | def __init__(self, number, text): 24 | super(SmsSubmit, self).__init__() 25 | self._number = None 26 | self._csca = None 27 | self._klass = None 28 | self._validity = None 29 | self.request_status = False 30 | self.ref = None 31 | self.rand_id = None 32 | self.id_list = range(0, 255) 33 | self.msgvp = 0xaa 34 | self.pid = 0x00 35 | 36 | self.number = number 37 | self.text = text 38 | self.text_gsm = None 39 | 40 | def _set_number(self, number): 41 | if number and not VALID_NUMBER.match(number): 42 | raise ValueError("Invalid number format: %s" % number) 43 | 44 | self._number = number 45 | 46 | number = property(lambda self: self._number, _set_number) 47 | 48 | def _set_csca(self, csca): 49 | if csca and not VALID_NUMBER.match(csca): 50 | raise ValueError("Invalid csca format: %s" % csca) 51 | 52 | self._csca = csca 53 | 54 | csca = property(lambda self: self._csca, _set_csca) 55 | 56 | def _set_validity(self, validity): 57 | if validity is None or isinstance(validity, (timedelta, datetime)): 58 | # valid values are None, timedelta and datetime 59 | self._validity = validity 60 | else: 61 | raise TypeError("Don't know what to do with %s" % validity) 62 | 63 | validity = property(lambda self: self._validity, _set_validity) 64 | 65 | def _set_klass(self, klass): 66 | if not isinstance(klass, int): 67 | raise TypeError("_set_klass only accepts int objects") 68 | 69 | if klass not in [0, 1, 2, 3]: 70 | raise ValueError("class must be between 0 and 3") 71 | 72 | self._klass = klass 73 | 74 | klass = property(lambda self: self._klass, _set_klass) 75 | 76 | def to_pdu(self): 77 | """Returns a list of :class:`~messaging.pdu.Pdu` objects""" 78 | smsc_pdu = self._get_smsc_pdu() 79 | sms_submit_pdu = self._get_sms_submit_pdu() 80 | tpmessref_pdu = self._get_tpmessref_pdu() 81 | sms_phone_pdu = self._get_phone_pdu() 82 | tppid_pdu = self._get_tppid_pdu() 83 | sms_msg_pdu = self._get_msg_pdu() 84 | 85 | if len(sms_msg_pdu) == 1: 86 | pdu = smsc_pdu 87 | len_smsc = len(smsc_pdu) / 2 88 | pdu += sms_submit_pdu 89 | pdu += tpmessref_pdu 90 | pdu += sms_phone_pdu 91 | pdu += tppid_pdu 92 | pdu += sms_msg_pdu[0] 93 | debug("smsc_pdu: %s" % smsc_pdu) 94 | debug("sms_submit_pdu: %s" % sms_submit_pdu) 95 | debug("tpmessref_pdu: %s" % tpmessref_pdu) 96 | debug("sms_phone_pdu: %s" % sms_phone_pdu) 97 | debug("tppid_pdu: %s" % tppid_pdu) 98 | debug("sms_msg_pdu: %s" % sms_msg_pdu) 99 | debug("-" * 20) 100 | debug("full_pdu: %s" % pdu) 101 | debug("full_text: %s" % self.text) 102 | debug("-" * 20) 103 | return [Pdu(pdu, len_smsc)] 104 | 105 | # multipart SMS 106 | sms_submit_pdu = self._get_sms_submit_pdu(udh=True) 107 | pdu_list = [] 108 | cnt = len(sms_msg_pdu) 109 | for i, sms_msg_pdu_item in enumerate(sms_msg_pdu): 110 | pdu = smsc_pdu 111 | len_smsc = len(smsc_pdu) / 2 112 | pdu += sms_submit_pdu 113 | pdu += tpmessref_pdu 114 | pdu += sms_phone_pdu 115 | pdu += tppid_pdu 116 | pdu += sms_msg_pdu_item 117 | debug("smsc_pdu: %s" % smsc_pdu) 118 | debug("sms_submit_pdu: %s" % sms_submit_pdu) 119 | debug("tpmessref_pdu: %s" % tpmessref_pdu) 120 | debug("sms_phone_pdu: %s" % sms_phone_pdu) 121 | debug("tppid_pdu: %s" % tppid_pdu) 122 | debug("sms_msg_pdu: %s" % sms_msg_pdu_item) 123 | debug("-" * 20) 124 | debug("full_pdu: %s" % pdu) 125 | debug("full_text: %s" % self.text) 126 | debug("-" * 20) 127 | 128 | pdu_list.append(Pdu(pdu, len_smsc, cnt=cnt, seq=i + 1)) 129 | 130 | return pdu_list 131 | 132 | def _get_smsc_pdu(self): 133 | if not self.csca or not self.csca.strip(): 134 | return "00" 135 | 136 | number = clean_number(self.csca) 137 | ptype = 0x81 # set to unknown number by default 138 | if number[0] == '+': 139 | number = number[1:] 140 | ptype = 0x91 141 | 142 | if len(number) % 2: 143 | number += 'F' 144 | 145 | ps = chr(ptype) 146 | for n in range(0, len(number), 2): 147 | num = number[n + 1] + number[n] 148 | ps += chr(int(num, 16)) 149 | 150 | pl = len(ps) 151 | ps = chr(pl) + ps 152 | 153 | return encode_str(ps) 154 | 155 | def _get_tpmessref_pdu(self): 156 | if self.ref is None: 157 | self.ref = self._get_rand_id() 158 | 159 | self.ref &= 0xFF 160 | return encode_str(chr(self.ref)) 161 | 162 | def _get_phone_pdu(self): 163 | number = clean_number(self.number) 164 | ptype = 0x81 165 | if number[0] == '+': 166 | number = number[1:] 167 | ptype = 0x91 168 | 169 | pl = len(number) 170 | if len(number) % 2: 171 | number += 'F' 172 | 173 | ps = chr(ptype) 174 | for n in range(0, len(number), 2): 175 | num = number[n + 1] + number[n] 176 | ps += chr(int(num, 16)) 177 | 178 | ps = chr(pl) + ps 179 | return encode_str(ps) 180 | 181 | def _get_tppid_pdu(self): 182 | return encode_str(chr(self.pid)) 183 | 184 | def _get_sms_submit_pdu(self, udh=False): 185 | sms_submit = 0x1 186 | if self.validity is None: 187 | # handle no validity 188 | pass 189 | elif isinstance(self.validity, datetime): 190 | # handle absolute validity 191 | sms_submit |= 0x18 192 | elif isinstance(self.validity, timedelta): 193 | # handle relative validity 194 | sms_submit |= 0x10 195 | 196 | if self.request_status: 197 | sms_submit |= 0x20 198 | 199 | if udh: 200 | sms_submit |= 0x40 201 | 202 | return encode_str(chr(sms_submit)) 203 | 204 | def _get_msg_pdu(self): 205 | # Data coding scheme 206 | if self.fmt is None: 207 | if is_gsm_text(self.text): 208 | self.fmt = 0x00 209 | else: 210 | self.fmt = 0x08 211 | 212 | self.dcs = self.fmt 213 | 214 | if self.klass is not None: 215 | if self.klass == 0: 216 | self.dcs |= 0x10 217 | elif self.klass == 1: 218 | self.dcs |= 0x11 219 | elif self.klass == 2: 220 | self.dcs |= 0x12 221 | elif self.klass == 3: 222 | self.dcs |= 0x13 223 | 224 | dcs_pdu = encode_str(chr(self.dcs)) 225 | 226 | # Validity period 227 | msgvp_pdu = "" 228 | if self.validity is None: 229 | # handle no validity 230 | pass 231 | 232 | elif isinstance(self.validity, timedelta): 233 | # handle relative 234 | msgvp = timedelta_to_relative_validity(self.validity) 235 | msgvp_pdu = encode_str(chr(msgvp)) 236 | 237 | elif isinstance(self.validity, datetime): 238 | # handle absolute 239 | msgvp = datetime_to_absolute_validity(self.validity) 240 | msgvp_pdu = ''.join(map(encode_str, map(chr, msgvp))) 241 | 242 | # UDL + UD 243 | message_pdu = "" 244 | 245 | if self.fmt == 0x00: 246 | self.text_gsm = self.text.encode("gsm0338") 247 | if len(self.text_gsm) <= consts.SEVENBIT_SIZE: 248 | message_pdu = [pack_8bits_to_7bits(self.text_gsm)] 249 | else: 250 | message_pdu = self._split_sms_message(self.text_gsm) 251 | elif self.fmt == 0x04: 252 | if len(self.text) <= consts.EIGHTBIT_SIZE: 253 | message_pdu = [pack_8bits_to_8bit(self.text)] 254 | else: 255 | message_pdu = self._split_sms_message(self.text) 256 | elif self.fmt == 0x08: 257 | if len(self.text) <= consts.UCS2_SIZE: 258 | message_pdu = [pack_8bits_to_ucs2(self.text)] 259 | else: 260 | message_pdu = self._split_sms_message(self.text) 261 | else: 262 | raise ValueError("Unknown data coding scheme: %d" % self.fmt) 263 | 264 | ret = [] 265 | for msg in message_pdu: 266 | ret.append(dcs_pdu + msgvp_pdu + msg) 267 | 268 | return ret 269 | 270 | def _split_sms_message(self, text): 271 | if self.fmt == 0x00: 272 | len_without_udh = consts.SEVENBIT_MP_SIZE 273 | limit = consts.SEVENBIT_SIZE 274 | packing_func = pack_8bits_to_7bits 275 | total_len = len(self.text_gsm) 276 | 277 | elif self.fmt == 0x04: 278 | len_without_udh = consts.EIGHTBIT_MP_SIZE 279 | limit = consts.EIGHTBIT_SIZE 280 | packing_func = pack_8bits_to_8bit 281 | total_len = len(self.text) 282 | 283 | elif self.fmt == 0x08: 284 | len_without_udh = consts.UCS2_MP_SIZE 285 | limit = consts.UCS2_SIZE 286 | packing_func = pack_8bits_to_ucs2 287 | total_len = len(self.text) 288 | 289 | msgs = [] 290 | pi, pe = 0, len_without_udh 291 | 292 | while pi < total_len: 293 | if text[pi:pe][-1] == '\x1b': 294 | pe -= 1 295 | 296 | msgs.append(text[pi:pe]) 297 | pi = pe 298 | pe += len_without_udh 299 | 300 | pdu_msgs = [] 301 | 302 | udh_len = 0x05 303 | mid = 0x00 304 | data_len = 0x03 305 | 306 | sms_ref = self._get_rand_id() if self.rand_id is None else self.rand_id 307 | sms_ref &= 0xFF 308 | 309 | for i, msg in enumerate(msgs): 310 | i += 1 311 | total_parts = len(msgs) 312 | if limit == consts.SEVENBIT_SIZE: 313 | udh = (chr(udh_len) + chr(mid) + chr(data_len) + 314 | chr(sms_ref) + chr(total_parts) + chr(i)) 315 | padding = " " 316 | else: 317 | udh = (unichr(int("%04x" % ((udh_len << 8) | mid), 16)) + 318 | unichr(int("%04x" % ((data_len << 8) | sms_ref), 16)) + 319 | unichr(int("%04x" % ((total_parts << 8) | i), 16))) 320 | padding = "" 321 | 322 | pdu_msgs.append(packing_func(padding + msg, udh)) 323 | 324 | return pdu_msgs 325 | 326 | def _get_rand_id(self): 327 | if not self.id_list: 328 | self.id_list = range(0, 255) 329 | 330 | return self.id_list.pop(0) 331 | -------------------------------------------------------------------------------- /messaging/sms/gsm0338.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or modify 2 | # it under the terms of the GNU General Public License as published by 3 | # the Free Software Foundation; either version 2 of the License, or 4 | # (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License along 12 | # with this program; if not, write to the Free Software Foundation, Inc., 13 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 14 | 15 | import codecs 16 | import sys 17 | import traceback 18 | 19 | # data from 20 | # http://snoops.roy202.org/testerman/browser/trunk/plugins/codecs/gsm0338.py 21 | 22 | # default GSM 03.38 -> unicode 23 | def_regular_decode_dict = { 24 | '\x00': u'\u0040', # COMMERCIAL AT 25 | '\x01': u'\u00A3', # POUND SIGN 26 | '\x02': u'\u0024', # DOLLAR SIGN 27 | '\x03': u'\u00A5', # YEN SIGN 28 | '\x04': u'\u00E8', # LATIN SMALL LETTER E WITH GRAVE 29 | '\x05': u'\u00E9', # LATIN SMALL LETTER E WITH ACUTE 30 | '\x06': u'\u00F9', # LATIN SMALL LETTER U WITH GRAVE 31 | '\x07': u'\u00EC', # LATIN SMALL LETTER I WITH GRAVE 32 | '\x08': u'\u00F2', # LATIN SMALL LETTER O WITH GRAVE 33 | '\x09': u'\u00C7', # LATIN CAPITAL LETTER C WITH CEDILLA 34 | # The Unicode page suggests this is a mistake: but 35 | # it's still in the latest version of the spec and 36 | # our implementation has to be exact. 37 | 38 | '\x0A': u'\u000A', # LINE FEED 39 | '\x0B': u'\u00D8', # LATIN CAPITAL LETTER O WITH STROKE 40 | '\x0C': u'\u00F8', # LATIN SMALL LETTER O WITH STROKE 41 | '\x0D': u'\u000D', # CARRIAGE RETURN 42 | '\x0E': u'\u00C5', # LATIN CAPITAL LETTER A WITH RING ABOVE 43 | '\x0F': u'\u00E5', # LATIN SMALL LETTER A WITH RING ABOVE 44 | '\x10': u'\u0394', # GREEK CAPITAL LETTER DELTA 45 | '\x11': u'\u005F', # LOW LINE 46 | '\x12': u'\u03A6', # GREEK CAPITAL LETTER PHI 47 | '\x13': u'\u0393', # GREEK CAPITAL LETTER GAMMA 48 | '\x14': u'\u039B', # GREEK CAPITAL LETTER LAMDA 49 | '\x15': u'\u03A9', # GREEK CAPITAL LETTER OMEGA 50 | '\x16': u'\u03A0', # GREEK CAPITAL LETTER PI 51 | '\x17': u'\u03A8', # GREEK CAPITAL LETTER PSI 52 | '\x18': u'\u03A3', # GREEK CAPITAL LETTER SIGMA 53 | '\x19': u'\u0398', # GREEK CAPITAL LETTER THETA 54 | '\x1A': u'\u039E', # GREEK CAPITAL LETTER XI 55 | '\x1C': u'\u00C6', # LATIN CAPITAL LETTER AE 56 | '\x1D': u'\u00E6', # LATIN SMALL LETTER AE 57 | '\x1E': u'\u00DF', # LATIN SMALL LETTER SHARP S (German) 58 | '\x1F': u'\u00C9', # LATIN CAPITAL LETTER E WITH ACUTE 59 | '\x20': u'\u0020', # SPACE 60 | '\x21': u'\u0021', # EXCLAMATION MARK 61 | '\x22': u'\u0022', # QUOTATION MARK 62 | '\x23': u'\u0023', # NUMBER SIGN 63 | '\x24': u'\u00A4', # CURRENCY SIGN 64 | '\x25': u'\u0025', # PERCENT SIGN 65 | '\x26': u'\u0026', # AMPERSAND 66 | '\x27': u'\u0027', # APOSTROPHE 67 | '\x28': u'\u0028', # LEFT PARENTHESIS 68 | '\x29': u'\u0029', # RIGHT PARENTHESIS 69 | '\x2A': u'\u002A', # ASTERISK 70 | '\x2B': u'\u002B', # PLUS SIGN 71 | '\x2C': u'\u002C', # COMMA 72 | '\x2D': u'\u002D', # HYPHEN-MINUS 73 | '\x2E': u'\u002E', # FULL STOP 74 | '\x2F': u'\u002F', # SOLIDUS 75 | '\x30': u'\u0030', # DIGIT ZERO 76 | '\x31': u'\u0031', # DIGIT ONE 77 | '\x32': u'\u0032', # DIGIT TWO 78 | '\x33': u'\u0033', # DIGIT THREE 79 | '\x34': u'\u0034', # DIGIT FOUR 80 | '\x35': u'\u0035', # DIGIT FIVE 81 | '\x36': u'\u0036', # DIGIT SIX 82 | '\x37': u'\u0037', # DIGIT SEVEN 83 | '\x38': u'\u0038', # DIGIT EIGHT 84 | '\x39': u'\u0039', # DIGIT NINE 85 | '\x3A': u'\u003A', # COLON 86 | '\x3B': u'\u003B', # SEMICOLON 87 | '\x3C': u'\u003C', # LESS-THAN SIGN 88 | '\x3D': u'\u003D', # EQUALS SIGN 89 | '\x3E': u'\u003E', # GREATER-THAN SIGN 90 | '\x3F': u'\u003F', # QUESTION MARK 91 | '\x40': u'\u00A1', # INVERTED EXCLAMATION MARK 92 | '\x41': u'\u0041', # LATIN CAPITAL LETTER A 93 | '\x42': u'\u0042', # LATIN CAPITAL LETTER B 94 | '\x43': u'\u0043', # LATIN CAPITAL LETTER C 95 | '\x44': u'\u0044', # LATIN CAPITAL LETTER D 96 | '\x45': u'\u0045', # LATIN CAPITAL LETTER E 97 | '\x46': u'\u0046', # LATIN CAPITAL LETTER F 98 | '\x47': u'\u0047', # LATIN CAPITAL LETTER G 99 | '\x48': u'\u0048', # LATIN CAPITAL LETTER H 100 | '\x49': u'\u0049', # LATIN CAPITAL LETTER I 101 | '\x4A': u'\u004A', # LATIN CAPITAL LETTER J 102 | '\x4B': u'\u004B', # LATIN CAPITAL LETTER K 103 | '\x4C': u'\u004C', # LATIN CAPITAL LETTER L 104 | '\x4D': u'\u004D', # LATIN CAPITAL LETTER M 105 | '\x4E': u'\u004E', # LATIN CAPITAL LETTER N 106 | '\x4F': u'\u004F', # LATIN CAPITAL LETTER O 107 | '\x50': u'\u0050', # LATIN CAPITAL LETTER P 108 | '\x51': u'\u0051', # LATIN CAPITAL LETTER Q 109 | '\x52': u'\u0052', # LATIN CAPITAL LETTER R 110 | '\x53': u'\u0053', # LATIN CAPITAL LETTER S 111 | '\x54': u'\u0054', # LATIN CAPITAL LETTER T 112 | '\x55': u'\u0055', # LATIN CAPITAL LETTER U 113 | '\x56': u'\u0056', # LATIN CAPITAL LETTER V 114 | '\x57': u'\u0057', # LATIN CAPITAL LETTER W 115 | '\x58': u'\u0058', # LATIN CAPITAL LETTER X 116 | '\x59': u'\u0059', # LATIN CAPITAL LETTER Y 117 | '\x5A': u'\u005A', # LATIN CAPITAL LETTER Z 118 | '\x5B': u'\u00C4', # LATIN CAPITAL LETTER A WITH DIAERESIS 119 | '\x5C': u'\u00D6', # LATIN CAPITAL LETTER O WITH DIAERESIS 120 | '\x5D': u'\u00D1', # LATIN CAPITAL LETTER N WITH TILDE 121 | '\x5E': u'\u00DC', # LATIN CAPITAL LETTER U WITH DIAERESIS 122 | '\x5F': u'\u00A7', # SECTION SIGN 123 | '\x60': u'\u00BF', # INVERTED QUESTION MARK 124 | '\x61': u'\u0061', # LATIN SMALL LETTER A 125 | '\x62': u'\u0062', # LATIN SMALL LETTER B 126 | '\x63': u'\u0063', # LATIN SMALL LETTER C 127 | '\x64': u'\u0064', # LATIN SMALL LETTER D 128 | '\x65': u'\u0065', # LATIN SMALL LETTER E 129 | '\x66': u'\u0066', # LATIN SMALL LETTER F 130 | '\x67': u'\u0067', # LATIN SMALL LETTER G 131 | '\x68': u'\u0068', # LATIN SMALL LETTER H 132 | '\x69': u'\u0069', # LATIN SMALL LETTER I 133 | '\x6A': u'\u006A', # LATIN SMALL LETTER J 134 | '\x6B': u'\u006B', # LATIN SMALL LETTER K 135 | '\x6C': u'\u006C', # LATIN SMALL LETTER L 136 | '\x6D': u'\u006D', # LATIN SMALL LETTER M 137 | '\x6E': u'\u006E', # LATIN SMALL LETTER N 138 | '\x6F': u'\u006F', # LATIN SMALL LETTER O 139 | '\x70': u'\u0070', # LATIN SMALL LETTER P 140 | '\x71': u'\u0071', # LATIN SMALL LETTER Q 141 | '\x72': u'\u0072', # LATIN SMALL LETTER R 142 | '\x73': u'\u0073', # LATIN SMALL LETTER S 143 | '\x74': u'\u0074', # LATIN SMALL LETTER T 144 | '\x75': u'\u0075', # LATIN SMALL LETTER U 145 | '\x76': u'\u0076', # LATIN SMALL LETTER V 146 | '\x77': u'\u0077', # LATIN SMALL LETTER W 147 | '\x78': u'\u0078', # LATIN SMALL LETTER X 148 | '\x79': u'\u0079', # LATIN SMALL LETTER Y 149 | '\x7A': u'\u007A', # LATIN SMALL LETTER Z 150 | '\x7B': u'\u00E4', # LATIN SMALL LETTER A WITH DIAERESIS 151 | '\x7C': u'\u00F6', # LATIN SMALL LETTER O WITH DIAERESIS 152 | '\x7D': u'\u00F1', # LATIN SMALL LETTER N WITH TILDE 153 | '\x7E': u'\u00FC', # LATIN SMALL LETTER U WITH DIAERESIS 154 | '\x7F': u'\u00E0', # LATIN SMALL LETTER A WITH GRAVE 155 | } 156 | 157 | # default GSM 03.38 escaped characters -> unicode 158 | def_escape_decode_dict = { 159 | '\x0A': u'\u000C', # FORM FEED 160 | '\x14': u'\u005E', # CIRCUMFLEX ACCENT 161 | '\x28': u'\u007B', # LEFT CURLY BRACKET 162 | '\x29': u'\u007D', # RIGHT CURLY BRACKET 163 | '\x2F': u'\u005C', # REVERSE SOLIDUS 164 | '\x3C': u'\u005B', # LEFT SQUARE BRACKET 165 | '\x3D': u'\u007E', # TILDE 166 | '\x3E': u'\u005D', # RIGHT SQUARE BRACKET 167 | '\x40': u'\u007C', # VERTICAL LINE 168 | '\x65': u'\u20AC', # EURO SIGN 169 | } 170 | 171 | # Replacement characters, default is question mark. Used when it is not too 172 | # important to ensure exact UTF-8 -> GSM -> UTF-8 equivilence, such as when 173 | # humans read and write SMS. But for USSD and other M2M applications it's 174 | # important to ensure the conversion is exact. 175 | def_replace_encode_dict = { 176 | u'\u00E7': '\x09', # LATIN SMALL LETTER C WITH CEDILLA 177 | 178 | u'\u0391': '\x41', # GREEK CAPITAL LETTER ALPHA 179 | u'\u0392': '\x42', # GREEK CAPITAL LETTER BETA 180 | u'\u0395': '\x45', # GREEK CAPITAL LETTER EPSILON 181 | u'\u0397': '\x48', # GREEK CAPITAL LETTER ETA 182 | u'\u0399': '\x49', # GREEK CAPITAL LETTER IOTA 183 | u'\u039A': '\x4B', # GREEK CAPITAL LETTER KAPPA 184 | u'\u039C': '\x4D', # GREEK CAPITAL LETTER MU 185 | u'\u039D': '\x4E', # GREEK CAPITAL LETTER NU 186 | u'\u039F': '\x4F', # GREEK CAPITAL LETTER OMICRON 187 | u'\u03A1': '\x50', # GREEK CAPITAL LETTER RHO 188 | u'\u03A4': '\x54', # GREEK CAPITAL LETTER TAU 189 | u'\u03A7': '\x58', # GREEK CAPITAL LETTER CHI 190 | u'\u03A5': '\x59', # GREEK CAPITAL LETTER UPSILON 191 | u'\u0396': '\x5A', # GREEK CAPITAL LETTER ZETA 192 | } 193 | 194 | QUESTION_MARK = chr(0x3f) 195 | 196 | # unicode -> default GSM 03.38 197 | def_regular_encode_dict = \ 198 | dict((u, g) for g, u in def_regular_decode_dict.iteritems()) 199 | 200 | # unicode -> default escaped GSM 03.38 characters 201 | def_escape_encode_dict = \ 202 | dict((u, g) for g, u in def_escape_decode_dict.iteritems()) 203 | 204 | 205 | def encode(input_, errors='strict'): 206 | """ 207 | :type input_: unicode 208 | 209 | :return: string 210 | """ 211 | result = [] 212 | for c in input_: 213 | try: 214 | result.append(def_regular_encode_dict[c]) 215 | except KeyError: 216 | if c in def_escape_encode_dict: 217 | # OK, let's encode it as an escaped characters 218 | result.append('\x1b') 219 | result.append(def_escape_encode_dict[c]) 220 | else: 221 | if errors == 'strict': 222 | raise UnicodeError("Invalid GSM character") 223 | elif errors == 'replace': 224 | result.append( 225 | def_replace_encode_dict.get(c, QUESTION_MARK)) 226 | elif errors == 'ignore': 227 | pass 228 | else: 229 | raise UnicodeError("Unknown error handling") 230 | 231 | ret = ''.join(result) 232 | return ret, len(ret) 233 | 234 | 235 | def decode(input_, errors='strict'): 236 | """ 237 | :type input_: str 238 | 239 | :return: unicode 240 | """ 241 | result = [] 242 | index = 0 243 | while index < len(input_): 244 | c = input_[index] 245 | index += 1 246 | if c == '\x1b': 247 | if index < len(input_): 248 | c = input_[index] 249 | index += 1 250 | result.append(def_escape_decode_dict.get(c, u'\xa0')) 251 | else: 252 | result.append(u'\xa0') 253 | else: 254 | try: 255 | result.append(def_regular_decode_dict[c]) 256 | except KeyError: 257 | # error handling: unassigned byte, must be > 0x7f 258 | if errors == 'strict': 259 | raise UnicodeError("Unrecognized GSM character") 260 | elif errors == 'replace': 261 | result.append('?') 262 | elif errors == 'ignore': 263 | pass 264 | else: 265 | raise UnicodeError("Unknown error handling") 266 | 267 | ret = u''.join(result) 268 | return ret, len(ret) 269 | 270 | 271 | # encodings module API 272 | def getregentry(encoding): 273 | if encoding == 'gsm0338': 274 | return codecs.CodecInfo(name='gsm0338', 275 | encode=encode, 276 | decode=decode) 277 | 278 | # Codec registration 279 | codecs.register(getregentry) 280 | 281 | 282 | def is_gsm_text(text): 283 | """Returns True if ``text`` can be encoded as gsm text""" 284 | try: 285 | text.encode("gsm0338") 286 | except UnicodeError: 287 | return False 288 | except: 289 | traceback.print_exc(file=sys.stdout) 290 | return False 291 | 292 | return True 293 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /messaging/test/test_sms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime, timedelta 3 | 4 | try: 5 | import unittest2 as unittest 6 | except ImportError: 7 | import unittest 8 | 9 | from messaging.sms import SmsSubmit, SmsDeliver 10 | from messaging.utils import (timedelta_to_relative_validity as to_relative, 11 | datetime_to_absolute_validity as to_absolute, 12 | FixedOffset) 13 | 14 | 15 | class TestEncodingFunctions(unittest.TestCase): 16 | 17 | def test_converting_timedelta_to_validity(self): 18 | self.assertRaises(ValueError, to_relative, timedelta(minutes=4)) 19 | self.assertRaises(ValueError, to_relative, timedelta(weeks=64)) 20 | 21 | self.assertTrue(isinstance(to_relative(timedelta(hours=6)), int)) 22 | self.assertTrue(isinstance(to_relative(timedelta(hours=18)), int)) 23 | self.assertTrue(isinstance(to_relative(timedelta(days=15)), int)) 24 | self.assertTrue(isinstance(to_relative(timedelta(weeks=31)), int)) 25 | 26 | self.assertEqual(to_relative(timedelta(minutes=5)), 0) 27 | self.assertEqual(to_relative(timedelta(minutes=6)), 0) 28 | self.assertEqual(to_relative(timedelta(minutes=10)), 1) 29 | 30 | self.assertEqual(to_relative(timedelta(hours=12)), 143) 31 | self.assertEqual(to_relative(timedelta(hours=13)), 145) 32 | self.assertEqual(to_relative(timedelta(hours=24)), 167) 33 | 34 | self.assertEqual(to_relative(timedelta(days=2)), 168) 35 | self.assertEqual(to_relative(timedelta(days=30)), 196) 36 | 37 | def test_converting_datetime_to_validity(self): 38 | # http://www.dreamfabric.com/sms/scts.html 39 | # 12. Feb 1999 05:57:30 GMT+3 40 | when = datetime(1999, 2, 12, 5, 57, 30, 0, 41 | FixedOffset(3 * 60, "GMT+3")) 42 | expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x21] 43 | self.assertEqual(to_absolute(when, "GMT+3"), expected) 44 | 45 | when = datetime(1999, 2, 12, 5, 57, 30, 0) 46 | expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x0] 47 | self.assertEqual(to_absolute(when, "UTC"), expected) 48 | 49 | when = datetime(1999, 2, 12, 5, 57, 30, 0, 50 | FixedOffset(-3 * 60, "GMT-3")) 51 | expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x29] 52 | self.assertEqual(to_absolute(when, "GMT-3"), expected) 53 | 54 | 55 | class TestSmsSubmit(unittest.TestCase): 56 | 57 | def test_encoding_validity(self): 58 | # no validity 59 | number = '2b3334363136353835313139'.decode('hex') 60 | text = "hola" 61 | expected = "0001000B914316565811F9000004E8373B0C" 62 | 63 | sms = SmsSubmit(number, text) 64 | sms.ref = 0x0 65 | 66 | pdu = sms.to_pdu()[0] 67 | self.assertEqual(pdu.pdu, expected) 68 | 69 | # absolute validity 70 | number = '2b3334363136353835313139'.decode('hex') 71 | text = "hola" 72 | expected = "0019000B914316565811F900000170520251930004E8373B0C" 73 | 74 | sms = SmsSubmit(number, text) 75 | sms.ref = 0x0 76 | sms.validity = datetime(2010, 7, 25, 20, 15, 39) 77 | 78 | pdu = sms.to_pdu()[0] 79 | self.assertEqual(pdu.pdu, expected) 80 | 81 | # relative validity 82 | number = '2b3334363136353835313139'.decode('hex') 83 | text = "hola" 84 | expected = "0011000B914316565811F90000AA04E8373B0C" 85 | expected_len = 18 86 | 87 | sms = SmsSubmit(number, text) 88 | sms.ref = 0x0 89 | sms.validity = timedelta(days=4) 90 | 91 | pdu = sms.to_pdu()[0] 92 | self.assertEqual(pdu.pdu, expected) 93 | self.assertEqual(pdu.length, expected_len) 94 | 95 | def test_encoding_csca(self): 96 | number = '2b3334363136353835313139'.decode('hex') 97 | text = "hola" 98 | csca = "+34646456456" 99 | expected = "07914346466554F601000B914316565811F9000004E8373B0C" 100 | expected_len = 17 101 | 102 | sms = SmsSubmit(number, text) 103 | sms.csca = csca 104 | sms.ref = 0x0 105 | 106 | pdu = sms.to_pdu()[0] 107 | self.assertEqual(pdu.pdu, expected) 108 | self.assertEqual(pdu.length, expected_len) 109 | self.assertEqual(pdu.cnt, 1) 110 | self.assertEqual(pdu.seq, 1) 111 | 112 | def test_encoding_class(self): 113 | number = '2b3334363534313233343536'.decode('hex') 114 | text = "hey yo" 115 | expected_0 = "0001000B914356143254F6001006E8721E947F03" 116 | expected_1 = "0001000B914356143254F6001106E8721E947F03" 117 | expected_2 = "0001000B914356143254F6001206E8721E947F03" 118 | expected_3 = "0001000B914356143254F6001306E8721E947F03" 119 | 120 | sms = SmsSubmit(number, text) 121 | sms.ref = 0x0 122 | sms.klass = 0 123 | 124 | pdu = sms.to_pdu()[0] 125 | self.assertEqual(pdu.pdu, expected_0) 126 | 127 | sms.klass = 1 128 | pdu = sms.to_pdu()[0] 129 | self.assertEqual(pdu.pdu, expected_1) 130 | 131 | sms.klass = 2 132 | pdu = sms.to_pdu()[0] 133 | self.assertEqual(pdu.pdu, expected_2) 134 | 135 | sms.klass = 3 136 | pdu = sms.to_pdu()[0] 137 | self.assertEqual(pdu.pdu, expected_3) 138 | 139 | def test_encoding_request_status(self): 140 | # tested with pduspy.exe and http://www.rednaxela.net/pdu.php 141 | number = '2b3334363534313233343536'.decode('hex') 142 | text = "hey yo" 143 | expected = "0021000B914356143254F6000006E8721E947F03" 144 | 145 | sms = SmsSubmit(number, text) 146 | sms.ref = 0x0 147 | sms.request_status = True 148 | 149 | pdu = sms.to_pdu()[0] 150 | self.assertEqual(pdu.pdu, expected) 151 | 152 | def test_encoding_message_with_latin1_chars(self): 153 | # tested with pduspy.exe 154 | number = '2b3334363534313233343536'.decode('hex') 155 | text = u"Hölä" 156 | expected = "0011000B914356143254F60000AA04483E7B0F" 157 | 158 | sms = SmsSubmit(number, text) 159 | sms.ref = 0x0 160 | sms.validity = timedelta(days=4) 161 | 162 | pdu = sms.to_pdu()[0] 163 | self.assertEqual(pdu.pdu, expected) 164 | 165 | # tested with pduspy.exe 166 | number = '2b3334363534313233343536'.decode('hex') 167 | text = u"BÄRÇA äñ@" 168 | expected = "0001000B914356143254F6000009C2AD341104EDFB00" 169 | 170 | sms = SmsSubmit(number, text) 171 | sms.ref = 0x0 172 | 173 | pdu = sms.to_pdu()[0] 174 | self.assertEqual(pdu.pdu, expected) 175 | 176 | def test_encoding_8bit_message(self): 177 | number = "01000000000" 178 | csca = "+44000000000" 179 | text = "Hi there..." 180 | expected = "07914400000000F001000B811000000000F000040B48692074686572652E2E2E" 181 | 182 | sms = SmsSubmit(number, text) 183 | sms.ref = 0x0 184 | sms.csca = csca 185 | sms.fmt = 0x04 # 8 bits 186 | 187 | pdu = sms.to_pdu()[0] 188 | self.assertEqual(pdu.pdu, expected) 189 | 190 | def test_encoding_ucs2_message(self): 191 | number = '2b3334363136353835313139'.decode('hex') 192 | text = u'あ叶葉' 193 | csca = '+34646456456' 194 | expected = "07914346466554F601000B914316565811F9000806304253F68449" 195 | 196 | sms = SmsSubmit(number, text) 197 | sms.ref = 0x0 198 | sms.csca = csca 199 | 200 | pdu = sms.to_pdu()[0] 201 | self.assertEqual(pdu.pdu, expected) 202 | 203 | text = u"Русский" 204 | number = '363535333435363738'.decode('hex') 205 | expected = "001100098156355476F80008AA0E0420044304410441043A04380439" 206 | 207 | sms = SmsSubmit(number, text) 208 | sms.ref = 0x0 209 | sms.validity = timedelta(days=4) 210 | 211 | pdu = sms.to_pdu()[0] 212 | self.assertEqual(pdu.pdu, expected) 213 | 214 | def test_encoding_multipart_7bit(self): 215 | # text encoded with umts-tools 216 | text = "Or walk with Kings - nor lose the common touch, if neither foes nor loving friends can hurt you, If all men count with you, but none too much; If you can fill the unforgiving minute With sixty seconds' worth of distance run, Yours is the Earth and everything thats in it, And - which is more - you will be a Man, my son" 217 | number = '363535333435363738'.decode('hex') 218 | expected = [ 219 | "005100098156355476F80000AAA00500038803019E72D03DCC5E83EE693A1AB44CBBCF73500BE47ECB41ECF7BC0CA2A3CBA0F1BBDD7EBB41F4777D8C6681D26690BB9CA6A3CB7290F95D9E83DC6F3988FDB6A7DD6790599E2EBBC973D038EC06A1EB723A28FFAEB340493328CC6683DA653768FCAEBBE9A07B9A8E06E5DF7516485CA783DC6F7719447FBF41EDFA18BD0325CDA0FCBB0E1A87DD", 220 | "005100098156355476F80000AAA005000388030240E6349B0DA2A3CBA0BADBFC969FD3F6B4FB0C6AA7DD757A19744DD3D1A0791A4FCF83E6E5F1DB4D9E9F40F7B79C8E06BDCD20727A4E0FBBC76590BCEE6681B2EFBA7C0E4ACF41747419540CCBE96850D84D0695ED65799E8E4EBBCF203A3A4C9F83D26E509ACE0205DD64500B7447A7C768507A0E6ABFE565500B947FD741F7349B0D129741", 221 | "005100098156355476F80000AA14050003880303C2A066D8CD02B5F3A0F9DB0D", 222 | ] 223 | 224 | sms = SmsSubmit(number, text) 225 | sms.ref = 0x0 226 | sms.rand_id = 136 227 | sms.validity = timedelta(days=4) 228 | 229 | ret = sms.to_pdu() 230 | cnt = len(ret) 231 | for i, pdu in enumerate(ret): 232 | self.assertEqual(pdu.pdu, expected[i]) 233 | self.assertEqual(pdu.seq, i + 1) 234 | self.assertEqual(pdu.cnt, cnt) 235 | 236 | def test_encoding_bad_number_raises_error(self): 237 | self.assertRaises(ValueError, SmsSubmit, "032BADNUMBER", "text") 238 | 239 | def test_encoding_bad_csca_raises_error(self): 240 | sms = SmsSubmit("54342342", "text") 241 | self.assertRaises(ValueError, setattr, sms, 'csca', "1badcsca") 242 | 243 | 244 | class TestSubmitPduCounts(unittest.TestCase): 245 | 246 | DEST = "+3530000000" 247 | GSM_CHAR = "x" 248 | EGSM_CHAR = u"€" 249 | UNICODE_CHAR = u"ő" 250 | 251 | def test_gsm_1(self): 252 | sms = SmsSubmit(self.DEST, self.GSM_CHAR * 160) 253 | self.assertEqual(len(sms.to_pdu()), 1) 254 | 255 | def test_gsm_2(self): 256 | sms = SmsSubmit(self.DEST, self.GSM_CHAR * 161) 257 | self.assertEqual(len(sms.to_pdu()), 2) 258 | 259 | def test_gsm_3(self): 260 | sms = SmsSubmit(self.DEST, self.GSM_CHAR * 153 * 2) 261 | self.assertEqual(len(sms.to_pdu()), 2) 262 | 263 | def test_gsm_4(self): 264 | sms = SmsSubmit(self.DEST, 265 | self.GSM_CHAR * 153 * 2 + self.GSM_CHAR) 266 | self.assertEqual(len(sms.to_pdu()), 3) 267 | 268 | def test_gsm_5(self): 269 | sms = SmsSubmit(self.DEST, self.GSM_CHAR * 153 * 3) 270 | self.assertEqual(len(sms.to_pdu()), 3) 271 | 272 | def test_gsm_6(self): 273 | sms = SmsSubmit(self.DEST, 274 | self.GSM_CHAR * 153 * 3 + self.GSM_CHAR) 275 | self.assertEqual(len(sms.to_pdu()), 4) 276 | 277 | def test_egsm_1(self): 278 | sms = SmsSubmit(self.DEST, self.EGSM_CHAR * 80) 279 | self.assertEqual(len(sms.to_pdu()), 1) 280 | 281 | def test_egsm_2(self): 282 | sms = SmsSubmit(self.DEST, 283 | self.EGSM_CHAR * 79 + self.GSM_CHAR) 284 | self.assertEqual(len(sms.to_pdu()), 1) 285 | 286 | def test_egsm_3(self): 287 | sms = SmsSubmit(self.DEST, self.EGSM_CHAR * 153) # 306 septets 288 | self.assertEqual(len(sms.to_pdu()), 3) 289 | 290 | def test_egsm_4(self): 291 | sms = SmsSubmit(self.DEST, 292 | self.EGSM_CHAR * 229 + self.GSM_CHAR) # 459 septets 293 | self.assertEqual(len(sms.to_pdu()), 4) 294 | 295 | def test_unicode_1(self): 296 | sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 70) 297 | self.assertEqual(len(sms.to_pdu()), 1) 298 | 299 | def test_unicode_2(self): 300 | sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 70 + self.GSM_CHAR) 301 | self.assertEqual(len(sms.to_pdu()), 2) 302 | 303 | def test_unicode_3(self): 304 | sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 2) 305 | self.assertEqual(len(sms.to_pdu()), 2) 306 | 307 | def test_unicode_4(self): 308 | sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 2 + self.GSM_CHAR) 309 | self.assertEqual(len(sms.to_pdu()), 3) 310 | 311 | def test_unicode_5(self): 312 | sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 3) 313 | self.assertEqual(len(sms.to_pdu()), 3) 314 | 315 | def test_unicode_6(self): 316 | sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 3 + self.GSM_CHAR) 317 | self.assertEqual(len(sms.to_pdu()), 4) 318 | 319 | 320 | class TestSmsDeliver(unittest.TestCase): 321 | 322 | def test_decoding_7bit_pdu(self): 323 | pdu = "07911326040000F0040B911346610089F60000208062917314080CC8F71D14969741F977FD07" 324 | text = "How are you?" 325 | csca = "+31624000000" 326 | number = '2b3331363431363030393836'.decode('hex') 327 | 328 | sms = SmsDeliver(pdu) 329 | self.assertEqual(sms.text, text) 330 | self.assertEqual(sms.csca, csca) 331 | self.assertEqual(sms.number, number) 332 | 333 | def test_decoding_ucs2_pdu(self): 334 | pdu = "07914306073011F0040B914316709807F2000880604290224080084E2D5174901A8BAF" 335 | text = u"中兴通讯" 336 | csca = "+34607003110" 337 | number = '2b3334363130373839373032'.decode('hex') 338 | 339 | sms = SmsDeliver(pdu) 340 | self.assertEqual(sms.text, text) 341 | self.assertEqual(sms.csca, csca) 342 | self.assertEqual(sms.number, number) 343 | 344 | def test_decoding_7bit_pdu_data(self): 345 | pdu = "07911326040000F0040B911346610089F60000208062917314080CC8F71D14969741F977FD07" 346 | text = "How are you?" 347 | csca = "+31624000000" 348 | number = '2b3331363431363030393836'.decode('hex') 349 | 350 | data = SmsDeliver(pdu).data 351 | self.assertEqual(data['text'], text) 352 | self.assertEqual(data['csca'], csca) 353 | self.assertEqual(data['number'], number) 354 | self.assertEqual(data['pid'], 0) 355 | self.assertEqual(data['fmt'], 0) 356 | self.assertEqual(data['date'], datetime(2002, 8, 26, 19, 37, 41)) 357 | 358 | def test_decoding_datetime_gmtplusone(self): 359 | pdu = "0791447758100650040C914497716247010000909010711423400A2050EC468B81C4733A" 360 | text = " 1741 bst" 361 | number = '2b343437393137323637343130'.decode('hex') 362 | date = datetime(2009, 9, 1, 16, 41, 32) 363 | 364 | sms = SmsDeliver(pdu) 365 | self.assertEqual(sms.text, text) 366 | self.assertEqual(sms.number, number) 367 | self.assertEqual(sms.date, date) 368 | 369 | def test_decoding_datetime_gmtminusthree(self): 370 | pdu = "0791553001000001040491578800000190115101112979CF340B342F9FEBE536E83D0791C3E4F71C440E83E6F53068FE66A7C7697A781C7EBB4050F99BFE1EBFD96F1D48068BC16030182E66ABD560B41988FC06D1D3F03768FA66A7C7697A781C7E83CCEF34282C2ECBE96F50B90D8AC55EB0DC4B068BC140B1994E16D3D1622E" 371 | date = datetime(2010, 9, 11, 18, 10, 11) # 11/09/10 15:10 GMT-3.00 372 | 373 | sms = SmsDeliver(pdu) 374 | self.assertEqual(sms.date, date) 375 | 376 | def test_decoding_number_alphanumeric(self): 377 | # Odd length test 378 | pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701B" 379 | number = "FONIC" 380 | text = "Lieber FONIC Kunde, die Tarifoption Internet-Tagesflatrate wurde aktiviert. Internet-Nutzung wird jetzt pro Nutzungstag abgerechnet. Ihr FONIC Team" 381 | csca = "+491760000443" 382 | 383 | sms = SmsDeliver(pdu) 384 | self.assertEqual(sms.text, text) 385 | self.assertEqual(sms.csca, csca) 386 | self.assertEqual(sms.number, number) 387 | 388 | # Even length test 389 | pdu = "07919333852804000412D0F7FBDD454FB75D693A0000903002801153402BCD301E9F0605D9E971191483C140412A35690D52832063D2F9040599A058EE05A3BD6430580E" 390 | number = "www.tim.it" 391 | text = 'Maxxi Alice 100 ATTIVATA FINO AL 19/04/2009' 392 | csca = '+393358824000' 393 | 394 | sms = SmsDeliver(pdu) 395 | self.assertEqual(sms.text, text) 396 | self.assertEqual(sms.csca, csca) 397 | self.assertEqual(sms.number, number) 398 | 399 | def test_decode_sms_confirmation(self): 400 | pdu = "07914306073011F006270B913426565711F7012081111345400120811174054043" 401 | csca = "+34607003110" 402 | date = datetime(2010, 2, 18, 11, 31, 54) 403 | number = "SR-UNKNOWN" 404 | # XXX: the number should be +344626575117, is the prefix flipped ? 405 | text = "+43626575117|10/02/18 11:31:54|" 406 | 407 | sms = SmsDeliver(pdu) 408 | self.assertEqual(sms.text, text) 409 | self.assertEqual(sms.csca, csca) 410 | self.assertEqual(sms.number, number) 411 | self.assertEqual(sms.date, date) 412 | 413 | def test_decode_weird_multipart_german_pdu(self): 414 | pdus = [ 415 | "07919471227210244405852122F039F101506271217180A005000319020198E9B2B82C0759DFE4B0F9ED2EB7967537B9CC02B5D37450122D2FCB41EE303DFD7687D96537881A96A7CD6F383DFD7683F46134BBEC064DD36550DA0D22A7CBF3721BE42CD3F5A0198B56036DCA20B8FC0D6A0A4170767D0EAAE540433A082E7F83A6E5F93CFD76BB40D7B2DB0D9AA6CB2072BA3C2F83926EF31BE44E8FD17450BB8C9683CA", 416 | "07919471227210244405852122F039F1015062712181804F050003190202E4E8309B5E7683DAFC319A5E76B340F73D9A5D7683A6E93268FD9ED3CB6EF67B0E5AD172B19B2C2693C9602E90355D6683A6F0B007946E8382F5393BEC26BB00", 417 | ] 418 | texts = [ 419 | u"Lieber Vodafone-Kunde, mit Ihrer nationalen Tarifoption zahlen Sie in diesem Netz 3,45 € pro MB plus 59 Ct pro Session. Wenn Sie diese Info nicht mehr e", 420 | u"rhalten möchten, wählen Sie kostenlos +4917212220. Viel Spaß im Ausland.", 421 | ] 422 | 423 | for i, sms in enumerate(map(SmsDeliver, pdus)): 424 | self.assertEqual(sms.text, texts[i]) 425 | self.assertEqual(sms.udh.concat.cnt, len(pdus)) 426 | self.assertEqual(sms.udh.concat.seq, i + 1) 427 | self.assertEqual(sms.udh.concat.ref, 25) 428 | 429 | def test_decoding_odd_length_pdu_strict_raises_valueerror(self): 430 | # same pdu as in test_decoding_number_alpha1 minus last char 431 | pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701" 432 | self.assertRaises(ValueError, SmsDeliver, pdu) 433 | 434 | def test_decoding_odd_length_pdu_no_strict(self): 435 | # same pdu as in test_decoding_number_alpha1 minus last char 436 | pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701" 437 | text = "Lieber FONIC Kunde, die Tarifoption Internet-Tagesflatrate wurde aktiviert. Internet-Nutzung wird jetzt pro Nutzungstag abgerechnet. Ihr FONIC Tea" 438 | 439 | sms = SmsDeliver(pdu, strict=False) 440 | self.assertEqual(sms.text, text) 441 | 442 | def test_decoding_delivery_status_report(self): 443 | pdu = "0791538375000075061805810531F1019082416500400190824165004000" 444 | sr = { 445 | 'status': 0, 446 | 'scts': datetime(2010, 9, 28, 14, 56), 447 | 'dt': datetime(2010, 9, 28, 14, 56), 448 | 'recipient': '50131' 449 | } 450 | 451 | sms = SmsDeliver(pdu) 452 | self.assertEqual(sms.csca, "+353857000057") 453 | data = sms.data 454 | self.assertEqual(data['ref'], 24) 455 | self.assertEqual(sms.sr, sr) 456 | 457 | def test_decoding_delivery_status_report_without_smsc_address(self): 458 | pdu = "00060505810531F1010150610000400101506100004000" 459 | sr = { 460 | 'status': 0, 461 | 'scts': datetime(2010, 10, 5, 16, 0), 462 | 'dt': datetime(2010, 10, 5, 16, 0), 463 | 'recipient': '50131' 464 | } 465 | 466 | sms = SmsDeliver(pdu) 467 | self.assertEqual(sms.csca, None) 468 | data = sms.data 469 | self.assertEqual(data['ref'], 5) 470 | self.assertEqual(sms.sr, sr) 471 | 472 | # XXX: renable when support added 473 | # def test_decoding_submit_status_report(self): 474 | # # sent from SMSC to indicate submission failed or additional info 475 | # pdu = "07914306073011F001000B914306565711F9000007F0B2FC0DCABF01" 476 | # csca = "+34607003110" 477 | # number = "SR-UNKNOWN" 478 | # 479 | # sms = SmsDeliver(pdu) 480 | # self.assertEqual(sms.csca, csca) 481 | # self.assertEqual(sms.number, number) 482 | -------------------------------------------------------------------------------- /messaging/mms/message.py: -------------------------------------------------------------------------------- 1 | # This library is free software. 2 | # 3 | # It was originally distributed under the terms of the GNU Lesser 4 | # General Public License Version 2. 5 | # 6 | # python-messaging opts to apply the terms of the ordinary GNU 7 | # General Public License v2, as permitted by section 3 of the LGPL 8 | # v2.1. This re-licensing allows the entirety of python-messaging to 9 | # be distributed according to the terms of GPL-2. 10 | # 11 | # See the COPYING file included in this archive 12 | # 13 | # The docstrings in this module contain epytext markup; API documentation 14 | # may be created by processing this file with epydoc: http://epydoc.sf.net 15 | """High-level MMS message classes""" 16 | 17 | from __future__ import with_statement 18 | import array 19 | import mimetypes 20 | import os 21 | import xml.dom.minidom 22 | 23 | 24 | class MMSMessage: 25 | """ 26 | I am an MMS message 27 | 28 | References used in this class: [1][2][3][4][5] 29 | """ 30 | def __init__(self): 31 | self._pages = [] 32 | self._data_parts = [] 33 | self._metaTags = {} 34 | self._mms_message = None 35 | self.headers = { 36 | 'Message-Type': 'm-send-req', 37 | 'Transaction-Id': '1234', 38 | 'MMS-Version': '1.0', 39 | 'Content-Type': ('application/vnd.wap.multipart.mixed', {}), 40 | } 41 | self.width = 176 42 | self.height = 220 43 | self.transactionID = '12345' 44 | self.subject = 'test' 45 | 46 | @property 47 | def content_type(self): 48 | """ 49 | Returns the Content-Type of this data part header 50 | 51 | No parameter information is returned; to get that, access the 52 | "Content-Type" header directly (which has a tuple value) from 53 | the message's ``headers`` attribute. 54 | 55 | This is equivalent to calling DataPart.headers['Content-Type'][0] 56 | """ 57 | return self.headers['Content-Type'][0] 58 | 59 | def add_page(self, page): 60 | """ 61 | Adds `page` to the message 62 | 63 | :type page: MMSMessagePage 64 | :param page: The message slide/page to add 65 | """ 66 | if self.content_type != 'application/vnd.wap.multipart.related': 67 | value = ('application/vnd.wap.multipart.related', {}) 68 | self.headers['Content-Type'] = value 69 | 70 | self._pages.append(page) 71 | 72 | @property 73 | def pages(self): 74 | """Returns a list of all the pages in this message""" 75 | return self._pages 76 | 77 | def add_data_part(self, data_part): 78 | """Adds a single data part (DataPart object) to the message, without 79 | connecting it to a specific slide/page in the message. 80 | 81 | A data part encapsulates some form of attachment, e.g. an image, audio 82 | etc. It is not necessary to explicitly add data parts to the message 83 | using this function if :func:`add_page` is used; this method is mainly 84 | useful if you want to create MMS messages without SMIL support, 85 | i.e. messages of type "application/vnd.wap.multipart.mixed" 86 | 87 | :param data_part: The data part to add 88 | :type data_part: DataPart 89 | """ 90 | self._data_parts.append(data_part) 91 | 92 | @property 93 | def data_parts(self): 94 | """ 95 | Returns a list of all the data parts in this message 96 | 97 | including data parts that were added to slides in this message""" 98 | parts = [] 99 | if len(self._pages): 100 | parts.append(self.smil()) 101 | for slide in self._mms_message._pages: 102 | parts.extend(slide.data_parts()) 103 | 104 | parts.extend(self._data_parts) 105 | return parts 106 | 107 | def smil(self): 108 | """Returns the text of the message's SMIL file""" 109 | impl = xml.dom.minidom.getDOMImplementation() 110 | smil_doc = impl.createDocument(None, "smil", None) 111 | 112 | # Create the SMIL header 113 | head_node = smil_doc.createElement('head') 114 | # Add metadata to header 115 | for tag_name in self._metaTags: 116 | meta_node = smil_doc.createElement('meta') 117 | meta_node.setAttribute(tag_name, self._metaTags[tag_name]) 118 | head_node.appendChild(meta_node) 119 | 120 | # Add layout info to header 121 | layout_node = smil_doc.createElement('layout') 122 | root_layout_node = smil_doc.createElement('root-layout') 123 | root_layout_node.setAttribute('width', str(self.width)) 124 | root_layout_node.setAttribute('height', str(self.height)) 125 | layout_node.appendChild(root_layout_node) 126 | 127 | areas = (('Image', '0', '0', '176', '144'), 128 | ('Text', '176', '144', '176', '76')) 129 | 130 | for region_id, left, top, width, height in areas: 131 | region_node = smil_doc.createElement('region') 132 | region_node.setAttribute('id', region_id) 133 | region_node.setAttribute('left', left) 134 | region_node.setAttribute('top', top) 135 | region_node.setAttribute('width', width) 136 | region_node.setAttribute('height', height) 137 | layout_node.appendChild(region_node) 138 | 139 | head_node.appendChild(layout_node) 140 | smil_doc.documentElement.appendChild(head_node) 141 | 142 | # Create the SMIL body 143 | body_node = smil_doc.createElement('body') 144 | # Add pages to body 145 | for page in self._pages: 146 | par_node = smil_doc.createElement('par') 147 | par_node.setAttribute('duration', str(page.duration)) 148 | # Add the page content information 149 | if page.image is not None: 150 | #TODO: catch unpack exception 151 | part, begin, end = page.image 152 | if 'Content-Location' in part.headers: 153 | src = part.headers['Content-Location'] 154 | elif 'Content-ID' in part.headers: 155 | src = part.headers['Content-ID'] 156 | else: 157 | src = part.data 158 | 159 | image_node = smil_doc.createElement('img') 160 | image_node.setAttribute('src', src) 161 | image_node.setAttribute('region', 'Image') 162 | if begin > 0 or end > 0: 163 | if end > page.duration: 164 | end = page.duration 165 | 166 | image_node.setAttribute('begin', str(begin)) 167 | image_node.setAttribute('end', str(end)) 168 | 169 | par_node.appendChild(image_node) 170 | 171 | if page.text is not None: 172 | part, begin, end = page.text 173 | src = part.data 174 | text_node = smil_doc.createElement('text') 175 | text_node.setAttribute('src', src) 176 | text_node.setAttribute('region', 'Text') 177 | if begin > 0 or end > 0: 178 | if end > page.duration: 179 | end = page.duration 180 | 181 | text_node.setAttribute('begin', str(begin)) 182 | text_node.setAttribute('end', str(end)) 183 | 184 | par_node.appendChild(text_node) 185 | 186 | if page.audio is not None: 187 | part, begin, end = page.audio 188 | if 'Content-Location' in part.headers: 189 | src = part.headers['Content-Location'] 190 | elif 'Content-ID' in part.headers: 191 | src = part.headers['Content-ID'] 192 | else: 193 | src = part.data 194 | 195 | audio_node = smil_doc.createElement('audio') 196 | audio_node.setAttribute('src', src) 197 | if begin > 0 or end > 0: 198 | if end > page.duration: 199 | end = page.duration 200 | 201 | audio_node.setAttribute('begin', str(begin)) 202 | audio_node.setAttribute('end', str(end)) 203 | 204 | par_node.appendChild(text_node) 205 | par_node.appendChild(audio_node) 206 | 207 | body_node.appendChild(par_node) 208 | 209 | smil_doc.documentElement.appendChild(body_node) 210 | return smil_doc.documentElement.toprettyxml() 211 | 212 | def encode(self): 213 | """ 214 | Return a binary representation of this MMS message 215 | 216 | This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally 217 | 218 | :return: The binary-encoded MMS data, as an array of bytes 219 | :rtype: array.array('B') 220 | """ 221 | from messaging.mms import mms_pdu 222 | encoder = mms_pdu.MMSEncoder() 223 | return encoder.encode(self) 224 | 225 | def to_file(self, filename): 226 | """ 227 | Writes this MMS message to `filename` in binary-encoded form 228 | 229 | This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally 230 | 231 | :param filename: The path where to store the message data 232 | :type filename: str 233 | 234 | :rtype array.array('B') 235 | :return: The binary-encode MMS data, as an array of bytes 236 | """ 237 | with open(filename, 'wb') as f: 238 | self.encode().tofile(f) 239 | 240 | @staticmethod 241 | def from_data(data): 242 | """ 243 | Returns a new `:class:MMSMessage` out of ``data`` 244 | 245 | This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally 246 | 247 | :param data: The data to load 248 | :type filename: array.array 249 | """ 250 | from messaging.mms import mms_pdu 251 | decoder = mms_pdu.MMSDecoder() 252 | return decoder.decode_data(data) 253 | 254 | @staticmethod 255 | def from_file(filename): 256 | """ 257 | Returns a new `:class:MMSMessage` out of file ``filename`` 258 | 259 | This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally 260 | 261 | :param filename: The name of the file to load 262 | :type filename: str 263 | """ 264 | from messaging.mms import mms_pdu 265 | decoder = mms_pdu.MMSDecoder() 266 | return decoder.decode_file(filename) 267 | 268 | 269 | class MMSMessagePage: 270 | """ 271 | A single page/slide in an MMS Message. 272 | 273 | In order to ensure that the MMS message can be correctly displayed by most 274 | terminals, each page's content is limited to having 1 image, 1 audio clip 275 | and 1 block of text, as stated in [1]. 276 | 277 | The default slide duration is set to 4 seconds; use :func:`set_duration` 278 | to change this. 279 | """ 280 | def __init__(self): 281 | self.duration = 4000 282 | self.image = None 283 | self.audio = None 284 | self.text = None 285 | 286 | @property 287 | def data_parts(self): 288 | """Returns a list of the data parst in this slide""" 289 | return [part for part in (self.image, self.audio, self.text) 290 | if part is not None] 291 | 292 | def number_of_parts(self): 293 | """ 294 | Returns the number of data parts in this slide 295 | 296 | @rtype: int 297 | """ 298 | num_parts = 0 299 | for item in (self.image, self.audio, self.text): 300 | if item is not None: 301 | num_parts += 1 302 | 303 | return num_parts 304 | 305 | #TODO: find out what the "ref" element in SMIL does 306 | #TODO: add support for "alt" element; also make sure what it does 307 | def add_image(self, filename, time_begin=0, time_end=0): 308 | """ 309 | Adds an image to this slide. 310 | 311 | :param filename: The name of the image file to add. Supported formats 312 | are JPEG, GIF and WBMP. 313 | :type filename: str 314 | :param time_begin: The time (in milliseconds) during the duration of 315 | this slide to begin displaying the image. If this is 316 | 0 or less, the image will be displayed from the 317 | moment the slide is opened. 318 | :type time_begin: int 319 | :param time_end: The time (in milliseconds) during the duration of this 320 | slide at which to stop showing (i.e. hide) the image. 321 | If this is 0 or less, or if it is greater than the 322 | actual duration of this slide, it will be shown until 323 | the next slide is accessed. 324 | :type time_end: int 325 | 326 | :raise TypeError: An inappropriate variable type was passed in of the 327 | parameters 328 | """ 329 | if not isinstance(filename, str): 330 | raise TypeError("filename must be a string") 331 | 332 | if not isinstance(time_begin, int) or not isinstance(time_end, int): 333 | raise TypeError("time_begin and time_end must be ints") 334 | 335 | if not os.path.isfile(filename): 336 | raise OSError("filename must be a file") 337 | 338 | if time_end > 0 and time_end < time_begin: 339 | raise ValueError('time_end cannot be lower than time_begin') 340 | 341 | self.image = (DataPart(filename), time_begin, time_end) 342 | 343 | def add_audio(self, filename, time_begin=0, time_end=0): 344 | """ 345 | Adds an audio clip to this slide. 346 | 347 | :param filename: The name of the audio file to add. Currently the only 348 | supported format is AMR. 349 | :type filename: str 350 | :param time_begin: The time (in milliseconds) during the duration of 351 | this slide to begin playback of the audio clip. If 352 | this is 0 or less, the audio clip will be played the 353 | moment the slide is opened. 354 | :type time_begin: int 355 | :param time_end: The time (in milliseconds) during the duration of this 356 | slide at which to stop playing (i.e. mute) the audio 357 | clip. If this is 0 or less, or if it is greater than 358 | the actual duration of this slide, the entire audio 359 | clip will be played, or until the next slide is 360 | accessed. 361 | :type time_end: int 362 | :raise TypeError: An inappropriate variable type was passed in of the 363 | parameters 364 | """ 365 | if not isinstance(filename, str): 366 | raise TypeError("filename must be a string") 367 | 368 | if not isinstance(time_begin, int) or not isinstance(time_end, int): 369 | raise TypeError("time_begin and time_end must be ints") 370 | 371 | if not os.path.isfile(filename): 372 | raise OSError("filename must be a file") 373 | 374 | if time_end > 0 and time_end < time_begin: 375 | raise ValueError('time_end cannot be lower than time_begin') 376 | 377 | self.audio = (DataPart(filename), time_begin, time_end) 378 | 379 | def add_text(self, text, time_begin=0, time_end=0): 380 | """ 381 | Adds a block of text to this slide. 382 | 383 | :param text: The text to add to the slide. 384 | :type text: str 385 | :param time_begin: The time (in milliseconds) during the duration of 386 | this slide to begin displaying the text. If this is 387 | 0 or less, the text will be displayed from the 388 | moment the slide is opened. 389 | :type time_begin: int 390 | :param time_end: The time (in milliseconds) during the duration of this 391 | slide at which to stop showing (i.e. hide) the text. 392 | If this is 0 or less, or if it is greater than the 393 | actual duration of this slide, it will be shown until 394 | the next slide is accessed. 395 | :type time_end: int 396 | 397 | :raise TypeError: An inappropriate variable type was passed in of the 398 | parameters 399 | """ 400 | if not isinstance(text, str): 401 | raise TypeError("Text must be a string") 402 | 403 | if not isinstance(time_begin, int) or not isinstance(time_end, int): 404 | raise TypeError("time_begin and time_end must be ints") 405 | 406 | if time_end > 0 and time_end < time_begin: 407 | raise ValueError('time_end cannot be lower than time_begin') 408 | 409 | time_data = DataPart() 410 | time_data.set_text(text) 411 | self.text = (time_data, time_begin, time_end) 412 | 413 | def set_duration(self, duration): 414 | """ Sets the maximum duration of this slide (i.e. how long this slide 415 | should be displayed) 416 | 417 | @param duration: the maxium slide duration, in milliseconds 418 | @type duration: int 419 | 420 | @raise TypeError: must be an integer 421 | @raise ValueError: the requested duration is invalid (must be a 422 | non-zero, positive integer) 423 | """ 424 | if not isinstance(duration, int): 425 | raise TypeError("Duration must be an int") 426 | 427 | if duration < 1: 428 | raise ValueError('duration may not be 0 or negative') 429 | 430 | self.duration = duration 431 | 432 | 433 | class DataPart(object): 434 | """ 435 | I am a data entry in the MMS body 436 | 437 | A DataPart object encapsulates any data content that is to be added 438 | to the MMS (e.g. an image , raw image data, audio clips, text, etc). 439 | 440 | A DataPart object can be queried using the Python built-in :func:`len` 441 | function. 442 | 443 | This encapsulation allows custom header/parameter information to be set 444 | for each data entry in the MMS. Refer to [5] for more information on 445 | these. 446 | """ 447 | def __init__(self, filename=None): 448 | """ @param srcFilename: If specified, load the content of the file 449 | with this name 450 | @type srcFilename: str 451 | """ 452 | super(DataPart, self).__init__() 453 | 454 | self.content_type_parameters = {} 455 | self.headers = {'Content-Type': ('application/octet-stream', {})} 456 | self._filename = None 457 | self._data = None 458 | 459 | if filename is not None: 460 | self.from_file(filename) 461 | 462 | def _get_content_type(self): 463 | """ Returns the string representation of this data part's 464 | "Content-Type" header. No parameter information is returned; 465 | to get that, access the "Content-Type" header directly (which has a 466 | tuple value)from this part's C{headers} attribute. 467 | 468 | This is equivalent to calling DataPart.headers['Content-Type'][0] 469 | """ 470 | return self.headers['Content-Type'][0] 471 | 472 | def _set_content_type(self, value): 473 | """Sets the content type string, with no parameters """ 474 | self.headers['Content-Type'] = value, {} 475 | 476 | content_type = property(_get_content_type, _set_content_type) 477 | 478 | def from_file(self, filename): 479 | """ 480 | Load the data contained in the specified file 481 | 482 | This function clears any previously-set header entries. 483 | 484 | :param filename: The name of the file to open 485 | :type filename: str 486 | 487 | :raises OSError: The filename is invalid 488 | """ 489 | if not os.path.isfile(filename): 490 | raise OSError('The file "%s" does not exist' % filename) 491 | 492 | # Clear any headers that are currently set 493 | self.headers = {} 494 | self._data = None 495 | self.headers['Content-Location'] = os.path.basename(filename) 496 | content_type = (mimetypes.guess_type(filename)[0] 497 | or 'application/octet-stream', {}) 498 | self.headers['Content-Type'] = content_type 499 | self._filename = filename 500 | 501 | def set_data(self, data, content_type, ct_parameters=None): 502 | """ 503 | Explicitly set the data contained by this part 504 | 505 | This function clears any previously-set header entries. 506 | 507 | :param data: The data to hold 508 | :type data: str 509 | :param content_type: The MIME content type of the specified data 510 | :type content_type: str 511 | :param ct_parameters: Any content type header paramaters to add 512 | :type ct_parameters: dict 513 | """ 514 | self.headers = {} 515 | self._filename = None 516 | self._data = data 517 | 518 | if ct_parameters is None: 519 | ct_parameters = {} 520 | 521 | self.headers['Content-Type'] = content_type, ct_parameters 522 | 523 | def set_text(self, text): 524 | """ 525 | Convenience wrapper method for set_data() 526 | 527 | This method sets the :class:`DataPart` object to hold the 528 | specified text string, with MIME content type "text/plain". 529 | 530 | @param text: The text to hold 531 | @type text: str 532 | """ 533 | self.set_data(text, 'text/plain') 534 | 535 | def __len__(self): 536 | """Provides the length of the data encapsulated by this object""" 537 | if self._filename is not None: 538 | return int(os.stat(self._filename)[6]) 539 | else: 540 | return len(self.data) 541 | 542 | @property 543 | def data(self): 544 | """A buffer containing the binary data of this part""" 545 | if self._data is not None: 546 | if type(self._data) == array.array: 547 | self._data = self._data.tostring() 548 | return self._data 549 | 550 | elif self._filename is not None: 551 | with open(self._filename, 'r') as f: 552 | self._data = f.read() 553 | return self._data 554 | 555 | return '' 556 | -------------------------------------------------------------------------------- /messaging/test/test_mms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from array import array 3 | import datetime 4 | import os 5 | import unittest 6 | 7 | from messaging.mms.message import MMSMessage 8 | 9 | # test data extracted from heyman's 10 | # http://github.com/heyman/mms-decoder 11 | DATA_DIR = os.path.join(os.path.dirname(__file__), 'mms-data') 12 | 13 | 14 | class TestMmsDecoding(unittest.TestCase): 15 | 16 | def test_decoding_from_data(self): 17 | path = os.path.join(DATA_DIR, 'iPhone.mms') 18 | data = array("B", open(path, 'rb').read()) 19 | mms = MMSMessage.from_data(data) 20 | headers = { 21 | 'From': '', 'Transaction-Id': '1262957356-3', 22 | 'MMS-Version': '1.2', 'To': '1337/TYPE=PLMN', 23 | 'Message-Type': 'm-send-req', 24 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '0.smil', 'Type': 'application/smil'}), 25 | } 26 | self.assertEqual(mms.headers, headers) 27 | 28 | def test_decoding_iPhone_mms(self): 29 | path = os.path.join(DATA_DIR, 'iPhone.mms') 30 | mms = MMSMessage.from_file(path) 31 | self.assertTrue(isinstance(mms, MMSMessage)) 32 | headers = { 33 | 'From': '', 'Transaction-Id': '1262957356-3', 34 | 'MMS-Version': '1.2', 'To': '1337/TYPE=PLMN', 35 | 'Message-Type': 'm-send-req', 36 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '0.smil', 'Type': 'application/smil'}), 37 | } 38 | smil_data = '\n\n\n \n\n\n\n\n\n\n\n\n\n\n' 39 | self.assertEqual(mms.headers, headers) 40 | self.assertEqual(mms.content_type, 41 | 'application/vnd.wap.multipart.related') 42 | self.assertEqual(len(mms.data_parts), 2) 43 | self.assertEqual(mms.data_parts[0].content_type, 'application/smil') 44 | self.assertEqual(mms.data_parts[0].data, smil_data) 45 | self.assertEqual(mms.data_parts[1].content_type, 'image/jpeg') 46 | self.assertEqual(mms.data_parts[1].content_type_parameters, 47 | {'Name': 'IMG_6807.jpg'}) 48 | 49 | def test_decoding_SIMPLE_mms(self): 50 | path = os.path.join(DATA_DIR, 'SIMPLE.MMS') 51 | mms = MMSMessage.from_file(path) 52 | self.assertTrue(isinstance(mms, MMSMessage)) 53 | headers = { 54 | 'Transaction-Id': '1234', 'MMS-Version': '1.0', 55 | 'Message-Type': 'm-retrieve-conf', 56 | 'Date': datetime.datetime(2002, 12, 20, 21, 26, 56), 57 | 'Content-Type': ('application/vnd.wap.multipart.related', {}), 58 | 'Subject': 'Simple message', 59 | } 60 | text_data = "This is a simple MMS message with a single text body part." 61 | self.assertEqual(mms.headers, headers) 62 | self.assertEqual(mms.content_type, 63 | 'application/vnd.wap.multipart.related') 64 | self.assertEqual(len(mms.data_parts), 1) 65 | self.assertEqual(mms.data_parts[0].content_type, 'text/plain') 66 | self.assertEqual(mms.data_parts[0].data, text_data) 67 | 68 | def test_decoding_BTMMS_mms(self): 69 | path = os.path.join(DATA_DIR, 'BTMMS.MMS') 70 | mms = MMSMessage.from_file(path) 71 | self.assertTrue(isinstance(mms, MMSMessage)) 72 | headers = { 73 | 'Transaction-Id': '1234', 'MMS-Version': '1.0', 74 | 'Message-Type': 'm-retrieve-conf', 75 | 'Date': datetime.datetime(2003, 1, 21, 1, 57, 4), 76 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 77 | 'Subject': 'BT Ignite MMS', 78 | } 79 | smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n' 80 | text_data = 'BT Ignite\r\n\r\nMMS Services' 81 | self.assertEqual(mms.headers, headers) 82 | self.assertEqual(mms.content_type, 83 | 'application/vnd.wap.multipart.related') 84 | self.assertEqual(len(mms.data_parts), 4) 85 | self.assertEqual(mms.data_parts[0].content_type, 'application/smil') 86 | self.assertEqual(mms.data_parts[0].data, smil_data) 87 | self.assertEqual(mms.data_parts[1].content_type, 'image/gif') 88 | self.assertEqual(mms.data_parts[2].content_type, 'audio/amr') 89 | self.assertEqual(mms.data_parts[3].content_type, 'text/plain') 90 | self.assertEqual(mms.data_parts[3].data, text_data) 91 | 92 | def test_decoding_TOMSLOT_mms(self): 93 | path = os.path.join(DATA_DIR, 'TOMSLOT.MMS') 94 | mms = MMSMessage.from_file(path) 95 | self.assertTrue(isinstance(mms, MMSMessage)) 96 | headers = { 97 | 'From': '616c6c616e40746f6d736c6f742e636f6d'.decode('hex'), 98 | 'Transaction-Id': '1234', 99 | 'MMS-Version': '1.0', 'Message-Type': 'm-retrieve-conf', 100 | 'Date': datetime.datetime(2003, 2, 16, 3, 48, 33), 101 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 102 | 'Subject': 'Tom Slot Band', 103 | } 104 | smil_data = '\r\n\t\r\n\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\r\n\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\r\n\r\n' 105 | text_data = 'Presented by NowMMS\r\n' 106 | self.assertEqual(mms.headers, headers) 107 | self.assertEqual(mms.content_type, 108 | 'application/vnd.wap.multipart.related') 109 | self.assertEqual(len(mms.data_parts), 8) 110 | self.assertEqual(mms.data_parts[0].content_type, 'application/smil') 111 | self.assertEqual(mms.data_parts[0].data, smil_data) 112 | self.assertEqual(mms.data_parts[1].content_type, 'image/jpeg') 113 | self.assertEqual(mms.data_parts[2].content_type, 'image/jpeg') 114 | self.assertEqual(mms.data_parts[3].content_type, 'image/jpeg') 115 | self.assertEqual(mms.data_parts[4].content_type, 'image/jpeg') 116 | self.assertEqual(mms.data_parts[5].content_type, 'image/jpeg') 117 | self.assertEqual(mms.data_parts[6].content_type, 'text/plain') 118 | self.assertEqual(mms.data_parts[6].data, text_data) 119 | self.assertEqual(mms.data_parts[7].content_type, 'audio/amr') 120 | 121 | def test_decoding_images_are_cut_off_debug_mms(self): 122 | path = os.path.join(DATA_DIR, 'images_are_cut_off_debug.mms') 123 | mms = MMSMessage.from_file(path) 124 | self.assertTrue(isinstance(mms, MMSMessage)) 125 | headers = { 126 | 'From': '', 'Read-Reply': False, 127 | 'Transaction-Id': '2112410527', 'MMS-Version': '1.0', 128 | 'To': '7464707440616a616a672e63646d'.decode('hex'), 129 | 'Delivery-Report': False, 130 | 'Message-Type': 'm-send-req', 131 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 132 | 'Subject': 'Picture3', 133 | } 134 | smil_data = '' 135 | self.assertEqual(mms.headers, headers) 136 | self.assertEqual(len(mms.data_parts), 2) 137 | self.assertEqual(mms.content_type, 138 | 'application/vnd.wap.multipart.related') 139 | self.assertEqual(mms.data_parts[0].content_type, 'image/jpeg') 140 | self.assertEqual(mms.data_parts[0].content_type_parameters, 141 | {'Name': 'Picture3.jpg'}) 142 | self.assertEqual(mms.data_parts[1].content_type, 'application/smil') 143 | self.assertEqual(mms.data_parts[1].data, smil_data) 144 | 145 | def test_decoding_openwave_mms(self): 146 | path = os.path.join(DATA_DIR, 'openwave.mms') 147 | mms = MMSMessage.from_file(path) 148 | self.assertTrue(isinstance(mms, MMSMessage)) 149 | headers = { 150 | 'From': '2b31363530353535303030302f545950453d504c4d4e'.decode('hex'), 151 | 'Message-Class': 'Personal', 152 | 'Transaction-Id': '1067263672', 'MMS-Version': '1.0', 153 | 'Priority': 'Normal', 'To': '112/TYPE=PLMN', 154 | 'Delivery-Report': False, 'Message-Type': 'm-send-req', 155 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 156 | 'Subject': 'rubrik', 157 | } 158 | smil_data = '\n \n \n \n \n \n \n \n \n \n \n \n \n\n' 159 | text_data = 'rubrik' 160 | self.assertEqual(mms.headers, headers) 161 | self.assertEqual(len(mms.data_parts), 2) 162 | self.assertEqual(mms.content_type, 163 | 'application/vnd.wap.multipart.related') 164 | self.assertEqual(mms.data_parts[0].content_type, 'application/smil') 165 | self.assertEqual(mms.data_parts[0].data, smil_data) 166 | self.assertEqual(mms.data_parts[1].data, text_data) 167 | 168 | def test_decoding_SonyEricssonT310_R201_mms(self): 169 | path = os.path.join(DATA_DIR, 'SonyEricssonT310-R201.mms') 170 | mms = MMSMessage.from_file(path) 171 | self.assertTrue(isinstance(mms, MMSMessage)) 172 | headers = { 173 | 'Sender-Visibility': 'Show', 'From': '', 174 | 'Read-Reply': False, 'Message-Class': 'Personal', 175 | 'Transaction-Id': '1-8db', 'MMS-Version': '1.0', 176 | 'Priority': 'Normal', 'To': '55225/TYPE=PLMN', 177 | 'Delivery-Report': False, 'Message-Type': 'm-send-req', 178 | 'Date': datetime.datetime(2004, 3, 18, 7, 30, 34), 179 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 180 | } 181 | text_data = 'Hej hopp' 182 | smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n' 183 | self.assertEqual(mms.headers, headers) 184 | self.assertEqual(len(mms.data_parts), 4) 185 | self.assertEqual(mms.content_type, 186 | 'application/vnd.wap.multipart.related') 187 | self.assertEqual(mms.data_parts[0].content_type, 'image/gif') 188 | self.assertEqual(mms.data_parts[0].content_type_parameters, 189 | {'Name': 'Tony.gif'}) 190 | self.assertEqual(mms.data_parts[1].content_type, 'text/plain') 191 | self.assertEqual(mms.data_parts[1].data, text_data) 192 | self.assertEqual(mms.data_parts[2].content_type, 'audio/midi') 193 | self.assertEqual(mms.data_parts[2].content_type_parameters, 194 | {'Name': 'OldhPhone.mid'}) 195 | self.assertEqual(mms.data_parts[3].content_type, 'application/smil') 196 | self.assertEqual(mms.data_parts[3].data, smil_data) 197 | 198 | def test_decoding_gallery2test_mms(self): 199 | path = os.path.join(DATA_DIR, 'gallery2test.mms') 200 | mms = MMSMessage.from_file(path) 201 | self.assertTrue(isinstance(mms, MMSMessage)) 202 | headers = { 203 | 'From': '2b31363530353535303030302f545950453d504c4d4e'.decode('hex'), 204 | 'Message-Class': 'Personal', 205 | 'Transaction-Id': '1118775337', 'MMS-Version': '1.0', 206 | 'Priority': 'Normal', 'To': 'Jg', 'Delivery-Report': False, 207 | 'Message-Type': 'm-send-req', 208 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 209 | 'Subject': 'Jgj', 210 | } 211 | text_data = 'Jgj' 212 | smil_data = '\n \n \n \n \n \n \n \n \n \n gnu-head\n \n \n \n\n' 213 | self.assertEqual(mms.headers, headers) 214 | self.assertEqual(len(mms.data_parts), 3) 215 | self.assertEqual(mms.content_type, 216 | 'application/vnd.wap.multipart.related') 217 | self.assertEqual(mms.data_parts[0].content_type, 'application/smil') 218 | self.assertEqual(mms.data_parts[0].data, smil_data) 219 | self.assertEqual(mms.data_parts[1].content_type, 'text/plain') 220 | self.assertEqual(mms.data_parts[1].data, text_data) 221 | self.assertEqual(mms.data_parts[2].content_type, 'image/jpeg') 222 | # XXX: Shouldn't it be 'Name' instead ? 223 | self.assertEqual(mms.data_parts[2].content_type_parameters, 224 | {'name': 'gnu-head.jpg'}) 225 | 226 | def test_decoding_projekt_exempel_mms(self): 227 | path = os.path.join(DATA_DIR, 'projekt_exempel.mms') 228 | mms = MMSMessage.from_file(path) 229 | self.assertTrue(isinstance(mms, MMSMessage)) 230 | headers = { 231 | 'Sender-Visibility': 'Show', 'From': '', 232 | 'Read-Reply': False, 'Message-Class': 'Personal', 233 | 'Transaction-Id': '4-fc60', 'MMS-Version': '1.0', 234 | 'Priority': 'Normal', 'To': '12345/TYPE=PLMN', 235 | 'Delivery-Report': False, 'Message-Type': 'm-send-req', 236 | 'Date': datetime.datetime(2004, 5, 23, 15, 13, 40), 237 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 238 | 'Subject': 'Hej', 239 | } 240 | smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n' 241 | text_data = 'Jonatan \xc3\xa4r en GNU' 242 | self.assertEqual(mms.headers, headers) 243 | self.assertEqual(len(mms.data_parts), 3) 244 | self.assertEqual(mms.content_type, 245 | 'application/vnd.wap.multipart.related') 246 | self.assertEqual(mms.data_parts[0].content_type, 'text/plain') 247 | self.assertEqual(mms.data_parts[0].data, text_data) 248 | self.assertEqual(mms.data_parts[1].content_type, 'image/gif') 249 | self.assertEqual(mms.data_parts[2].content_type, 'application/smil') 250 | self.assertEqual(mms.data_parts[2].data, smil_data) 251 | self.assertEqual(mms.data_parts[2].content_type_parameters, 252 | {'Charset': 'utf-8', 'Name': 'mms.smil'}) 253 | 254 | def test_decoding_m_mms(self): 255 | path = os.path.join(DATA_DIR, 'm.mms') 256 | mms = MMSMessage.from_file(path) 257 | self.assertTrue(isinstance(mms, MMSMessage)) 258 | headers = { 259 | 'From': '676f6c64706f737440686f746d61696c2e636f6d'.decode('hex'), 260 | 'Transaction-Id': '0000000001', 261 | 'MMS-Version': '1.0', 'Message-Type': 'm-retrieve-conf', 262 | 'Date': datetime.datetime(2002, 8, 9, 13, 8, 2), 263 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 264 | 'Subject': 'GOLD', 265 | } 266 | text_data1 = 'Audio' 267 | text_data2 = 'Text +' 268 | text_data3 = 'tagtag.com/gold\r\n' 269 | text_data4 = 'globalisierunglobalisierunglobalisierunglobalisierunglobalisierunglobalisierunglobalisierungnureisilabolg' 270 | text_data5 = 'KLONE\r\nKLONE\r\n' 271 | text_data6 = 'pr\xe4sentiert..' 272 | text_data7 = 'GOLD' 273 | smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n' 274 | self.assertEqual(mms.headers, headers) 275 | self.assertEqual(len(mms.data_parts), 9) 276 | self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related') 277 | self.assertEqual(mms.data_parts[0].content_type, 'text/plain') 278 | self.assertEqual(mms.data_parts[0].data, text_data1) 279 | self.assertEqual(mms.data_parts[0].content_type_parameters, 280 | {'Charset': 'us-ascii'}) 281 | self.assertEqual(mms.data_parts[1].content_type, 'application/smil') 282 | self.assertEqual(mms.data_parts[1].data, smil_data) 283 | self.assertEqual(mms.data_parts[1].content_type_parameters, 284 | {'Charset': 'us-ascii'}) 285 | self.assertEqual(mms.data_parts[2].content_type, 'text/plain') 286 | self.assertEqual(mms.data_parts[2].data, text_data2) 287 | self.assertEqual(mms.data_parts[2].content_type_parameters, 288 | {'Charset': 'us-ascii'}) 289 | self.assertEqual(mms.data_parts[3].content_type, 'text/plain') 290 | self.assertEqual(mms.data_parts[3].data, text_data3) 291 | self.assertEqual(mms.data_parts[3].content_type_parameters, 292 | {'Charset': 'us-ascii'}) 293 | self.assertEqual(mms.data_parts[4].content_type, 'audio/amr') 294 | self.assertEqual(mms.data_parts[5].content_type, 'text/plain') 295 | self.assertEqual(mms.data_parts[5].data, text_data4) 296 | self.assertEqual(mms.data_parts[5].content_type_parameters, 297 | {'Charset': 'us-ascii'}) 298 | self.assertEqual(mms.data_parts[6].content_type, 'text/plain') 299 | self.assertEqual(mms.data_parts[6].data, text_data5) 300 | self.assertEqual(mms.data_parts[6].content_type_parameters, 301 | {'Charset': 'us-ascii'}) 302 | self.assertEqual(mms.data_parts[7].content_type, 'text/plain') 303 | self.assertEqual(mms.data_parts[7].data, text_data6) 304 | self.assertEqual(mms.data_parts[7].content_type_parameters, 305 | {'Charset': 'us-ascii'}) 306 | self.assertEqual(mms.data_parts[8].content_type, 'text/plain') 307 | self.assertEqual(mms.data_parts[8].data, text_data7) 308 | self.assertEqual(mms.data_parts[8].content_type_parameters, 309 | {'Charset': 'us-ascii'}) 310 | 311 | def test_decoding_27d0a048cd79555de05283a22372b0eb_mms(self): 312 | path = os.path.join(DATA_DIR, '27d0a048cd79555de05283a22372b0eb.mms') 313 | mms = MMSMessage.from_file(path) 314 | self.assertTrue(isinstance(mms, MMSMessage)) 315 | headers = { 316 | 'Sender-Visibility': 'Show', 'From': '', 317 | 'Read-Reply': False, 'Message-Class': 'Personal', 318 | 'Transaction-Id': '3-31cb', 'MMS-Version': '1.0', 319 | 'Priority': 'Normal', 'To': '123/TYPE=PLMN', 320 | 'Delivery-Report': False, 'Message-Type': 'm-send-req', 321 | 'Date': datetime.datetime(2004, 5, 23, 14, 14, 58), 322 | 'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '', 'Type': 'application/smil'}), 323 | 'Subject': 'Angående art-tillhörighet', 324 | #'Subject': 'Ang\xc3\xa5ende art-tillh\xc3\xb6righet', 325 | } 326 | smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n' 327 | text_data = 'Jonatan \xc3\xa4r en gnu.' 328 | self.assertEqual(mms.headers, headers) 329 | self.assertEqual(len(mms.data_parts), 3) 330 | self.assertEqual(mms.content_type, 331 | 'application/vnd.wap.multipart.related') 332 | self.assertEqual(mms.data_parts[0].content_type, 'image/vnd.wap.wbmp') 333 | self.assertEqual(mms.data_parts[0].content_type_parameters, 334 | {'Name': 'Rain.wbmp'}) 335 | self.assertEqual(mms.data_parts[1].content_type, 'text/plain') 336 | self.assertEqual(mms.data_parts[1].data, text_data) 337 | self.assertEqual(mms.data_parts[1].content_type_parameters, 338 | {'Charset': 'utf-8', 'Name': 'mms.txt'}) 339 | self.assertEqual(mms.data_parts[2].content_type, 'application/smil') 340 | self.assertEqual(mms.data_parts[2].data, smil_data) 341 | self.assertEqual(mms.data_parts[2].content_type_parameters, 342 | {'Charset': 'utf-8', 'Name': 'mms.smil'}) 343 | 344 | def test_decoding_SEC_SGHS300M(self): 345 | path = os.path.join(DATA_DIR, 'SEC-SGHS300M.mms') 346 | mms = MMSMessage.from_file(path) 347 | self.assertTrue(isinstance(mms, MMSMessage)) 348 | headers = { 349 | 'Sender-Visibility': 'Show', 'From': '', 350 | 'Read-Reply': False, 'Message-Class': 'Personal', 351 | 'Transaction-Id': '31887', 'MMS-Version': '1.0', 352 | 'To': '303733383334353636342f545950453d504c4d4e'.decode('hex'), 353 | 'Delivery-Report': False, 354 | 'Message-Type': 'm-send-req', 'Subject': 'IL', 355 | 'Content-Type': ('application/vnd.wap.multipart.mixed', {}), 356 | } 357 | text_data = 'HV' 358 | self.assertEqual(mms.headers, headers) 359 | self.assertEqual(len(mms.data_parts), 1) 360 | self.assertEqual(mms.content_type, 361 | 'application/vnd.wap.multipart.mixed') 362 | self.assertEqual(mms.data_parts[0].content_type, 'text/plain') 363 | self.assertEqual(mms.data_parts[0].data, text_data) 364 | self.assertEqual(mms.data_parts[0].content_type_parameters, 365 | {'Charset': 'utf-8'}) 366 | 367 | def test_encoding_m_sendnotifyresp_ind(self): 368 | message = MMSMessage() 369 | message.headers['Transaction-Id'] = 'NOK5AIdhfTMYSG4JeIgAAsHtp72AGAAAAAAAA' 370 | message.headers['Message-Type'] = 'm-notifyresp-ind' 371 | message.headers['Status'] = 'Retrieved' 372 | data = [ 373 | 140, 131, 152, 78, 79, 75, 53, 65, 73, 100, 104, 102, 84, 77, 374 | 89, 83, 71, 52, 74, 101, 73, 103, 65, 65, 115, 72, 116, 112, 375 | 55, 50, 65, 71, 65, 65, 65, 65, 65, 65, 65, 65, 0, 141, 144, 376 | 149, 129, 132, 163, 1, 35, 129] 377 | 378 | self.assertEqual(list(message.encode()[:50]), data) 379 | --------------------------------------------------------------------------------