Browse Source

Add receiving sms messages from asterisk via ami

devel
bashmak 8 years ago
parent
commit
ec78468ef4
  1. 141
      dialing.py
  2. 77
      dialing_app/locale/ru/LC_MESSAGES/django.po
  3. 35
      dialing_app/migrations/0002_auto_20171229_1353.py
  4. 18
      dialing_app/models.py
  5. 7
      dialing_app/templates/ext.html
  6. 30
      dialing_app/templates/inbox_sms.html
  7. 3
      dialing_app/urls.py
  8. 11
      dialing_app/views.py
  9. 2
      djing/settings_example.py
  10. 3
      messaging/__init__.py
  11. 63
      messaging/mms/__init__.py
  12. 71
      messaging/mms/iterator.py
  13. 555
      messaging/mms/message.py
  14. 996
      messaging/mms/mms_pdu.py
  15. 2055
      messaging/mms/wsp_pdu.py
  16. 7
      messaging/sms/__init__.py
  17. 14
      messaging/sms/base.py
  18. 17
      messaging/sms/consts.py
  19. 264
      messaging/sms/deliver.py
  20. 292
      messaging/sms/gsm0338.py
  21. 10
      messaging/sms/pdu.py
  22. 330
      messaging/sms/submit.py
  23. 79
      messaging/sms/udh.py
  24. 38
      messaging/sms/wap.py
  25. 0
      messaging/test/__init__.py
  26. BIN
      messaging/test/mms-data/27d0a048cd79555de05283a22372b0eb.mms
  27. BIN
      messaging/test/mms-data/BTMMS.MMS
  28. BIN
      messaging/test/mms-data/NOWMMS.MMS
  29. BIN
      messaging/test/mms-data/SEC-SGHS300M.mms
  30. BIN
      messaging/test/mms-data/SIMPLE.MMS
  31. BIN
      messaging/test/mms-data/SonyEricssonT310-R201.mms
  32. BIN
      messaging/test/mms-data/TOMSLOT.MMS
  33. BIN
      messaging/test/mms-data/gallery2test.mms
  34. BIN
      messaging/test/mms-data/iPhone.mms
  35. BIN
      messaging/test/mms-data/images_are_cut_off_debug.mms
  36. BIN
      messaging/test/mms-data/m.mms
  37. BIN
      messaging/test/mms-data/openwave.mms
  38. BIN
      messaging/test/mms-data/projekt_exempel.mms
  39. 267
      messaging/test/test_gsm_encoding.py
  40. 378
      messaging/test/test_mms.py
  41. 481
      messaging/test/test_sms.py
  42. 24
      messaging/test/test_udh.py
  43. 140
      messaging/test/test_wap.py
  44. 267
      messaging/utils.py
  45. 1
      requirements.txt
  46. 12
      systemd_units/djing_dial.service

141
dialing.py

@ -0,0 +1,141 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djing.settings")
django.setup()
from messaging.sms import SmsSubmit, SmsDeliver
import re
import asterisk.manager
from time import sleep
from dialing_app.models import SMSModel
from django.conf import settings
DJING_USERNAME_PASSWORD = getattr(settings, 'DJING_USERNAME_PASSWORD', ('admin', 'admin'))
class SMS(object):
def __init__(self, text, who, dev):
self.text = text
self.who = who
self.dev = dev
def __add__(self, other):
if not isinstance(other, SMS):
raise TypeError
if self.who == other.who and self.dev == other.dev:
self.text += other.text
return self
def __str__(self):
return "%s: %s" % (self.who, self.text)
class ChunkedMsg(object):
def __init__(self, sms_count, ref, sms):
self.sms_count = sms_count
self.ref = ref
self.sms = sms
class MyAstManager(asterisk.manager.Manager):
sms_chunks = list()
def new_chunked_sms(self, count, ref, sms):
msg = ChunkedMsg(count, ref, sms)
self.sms_chunks.append(msg)
@staticmethod
def save_sms(sms):
print('Inbox %s:' % sms.who, sms.text)
if not isinstance(sms, SMS):
raise TypeError
SMSModel.objects.create(
who=sms.who,
dev=sms.dev,
text=sms.text
)
def push_text(self, sms, ref, cnt):
if not isinstance(sms, SMS):
raise TypeError
chunk = [c for c in self.sms_chunks if c.ref == ref]
chunk_len = len(chunk)
if chunk_len == 1:
chunk = chunk[0]
chunk.sms += sms
if chunk.sms_count == cnt:
self.save_sms(chunk.sms)
self.sms_chunks.remove(chunk)
elif chunk_len == 0:
self.new_chunked_sms(cnt, ref, sms)
manager = MyAstManager()
def validate_tel(tel, reg=re.compile(r'^\+7978\d{7}$')):
return bool(re.match(reg, tel))
def send_sms(dev, recipient, utext):
if not validate_tel(recipient):
print("Tel %s is not valid" % recipient)
return
sms = SmsSubmit(recipient, utext)
for pdu in sms.to_pdu():
response = manager.command('dongle pdu %s %s' % (dev, pdu.pdu))
print(response.data)
def handle_shutdown(event, manager):
print("Recieved shutdown event")
manager.close()
# we could analize the event and reconnect here
def handle_inbox_long_sms_message(event, manager):
if event.has_header('Message'):
pdu = event.get_header('Message')
pdu = re.sub(r'^\+CMGR\:\s\d\,\,\d{1,3}\\r\\n', '', pdu)
sd = SmsDeliver(pdu)
data = sd.data
chunks_count = data.get('cnt')
sms = SMS(
text=data.get('text'),
who=data.get('number'),
dev=event.get_header('Device')
)
if chunks_count is not None:
# more than 1 message
manager.push_text(sms=sms, ref=data.get('ref'), cnt=chunks_count)
else:
# one message
manager.save_sms(sms)
if __name__ == '__main__':
try:
manager.connect('10.12.1.2')
manager.login(*DJING_USERNAME_PASSWORD)
# register some callbacks
manager.register_event('Shutdown', handle_shutdown)
manager.register_event('DongleNewCMGR', handle_inbox_long_sms_message) # PDU Here
# get a status report
response = manager.status()
print(response)
while True:
sleep(60)
except asterisk.manager.ManagerSocketException as e:
print("Error connecting to the manager: %s" % e.strerror)
except asterisk.manager.ManagerAuthException as e:
print("Error logging in to the manager: %s" % e.strerror)
except asterisk.manager.ManagerException as e:
print("Error: %s" % e.strerror)
finally:
manager.logoff()

77
dialing_app/locale/ru/LC_MESSAGES/django.po

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-22 11:59+0300\n"
"POT-Creation-Date: 2017-12-29 13:59+0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Dmitry Novikov nerosketch@gmail.com\n"
"Language: \n"
@ -19,85 +19,108 @@ msgstr ""
"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
"%100>=11 && n%100<=14)? 2 : 3);\n"
#: dialing_app/models.py:8 dialing_app/models.py:43
#: models.py:8 models.py:43
msgid "No answer"
msgstr "Не отвечен"
#: dialing_app/models.py:9 dialing_app/models.py:45
#: models.py:9 models.py:45
msgid "Failed"
msgstr "С ошибкой"
#: dialing_app/models.py:10 dialing_app/models.py:47
#: models.py:10 models.py:47
msgid "Busy"
msgstr "Занято"
#: dialing_app/models.py:11 dialing_app/models.py:49
#: models.py:11 models.py:49
msgid "Answered"
msgstr "Отвечен"
#: dialing_app/models.py:12 dialing_app/models.py:51
#: models.py:12 models.py:51
msgid "Unknown"
msgstr "Не определён"
#: dialing_app/templates/index.html:9
msgid "Dialing"
msgstr "Звонки"
#: models.py:79
msgid "Can view sms"
msgstr "Может просматривать смс"
#: models.py:81 models.py:82
msgid "SMS"
msgstr "СМС"
#: dialing_app/templates/index.html:12
#: templates/ext.html:7 templates/ext.html.py:21 views.py:19
msgid "Last calls"
msgstr "Последние звонки"
#: dialing_app/templates/index.html:20
#: templates/ext.html:28 views.py:46
msgid "Voice mail request"
msgstr "Заявки на подключение"
#: templates/ext.html:35 views.py:57
msgid "Voice mail report"
msgstr "Заявки на поломки"
#: templates/index.html:10 templates/vmail.html.py:10
msgid "Play"
msgstr "Слушать"
#: dialing_app/templates/index.html:21
#: templates/index.html:11 templates/vmail.html.py:11
msgid "calldate"
msgstr "дата звонка"
#: dialing_app/templates/index.html:22
#: templates/index.html:12 templates/vmail.html.py:12
msgid "src"
msgstr "кто"
#: dialing_app/templates/index.html:23
#: templates/index.html:13
msgid "dst"
msgstr "куда"
#: dialing_app/templates/index.html:24
#: templates/index.html:14 templates/vmail.html.py:13
msgid "duration"
msgstr "продолжительность"
#: dialing_app/templates/index.html:25
#: templates/index.html:15 templates/vmail.html.py:14
msgid "start"
msgstr "начало"
#: dialing_app/templates/index.html:26
#: templates/index.html:16 templates/vmail.html.py:15
msgid "answer"
msgstr "ответ"
#: dialing_app/templates/index.html:27
#: templates/index.html:17 templates/vmail.html.py:16
msgid "end"
msgstr "конец"
#: dialing_app/templates/index.html:28
#: templates/index.html:18 templates/vmail.html.py:17
msgid "disposition"
msgstr "состояние"
#: dialing_app/templates/index.html:50
#: templates/index.html:24 views.py:74
msgid "Find dials"
msgstr "Найти звонки"
#: templates/index.html:26
msgid "Telephone"
msgstr "Телефон"
#: templates/index.html:47
msgid "Download"
msgstr "Скачать"
#: templates/index.html:63 templates/vmail.html.py:42
msgid "Calls was not found"
msgstr "Звонки не найдены"
#: dialing_app/views.py:27
#: views.py:32
msgid "Multiple users with the telephone number"
msgstr "Несколько абонентов с указанным номером телефона"
#: dialing_app/views.py:29
#: views.py:34
msgid "User with the telephone number not found"
msgstr "Абонент с таким номером телефона не найден"
msgid "Voice mail request"
msgstr "Заявки на подключение"
msgid "Voice mail report"
msgstr "Заявки на поломки"
msgid "Dialing"
msgstr "Звонки"
msgid "Inbox sms"
msgstr "Входящие смс"

35
dialing_app/migrations/0002_auto_20171229_1353.py

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2017-12-29 13:53
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dialing_app', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='SMSModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('when', models.DateTimeField(auto_now_add=True)),
('who', models.CharField(max_length=32)),
('dev', models.CharField(max_length=20)),
('text', models.CharField(max_length=255)),
],
options={
'verbose_name': 'SMS',
'verbose_name_plural': 'SMS',
'db_table': 'sms',
'permissions': (('can_view_sms', 'Can view sms'),),
},
),
migrations.AlterModelOptions(
name='asteriskcdr',
options={'managed': False},
),
]

18
dialing_app/models.py

@ -65,3 +65,21 @@ class AsteriskCDR(models.Model):
class Meta:
db_table = 'cdr'
managed = False
class SMSModel(models.Model):
when = models.DateTimeField(auto_now_add=True)
who = models.CharField(max_length=32)
dev = models.CharField(max_length=20)
text = models.CharField(max_length=255)
class Meta:
db_table = 'sms'
permissions = (
('can_view_sms', _('Can view sms')),
)
verbose_name = _('SMS')
verbose_name_plural = _('SMS')
def __str__(self):
return self.text

7
dialing_app/templates/ext.html

@ -36,6 +36,13 @@
</a>
</li>
{% url 'dialapp:inbox_sms' as dialsmsin %}
<li{% if dialsmsin == request.path %} class="active"{% endif %}>
<a href="{{ dialsmsin }}">
{% trans 'Inbox sms' %}
</a>
</li>
</ul>

30
dialing_app/templates/inbox_sms.html

@ -0,0 +1,30 @@
{% extends request.is_ajax|yesno:'nullcont.htm,ext.html' %}
{% load i18n %}
{% block content %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans 'Inbox sms' %}</h3>
</div>
<div class="list-group scroll-area">
{% for msg in sms_messages %}
<div class="list-group-item">
<h5>From {{ msg.who }}
<small>{{ msg.when|date:'d M, H:i:s' }} via {{ msg.dev }}</small>
</h5>
<p>{{ msg.text }}</p>
</div>
{% empty %}
<div class="list-group-item">
<h4 class="list-group-item-heading">{% trans 'Message history is empty' %}</h4>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

3
dialing_app/urls.py

@ -7,5 +7,6 @@ urlpatterns = [
url(r'^filter$', views.vfilter, name='vfilter'),
url(r'^to_abon(?P<tel>\+?\d+)$', views.to_abon, name='to_abon'),
url(r'^requests$', views.vmail_request, name='vmail_request'),
url(r'^reports$', views.vmail_report, name='vmail_report')
url(r'^reports$', views.vmail_report, name='vmail_report'),
url(r'^sms/in$', views.inbox_sms, name='inbox_sms')
]

11
dialing_app/views.py

@ -7,7 +7,7 @@ from django.db.models import Q
from abonapp.models import Abon
from mydefs import only_admins, pag_mn
from .models import AsteriskCDR
from .models import AsteriskCDR, SMSModel
@login_required
@ -75,3 +75,12 @@ def vfilter(request):
's': s
})
@login_required
@permission_required('dialing_app.can_view_sms')
def inbox_sms(request):
msgs = SMSModel.objects.all()
msgs = pag_mn(request, msgs)
return render(request, 'inbox_sms.html', {
'sms_messages': msgs
})

2
djing/settings_example.py

@ -181,3 +181,5 @@ DEFAULT_SNMP_PASSWORD = 'public'
TELEGRAM_BOT_TOKEN = 'bot token'
TELEPHONE_REGEXP = r'^\+[7,8,9,3]\d{10,11}$'
DJING_USERNAME_PASSWORD = ('username', 'secret')

3
messaging/__init__.py

@ -0,0 +1,3 @@
# see LICENSE
VERSION = (0, 5, 12)

63
messaging/mms/__init__.py

@ -0,0 +1,63 @@
# This library is free software.
#
# It was originally distributed under the terms of the GNU Lesser
# General Public License Version 2.
#
# python-messaging opts to apply the terms of the ordinary GNU
# General Public License v2, as permitted by section 3 of the LGPL
# v2.1. This re-licensing allows the entirety of python-messaging to
# be distributed according to the terms of GPL-2.
#
# See the COPYING file included in this archive
#
# Copyright (C) 2007 Francois Aucamp <francois.aucamp@gmail.com>
#
"""
Multimedia Messaging Service (MMS) library
The :mod:`messaging.mms` module provides several classes for the creation
and manipulation of MMS messages (multimedia messages) used in mobile
devices such as cellular telephones.
Multimedia Messaging Service (MMS) is a messaging service for the mobile
environment standardized by the WAP Forum and 3GPP. To the end-user MMS is
very similar to the text-based Short Message Service (SMS): it provides
automatic immediate delivery for user-created content from device to device.
In addition to text, however, MMS messages can contain multimedia content such
as still images, audio clips and video clips, which are binded together
into a "mini presentation" (or slideshow) that controls for example, the order
in which images are to appear on the screen, how long they will be displayed,
when an audio clip should be played, etc. Furthermore, MMS messages do not have
the 160-character limit of SMS messages.
An MMS message is a multimedia presentation in one entity; it is not a text
file with attachments.
This library enables the creation of MMS messages with full support for
presentation layout, and multimedia data parts such as JPEG, GIF, AMR, MIDI,
3GP, etc. It also allows the decoding and unpacking of received MMS messages.
@version: 0.2
@author: Francois Aucamp C{<francois.aucamp@gmail.com>}
@license: GNU General Public License, version 2
@note: References used in the code and this document:
.. [1] MMS Conformance Document version 2.0.0, 6 February 2002
U{www.bogor.net/idkf/bio2/mobile-docs/mms_conformance_v2_0_0.pdf}
.. [2] Forum Nokia, "How To Create MMS Services, Version 4.0"
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}
.. [3] Wap Forum/Open Mobile ALliance, "WAP-206 MMS Client Transactions"
U{http://www.openmobilealliance.org/tech/affiliates/LicenseAgreement.asp?DocName=/wap/wap-206-mmsctr-20020115-a.pdf}
.. [4] Wap Forum/Open Mobile Alliance, "WAP-209 MMS Encapsulation Protocol"
U{http://www.openmobilealliance.org/tech/affiliates/LicenseAgreement.asp?DocName=/wap/wap-209-mmsencapsulation-20020105-a.pdf}
.. [5] Wap Forum/Open Mobile Alliance, "WAP-230 Wireless Session Protocol Specification"
U{http://www.openmobilealliance.org/tech/affiliates/LicenseAgreement.asp?DocName=/wap/wap-230-wsp-20010705-a.pdf}
.. [6] IANA: "Character Sets"
U{http://www.iana.org/assignments/character-sets}
"""

71
messaging/mms/iterator.py

@ -0,0 +1,71 @@
# This library is free software.
#
# It was originally distributed under the terms of the GNU Lesser
# General Public License Version 2.
#
# python-messaging opts to apply the terms of the ordinary GNU
# General Public License v2, as permitted by section 3 of the LGPL
# v2.1. This re-licensing allows the entirety of python-messaging to
# be distributed according to the terms of GPL-2.
#
# See the COPYING file included in this archive
#
# The docstrings in this module contain epytext markup; API documentation
# may be created by processing this file with epydoc: http://epydoc.sf.net
"""Iterator with "value preview" capability."""
class PreviewIterator(object):
"""An ``iter`` wrapper class providing a "previewable" iterator.
This "preview" functionality allows the iterator to return successive
values from its ``iterable`` object, without actually mvoving forward
itself. This is very usefuly if the next item(s) in an iterator must
be used for something, after which the iterator should "undo" those
read operations, so that they can be read again by another function.
From the user point of view, this class supersedes the builtin iter()
function: like iter(), it is called as PreviewIter(iterable).
"""
def __init__(self, data):
self._it = iter(data)
self._cached_values = []
self._preview_pos = 0
def __iter__(self):
return self
def next(self):
self.reset_preview()
if len(self._cached_values) > 0:
return self._cached_values.pop(0)
else:
return self._it.next()
def preview(self):
"""
Return the next item in the ``iteratable`` object
But it does not modify the actual iterator (i.e. do not
intefere with :func:`next`.
Successive calls to :func:`preview` will return successive values from
the ``iterable`` object, exactly in the same way :func:`next` does.
However, :func:`preview` will always return the next item from
``iterable`` after the item returned by the previous :func:`preview`
or :func:`next` call, whichever was called the most recently.
To force the "preview() iterator" to synchronize with the "next()
iterator" (without calling :func:`next`), use :func:`reset_preview`.
"""
if self._preview_pos < len(self._cached_values):
value = self._cached_values[self._preview_pos]
else:
value = self._it.next()
self._cached_values.append(value)
self._preview_pos += 1
return value
def reset_preview(self):
self._preview_pos = 0

555
messaging/mms/message.py

@ -0,0 +1,555 @@
# This library is free software.
#
# It was originally distributed under the terms of the GNU Lesser
# General Public License Version 2.
#
# python-messaging opts to apply the terms of the ordinary GNU
# General Public License v2, as permitted by section 3 of the LGPL
# v2.1. This re-licensing allows the entirety of python-messaging to
# be distributed according to the terms of GPL-2.
#
# See the COPYING file included in this archive
#
# The docstrings in this module contain epytext markup; API documentation
# may be created by processing this file with epydoc: http://epydoc.sf.net
"""High-level MMS message classes"""
from __future__ import with_statement
import array
import mimetypes
import os
import xml.dom.minidom
class MMSMessage:
"""
I am an MMS message
References used in this class: [1][2][3][4][5]
"""
def __init__(self):
self._pages = []
self._data_parts = []
self._metaTags = {}
self._mms_message = None
self.headers = {
'Message-Type': 'm-send-req',
'Transaction-Id': '1234',
'MMS-Version': '1.0',
'Content-Type': ('application/vnd.wap.multipart.mixed', {}),
}
self.width = 176
self.height = 220
self.transactionID = '12345'
self.subject = 'test'
@property
def content_type(self):
"""
Returns the Content-Type of this data part header
No parameter information is returned; to get that, access the
"Content-Type" header directly (which has a tuple value) from
the message's ``headers`` attribute.
This is equivalent to calling DataPart.headers['Content-Type'][0]
"""
return self.headers['Content-Type'][0]
def add_page(self, page):
"""
Adds `page` to the message
:type page: MMSMessagePage
:param page: The message slide/page to add
"""
if self.content_type != 'application/vnd.wap.multipart.related':
value = ('application/vnd.wap.multipart.related', {})
self.headers['Content-Type'] = value
self._pages.append(page)
@property
def pages(self):
"""Returns a list of all the pages in this message"""
return self._pages
def add_data_part(self, data_part):
"""Adds a single data part (DataPart object) to the message, without
connecting it to a specific slide/page in the message.
A data part encapsulates some form of attachment, e.g. an image, audio
etc. It is not necessary to explicitly add data parts to the message
using this function if :func:`add_page` is used; this method is mainly
useful if you want to create MMS messages without SMIL support,
i.e. messages of type "application/vnd.wap.multipart.mixed"
:param data_part: The data part to add
:type data_part: DataPart
"""
self._data_parts.append(data_part)
@property
def data_parts(self):
"""
Returns a list of all the data parts in this message
including data parts that were added to slides in this message"""
parts = []
if len(self._pages):
parts.append(self.smil())
for slide in self._mms_message._pages:
parts.extend(slide.data_parts())
parts.extend(self._data_parts)
return parts
def smil(self):
"""Returns the text of the message's SMIL file"""
impl = xml.dom.minidom.getDOMImplementation()
smil_doc = impl.createDocument(None, "smil", None)
# Create the SMIL header
head_node = smil_doc.createElement('head')
# Add metadata to header
for tag_name in self._metaTags:
meta_node = smil_doc.createElement('meta')
meta_node.setAttribute(tag_name, self._metaTags[tag_name])
head_node.appendChild(meta_node)
# Add layout info to header
layout_node = smil_doc.createElement('layout')
root_layout_node = smil_doc.createElement('root-layout')
root_layout_node.setAttribute('width', str(self.width))
root_layout_node.setAttribute('height', str(self.height))
layout_node.appendChild(root_layout_node)
areas = (('Image', '0', '0', '176', '144'),
('Text', '176', '144', '176', '76'))
for region_id, left, top, width, height in areas:
region_node = smil_doc.createElement('region')
region_node.setAttribute('id', region_id)
region_node.setAttribute('left', left)
region_node.setAttribute('top', top)
region_node.setAttribute('width', width)
region_node.setAttribute('height', height)
layout_node.appendChild(region_node)
head_node.appendChild(layout_node)
smil_doc.documentElement.appendChild(head_node)
# Create the SMIL body
body_node = smil_doc.createElement('body')
# Add pages to body
for page in self._pages:
par_node = smil_doc.createElement('par')
par_node.setAttribute('duration', str(page.duration))
# Add the page content information
if page.image is not None:
#TODO: catch unpack exception
part, begin, end = page.image
if 'Content-Location' in part.headers:
src = part.headers['Content-Location']
elif 'Content-ID' in part.headers:
src = part.headers['Content-ID']
else:
src = part.data
image_node = smil_doc.createElement('img')
image_node.setAttribute('src', src)
image_node.setAttribute('region', 'Image')
if begin > 0 or end > 0:
if end > page.duration:
end = page.duration
image_node.setAttribute('begin', str(begin))
image_node.setAttribute('end', str(end))
par_node.appendChild(image_node)
if page.text is not None:
part, begin, end = page.text
src = part.data
text_node = smil_doc.createElement('text')
text_node.setAttribute('src', src)
text_node.setAttribute('region', 'Text')
if begin > 0 or end > 0:
if end > page.duration:
end = page.duration
text_node.setAttribute('begin', str(begin))
text_node.setAttribute('end', str(end))
par_node.appendChild(text_node)
if page.audio is not None:
part, begin, end = page.audio
if 'Content-Location' in part.headers:
src = part.headers['Content-Location']
elif 'Content-ID' in part.headers:
src = part.headers['Content-ID']
else:
src = part.data
audio_node = smil_doc.createElement('audio')
audio_node.setAttribute('src', src)
if begin > 0 or end > 0:
if end > page.duration:
end = page.duration
audio_node.setAttribute('begin', str(begin))
audio_node.setAttribute('end', str(end))
par_node.appendChild(text_node)
par_node.appendChild(audio_node)
body_node.appendChild(par_node)
smil_doc.documentElement.appendChild(body_node)
return smil_doc.documentElement.toprettyxml()
def encode(self):
"""
Return a binary representation of this MMS message
This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally
:return: The binary-encoded MMS data, as an array of bytes
:rtype: array.array('B')
"""
from messaging.mms import mms_pdu
encoder = mms_pdu.MMSEncoder()
return encoder.encode(self)
def to_file(self, filename):
"""
Writes this MMS message to `filename` in binary-encoded form
This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally
:param filename: The path where to store the message data
:type filename: str
:rtype array.array('B')
:return: The binary-encode MMS data, as an array of bytes
"""
with open(filename, 'wb') as f:
self.encode().tofile(f)
@staticmethod
def from_data(data):
"""
Returns a new `:class:MMSMessage` out of ``data``
This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally
:param data: The data to load
:type filename: array.array
"""
from messaging.mms import mms_pdu
decoder = mms_pdu.MMSDecoder()
return decoder.decode_data(data)
@staticmethod
def from_file(filename):
"""
Returns a new `:class:MMSMessage` out of file ``filename``
This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally
:param filename: The name of the file to load
:type filename: str
"""
from messaging.mms import mms_pdu
decoder = mms_pdu.MMSDecoder()
return decoder.decode_file(filename)
class MMSMessagePage:
"""
A single page/slide in an MMS Message.
In order to ensure that the MMS message can be correctly displayed by most
terminals, each page's content is limited to having 1 image, 1 audio clip
and 1 block of text, as stated in [1].
The default slide duration is set to 4 seconds; use :func:`set_duration`
to change this.
"""
def __init__(self):
self.duration = 4000
self.image = None
self.audio = None
self.text = None
@property
def data_parts(self):
"""Returns a list of the data parst in this slide"""
return [part for part in (self.image, self.audio, self.text)
if part is not None]
def number_of_parts(self):
"""
Returns the number of data parts in this slide
@rtype: int
"""
num_parts = 0
for item in (self.image, self.audio, self.text):
if item is not None:
num_parts += 1
return num_parts
#TODO: find out what the "ref" element in SMIL does
#TODO: add support for "alt" element; also make sure what it does
def add_image(self, filename, time_begin=0, time_end=0):
"""
Adds an image to this slide.
:param filename: The name of the image file to add. Supported formats
are JPEG, GIF and WBMP.
:type filename: str
:param time_begin: The time (in milliseconds) during the duration of
this slide to begin displaying the image. If this is
0 or less, the image will be displayed from the
moment the slide is opened.
:type time_begin: int
:param time_end: The time (in milliseconds) during the duration of this
slide at which to stop showing (i.e. hide) the image.
If this is 0 or less, or if it is greater than the
actual duration of this slide, it will be shown until
the next slide is accessed.
:type time_end: int
:raise TypeError: An inappropriate variable type was passed in of the
parameters
"""
if not isinstance(filename, str):
raise TypeError("filename must be a string")
if not isinstance(time_begin, int) or not isinstance(time_end, int):
raise TypeError("time_begin and time_end must be ints")
if not os.path.isfile(filename):
raise OSError("filename must be a file")
if time_end > 0 and time_end < time_begin:
raise ValueError('time_end cannot be lower than time_begin')
self.image = (DataPart(filename), time_begin, time_end)
def add_audio(self, filename, time_begin=0, time_end=0):
"""
Adds an audio clip to this slide.
:param filename: The name of the audio file to add. Currently the only
supported format is AMR.
:type filename: str
:param time_begin: The time (in milliseconds) during the duration of
this slide to begin playback of the audio clip. If
this is 0 or less, the audio clip will be played the
moment the slide is opened.
:type time_begin: int
:param time_end: The time (in milliseconds) during the duration of this
slide at which to stop playing (i.e. mute) the audio
clip. If this is 0 or less, or if it is greater than
the actual duration of this slide, the entire audio
clip will be played, or until the next slide is
accessed.
:type time_end: int
:raise TypeError: An inappropriate variable type was passed in of the
parameters
"""
if not isinstance(filename, str):
raise TypeError("filename must be a string")
if not isinstance(time_begin, int) or not isinstance(time_end, int):
raise TypeError("time_begin and time_end must be ints")
if not os.path.isfile(filename):
raise OSError("filename must be a file")
if time_end > 0 and time_end < time_begin:
raise ValueError('time_end cannot be lower than time_begin')
self.audio = (DataPart(filename), time_begin, time_end)
def add_text(self, text, time_begin=0, time_end=0):
"""
Adds a block of text to this slide.
:param text: The text to add to the slide.
:type text: str
:param time_begin: The time (in milliseconds) during the duration of
this slide to begin displaying the text. If this is
0 or less, the text will be displayed from the
moment the slide is opened.
:type time_begin: int
:param time_end: The time (in milliseconds) during the duration of this
slide at which to stop showing (i.e. hide) the text.
If this is 0 or less, or if it is greater than the
actual duration of this slide, it will be shown until
the next slide is accessed.
:type time_end: int
:raise TypeError: An inappropriate variable type was passed in of the
parameters
"""
if not isinstance(text, str):
raise TypeError("Text must be a string")
if not isinstance(time_begin, int) or not isinstance(time_end, int):
raise TypeError("time_begin and time_end must be ints")
if time_end > 0 and time_end < time_begin:
raise ValueError('time_end cannot be lower than time_begin')
time_data = DataPart()
time_data.set_text(text)
self.text = (time_data, time_begin, time_end)
def set_duration(self, duration):
""" Sets the maximum duration of this slide (i.e. how long this slide
should be displayed)
@param duration: the maxium slide duration, in milliseconds
@type duration: int
@raise TypeError: <duration> must be an integer
@raise ValueError: the requested duration is invalid (must be a
non-zero, positive integer)
"""
if not isinstance(duration, int):
raise TypeError("Duration must be an int")
if duration < 1:
raise ValueError('duration may not be 0 or negative')
self.duration = duration
class DataPart(object):
"""
I am a data entry in the MMS body
A DataPart object encapsulates any data content that is to be added
to the MMS (e.g. an image , raw image data, audio clips, text, etc).
A DataPart object can be queried using the Python built-in :func:`len`
function.
This encapsulation allows custom header/parameter information to be set
for each data entry in the MMS. Refer to [5] for more information on
these.
"""
def __init__(self, filename=None):
""" @param srcFilename: If specified, load the content of the file
with this name
@type srcFilename: str
"""
super(DataPart, self).__init__()
self.content_type_parameters = {}
self.headers = {'Content-Type': ('application/octet-stream', {})}
self._filename = None
self._data = None
if filename is not None:
self.from_file(filename)
def _get_content_type(self):
""" Returns the string representation of this data part's
"Content-Type" header. No parameter information is returned;
to get that, access the "Content-Type" header directly (which has a
tuple value)from this part's C{headers} attribute.
This is equivalent to calling DataPart.headers['Content-Type'][0]
"""
return self.headers['Content-Type'][0]
def _set_content_type(self, value):
"""Sets the content type string, with no parameters """
self.headers['Content-Type'] = value, {}
content_type = property(_get_content_type, _set_content_type)
def from_file(self, filename):
"""
Load the data contained in the specified file
This function clears any previously-set header entries.
:param filename: The name of the file to open
:type filename: str
:raises OSError: The filename is invalid
"""
if not os.path.isfile(filename):
raise OSError('The file "%s" does not exist' % filename)
# Clear any headers that are currently set
self.headers = {}
self._data = None
self.headers['Content-Location'] = os.path.basename(filename)
content_type = (mimetypes.guess_type(filename)[0]
or 'application/octet-stream', {})
self.headers['Content-Type'] = content_type
self._filename = filename
def set_data(self, data, content_type, ct_parameters=None):
"""
Explicitly set the data contained by this part
This function clears any previously-set header entries.
:param data: The data to hold
:type data: str
:param content_type: The MIME content type of the specified data
:type content_type: str
:param ct_parameters: Any content type header paramaters to add
:type ct_parameters: dict
"""
self.headers = {}
self._filename = None
self._data = data
if ct_parameters is None:
ct_parameters = {}
self.headers['Content-Type'] = content_type, ct_parameters
def set_text(self, text):
"""
Convenience wrapper method for set_data()
This method sets the :class:`DataPart` object to hold the
specified text string, with MIME content type "text/plain".
@param text: The text to hold
@type text: str
"""
self.set_data(text, 'text/plain')
def __len__(self):
"""Provides the length of the data encapsulated by this object"""
if self._filename is not None:
return int(os.stat(self._filename)[6])
else:
return len(self.data)
@property
def data(self):
"""A buffer containing the binary data of this part"""
if self._data is not None:
if type(self._data) == array.array:
self._data = self._data.tostring()
return self._data
elif self._filename is not None:
with open(self._filename, 'r') as f:
self._data = f.read()
return self._data
return ''

996
messaging/mms/mms_pdu.py

@ -0,0 +1,996 @@
# This library is free software.
#
# It was originally distributed under the terms of the GNU Lesser
# General Public License Version 2.
#
# python-messaging opts to apply the terms of the ordinary GNU
# General Public License v2, as permitted by section 3 of the LGPL
# v2.1. This re-licensing allows the entirety of python-messaging to
# be distributed according to the terms of GPL-2.
#
# See the COPYING file included in this archive
"""MMS Data Unit structure encoding and decoding classes"""
from __future__ import with_statement
import array
import os
import random
from messaging.utils import debug
from messaging.mms import message, wsp_pdu
from messaging.mms.iterator import PreviewIterator
def flatten_list(x):
"""Flattens ``x`` into a single list"""
result = []
for el in x:
if hasattr(el, "__iter__") and not isinstance(el, basestring):
result.extend(flatten_list(el))
else:
result.append(el)
return result
mms_field_names = {
0x01: ('Bcc', 'encoded_string_value'),
0x02: ('Cc', 'encoded_string_value'),
0x03: ('Content-Location', 'uri_value'),
0x04: ('Content-Type', 'content_type_value'),
0x05: ('Date', 'date_value'),
0x06: ('Delivery-Report', 'boolean_value'),
0x07: ('Delivery-Time', 'delivery_time_value'),
0x08: ('Expiry', 'expiry_value'),
0x09: ('From', 'from_value'),
0x0a: ('Message-Class', 'message_class_value'),
0x0b: ('Message-ID', 'text_string'),
0x0c: ('Message-Type', 'message_type_value'),
0x0d: ('MMS-Version', 'version_value'),
0x0e: ('Message-Size', 'long_integer'),
0x0f: ('Priority', 'priority_value'),
0x10: ('Read-Reply', 'boolean_value'),
0x11: ('Report-Allowed', 'boolean_value'),
0x12: ('Response-Status', 'response_status_value'),
0x13: ('Response-Text', 'encoded_string_value'),
0x14: ('Sender-Visibility', 'sender_visibility_value'),
0x15: ('Status', 'status_value'),
0x16: ('Subject', 'encoded_string_value'),
0x17: ('To', 'encoded_string_value'),
0x18: ('Transaction-Id', 'text_string'),
}
class MMSDecoder(wsp_pdu.Decoder):
"""A decoder for MMS messages"""
def __init__(self, filename=None):
"""
:param filename: If specified, decode the content of the MMS
message file with this name
:type filename: str
"""
self._mms_data = array.array('B')
self._mms_message = message.MMSMessage()
self._parts = []
def decode_file(self, filename):
"""
Load the data contained in the specified ``filename``, and decode it.
:param filename: The name of the MMS message file to open
:type filename: str
:raise OSError: The filename is invalid
:return: The decoded MMS data
:rtype: MMSMessage
"""
num_bytes = os.stat(filename)[6]
data = array.array('B')
with open(filename, 'rb') as f:
data.fromfile(f, num_bytes)
return self.decode_data(data)
def decode_data(self, data):
"""
Decode the specified MMS message data
:param data: The MMS message data to decode
:type data: array.array('B')
:return: The decoded MMS data
:rtype: MMSMessage
"""
self._mms_message = message.MMSMessage()
self._mms_data = data
body_iter = self.decode_message_header()
self.decode_message_body(body_iter)
return self._mms_message
def decode_message_header(self):
"""
Decodes the (full) MMS header data
This must be called before :func:`_decodeBody`, as it sets
certain internal variables relating to data lengths, etc.
"""
data_iter = PreviewIterator(self._mms_data)
# First 3 headers (in order
############################
# - X-Mms-Message-Type
# - X-Mms-Transaction-ID
# - X-Mms-Version
# TODO: reimplement strictness - currently we allow these 3 headers
# to be mixed with any of the other headers (this allows the
# decoding of "broken" MMSs, but is technically incorrect)
# Misc headers
##############
# The next few headers will not be in a specific order, except for
# "Content-Type", which should be the last header
# According to [4], MMS header field names will be short integers
content_type_found = False
header = ''
while content_type_found == False:
try:
header, value = self.decode_header(data_iter)
except StopIteration:
break
if header == mms_field_names[0x04][0]:
content_type_found = True
else:
self._mms_message.headers[header] = value
if header == 'Content-Type':
# Otherwise it might break Content-Location
# content_type, params = value
self._mms_message.headers[header] = value
return data_iter
def decode_message_body(self, data_iter):
"""
Decodes the MMS message body
:param data_iter: an iterator over the sequence of bytes of the MMS
body
:type data_iter: iter
"""
######### MMS body: headers ###########
# Get the number of data parts in the MMS body
try:
num_entries = self.decode_uint_var(data_iter)
except StopIteration:
return
#print 'Number of data entries (parts) in MMS body:', num_entries
########## MMS body: entries ##########
# For every data "part", we have to read the following sequence:
# <length of content-type + other possible headers>,
# <length of data>,
# <content-type + other possible headers>,
# <data>
for part_num in xrange(num_entries):
#print '\nPart %d:\n------' % part_num
headers_len = self.decode_uint_var(data_iter)
data_len = self.decode_uint_var(data_iter)
# Prepare to read content-type + other possible headers
ct_field_bytes = []
for i in xrange(headers_len):
ct_field_bytes.append(data_iter.next())
ct_iter = PreviewIterator(ct_field_bytes)
# Get content type
ctype, ct_parameters = self.decode_content_type_value(ct_iter)
headers = {'Content-Type': (ctype, ct_parameters)}
# Now read other possible headers until <headers_len> bytes
# have been read
while True:
try:
hdr, value = self.decode_header(ct_iter)
headers[hdr] = value
except StopIteration:
break
# Data (note: this is not null-terminated)
data = array.array('B')
for i in xrange(data_len):
data.append(data_iter.next())
part = message.DataPart()
part.set_data(data, ctype)
part.content_type_parameters = ct_parameters
part.headers = headers
self._mms_message.add_data_part(part)
@staticmethod
def decode_header(byte_iter):
"""
Decodes a header entry from an MMS message
starting at the byte pointed to by :func:`byte_iter.next`
From [4], section 7.1::
Header = MMS-header | Application-header
The return type of the "header value" depends on the header
itself; it is thus up to the function calling this to determine
what that type is (or at least compensate for possibly
different return value types).
:raise DecodeError: This uses :func:`decode_mms_header` and
:func:`decode_application_header`, and will raise this
exception under the same circumstances as
:func:`decode_application_header`. ``byte_iter`` will
not be modified in this case.
:return: The decoded header entry from the MMS, in the format:
(<str:header name>, <str/int/float:header value>)
:rtype: tuple
"""
try:
return MMSDecoder.decode_mms_header(byte_iter)
except wsp_pdu.DecodeError:
return wsp_pdu.Decoder.decode_header(byte_iter)
@staticmethod
def decode_mms_header(byte_iter):
"""
Decodes the MMS header pointed by ``byte_iter``
This method takes into account the assigned number values for MMS
field names, as specified in [4], section 7.3, table 8.
From [4], section 7.1::
MMS-header = MMS-field-name MMS-value
MMS-field-name = Short-integer
MMS-value = Bcc-value | Cc-value | Content-location-value | Content-type-value | etc
:raise wsp_pdu.DecodeError: The MMS field name could not be parsed.
``byte_iter`` will not be modified.
:return: The decoded MMS header, in the format:
(<str:MMS-field-name>, <str:MMS-value>)
:rtype: tuple
"""
# Get the MMS-field-name
mms_field_name = ''
preview = byte_iter.preview()
byte = wsp_pdu.Decoder.decode_short_integer_from_byte(preview)
if byte in mms_field_names:
byte_iter.next()
mms_field_name = mms_field_names[byte][0]
else:
byte_iter.reset_preview()
raise wsp_pdu.DecodeError('Invalid MMS Header: could '
'not decode MMS field name')
# Now get the MMS-value
mms_value = ''
try:
name = mms_field_names[byte][1]
mms_value = getattr(MMSDecoder, 'decode_%s' % name)(byte_iter)
except wsp_pdu.DecodeError, msg:
raise wsp_pdu.DecodeError('Invalid MMS Header: Could '
'not decode MMS-value: %s' % msg)
except:
raise RuntimeError('A fatal error occurred, probably due to an '
'unimplemented decoding operation. Tried to '
'decode header: %s' % mms_field_name)
return mms_field_name, mms_value
@staticmethod
def decode_encoded_string_value(byte_iter):
"""
Decodes the encoded string value pointed by ``byte_iter``
From [4], section 7.2.9::
Encoded-string-value = Text-string | Value-length Char-set Text-string
The Char-set values are registered by IANA as MIBEnum value.
This function is not fully implemented, in that it does not
have proper support for the Char-set values; it basically just
reads over that sequence of bytes, and ignores it (see code for
details) - any help with this will be greatly appreciated.
:return: The decoded text string
:rtype: str
"""
try:
# First try "Value-length Char-set Text-string"
value_length = wsp_pdu.Decoder.decode_value_length(byte_iter)
# TODO: add proper support for charsets...
try:
charset = wsp_pdu.Decoder.decode_well_known_charset(byte_iter)
except wsp_pdu.DecodeError, msg:
raise Exception('encoded_string_value decoding error - '
'Could not decode Charset value: %s' % msg)
return wsp_pdu.Decoder.decode_text_string(byte_iter)
except wsp_pdu.DecodeError:
# Fall back on just "Text-string"
return wsp_pdu.Decoder.decode_text_string(byte_iter)
@staticmethod
def decode_boolean_value(byte_iter):
"""
Decodes the boolean value pointed by ``byte_iter``
From [4], section 7.2.6::
Delivery-report-value = Yes | No
Yes = <Octet 128>
No = <Octet 129>
A lot of other yes/no fields use this encoding (read-reply,
report-allowed, etc)
:raise wsp_pdu.DecodeError: The boolean value could not be parsed.
``byte_iter`` will not be modified.
:return: The value for the field
:rtype: bool
"""
byte = byte_iter.preview()
if byte not in (128, 129):
byte_iter.reset_preview()
raise wsp_pdu.DecodeError('Error parsing boolean value '
'for byte: %s' % hex(byte))
byte = byte_iter.next()
return byte == 128
@staticmethod
def decode_delivery_time_value(byte_iter):
value_length = wsp_pdu.Decoder.decode_value_length(byte_iter)
token = byte_iter.next()
value = wsp_pdu.Decoder.decode_long_integer(byte_iter)
if token == 128:
token_type = 'absolute'
elif token == 129:
token_type = 'relative'
else:
raise wsp_pdu.DecodeError('Delivery-Time type token value is undefined'
' (%s), should be either 128 or 129' % token)
return (token_type, value)
@staticmethod
def decode_from_value(byte_iter):
"""
Decodes the "From" value pointed by ``byte_iter``
From [4], section 7.2.11::
From-value = Value-length (Address-present-token Encoded-string-value | Insert-address-token )
Address-present-token = <Octet 128>
Insert-address-token = <Octet 129>
:return: The "From" address value
:rtype: str
"""
value_length = wsp_pdu.Decoder.decode_value_length(byte_iter)
# See what token we have
byte = byte_iter.next()
if byte == 129: # Insert-address-token
return '<not inserted>'
return MMSDecoder.decode_encoded_string_value(byte_iter)
@staticmethod
def decode_message_class_value(byte_iter):
"""
Decodes the "Message-Class" value pointed by ``byte_iter``
From [4], section 7.2.12::
Message-class-value = Class-identifier | Token-text
Class-identifier = Personal | Advertisement | Informational | Auto
Personal = <Octet 128>
Advertisement = <Octet 129>
Informational = <Octet 130>
Auto = <Octet 131>
The token-text is an extension method to the message class.
:return: The decoded message class
:rtype: str
"""
class_identifiers = {
128: 'Personal',
129: 'Advertisement',
130: 'Informational',
131: 'Auto',
}
byte = byte_iter.preview()
if byte in class_identifiers:
byte_iter.next()
return class_identifiers[byte]
byte_iter.reset_preview()
return wsp_pdu.Decoder.decode_token_text(byte_iter)
@staticmethod
def decode_message_type_value(byte_iter):
"""
Decodes the "Message-Type" value pointed by ``byte_iter``
Defined in [4], section 7.2.14.
:return: The decoded message type, or '<unknown>'
:rtype: str
"""
message_types = {
0x80: 'm-send-req',
0x81: 'm-send-conf',
0x82: 'm-notification-ind',
0x83: 'm-notifyresp-ind',
0x84: 'm-retrieve-conf',
0x85: 'm-acknowledge-ind',
0x86: 'm-delivery-ind',
}
byte = byte_iter.preview()
if byte in message_types:
byte_iter.next()
return message_types[byte]
byte_iter.reset_preview()
return '<unknown>'
@staticmethod
def decode_priority_value(byte_iter):
"""
Decode the "Priority" value pointed by ``byte_iter``
Defined in [4], section 7.2.17
:raise wsp_pdu.DecodeError: The priority value could not be decoded;
``byte_iter`` is not modified in this case.
:return: The decoded priority value
:rtype: str
"""
priorities = {128: 'Low', 129: 'Normal', 130: 'High'}
byte = byte_iter.preview()
if byte in priorities:
byte = byte_iter.next()
return priorities[byte]
byte_iter.reset_preview()
raise wsp_pdu.DecodeError('Error parsing Priority value '
'for byte: %s' % byte)
@staticmethod
def decode_sender_visibility_value(byte_iter):
"""
Decodes the sender visibility value pointed by ``byte_iter``
Defined in [4], section 7.2.22::
Sender-visibility-value = Hide | Show
Hide = <Octet 128>
Show = <Octet 129>
:raise wsp_pdu.DecodeError: The sender visibility value could not be
parsed. ``byte_iter`` will not be modified
in this case.
:return: The sender visibility: 'Hide' or 'Show'
:rtype: str
"""
byte = byte_iter.preview()
if byte not in (128, 129):
byte_iter.reset_preview()
raise wsp_pdu.DecodeError('Error parsing sender visibility '
'value for byte: %s' % hex(byte))
byte = byte_iter.next()
value = 'Hide' if byte == 128 else 'Show'
return value
@staticmethod
def decode_response_status_value(byte_iter):
"""
Decodes the "Response Status" value pointed by ``byte_iter``
Defined in [4], section 7.2.20
:raise wsp_pdu.DecodeError: The sender visibility value could not be
parsed. ``byte_iter`` will not be modified in
this case.
:return: The decoded Response-status-value
:rtype: str
"""
response_status_values = {
0x80: 'Ok',
0x81: 'Error-unspecified',
0x82: 'Error-service-denied',
0x83: 'Error-message-format-corrupt',
0x84: 'Error-sending-address-unresolved',
0x85: 'Error-message-not-found',
0x86: 'Error-network-problem',
0x87: 'Error-content-not-accepted',
0x88: 'Error-unsupported-message',
}
byte = byte_iter.preview()
byte_iter.next()
# Return error unspecified if it couldn't be decoded
return response_status_values.get(byte, 0x81)
@staticmethod
def decode_status_value(byte_iter):
"""
Used to decode the "Status" MMS header.
Defined in [4], section 7.2.23
:raise wsp_pdu.DecodeError: The sender visibility value could not be
parsed. ``byte_iter`` will not be
modified in this case.
:return: The decoded Status-value
:rtype: str
"""
status_values = {
0x80: 'Expired',
0x81: 'Retrieved',
0x82: 'Rejected',
0x83: 'Deferred',
0x84: 'Unrecognised',
}
byte = byte_iter.next()
# Return an unrecognised state if it couldn't be decoded
return status_values.get(byte, 0x84)
@staticmethod
def decode_expiry_value(byte_iter):
"""
Used to decode the "Expiry" MMS header.
From [4], section 7.2.10::
Expiry-value = Value-length (Absolute-token Date-value | Relative-token Delta-seconds-value)
Absolute-token = <Octet 128>
Relative-token = <Octet 129>
:raise wsp_pdu.DecodeError: The Expiry-value could not be decoded
:return: The decoded Expiry-value, either as a date, or as a delta-seconds value
:rtype: str or int
"""
value_length = MMSDecoder.decode_value_length(byte_iter)
token = byte_iter.next()
if token == 0x80: # Absolute-token
return MMSDecoder.decode_date_value(byte_iter)
elif token == 0x81: # Relative-token
return MMSDecoder.decode_delta_seconds_value(byte_iter)
raise wsp_pdu.DecodeError('Unrecognized token value: %s' % hex(token))
class MMSEncoder(wsp_pdu.Encoder):
"""MMS Encoder"""
def __init__(self):
self._mms_message = message.MMSMessage()
def encode(self, mms_message):
"""
Encodes the specified MMS message ``mms_message``
:param mms_message: The MMS message to encode
:type mms_message: MMSMessage
:return: The binary-encoded MMS data, as a sequence of bytes
:rtype: array.array('B')
"""
self._mms_message = mms_message
msg_data = self.encode_message_header()
msg_data.extend(self.encode_message_body())
return msg_data
def encode_message_header(self):
"""
Binary-encodes the MMS header data.
The encoding used for the MMS header is specified in [4].
All "constant" encoded values found/used in this method
are also defined in [4]. For a good example, see [2].
:return: the MMS PDU header, as an array of bytes
:rtype: array.array('B')
"""
# See [4], chapter 8 for info on how to use these
# from_types = {'Address-present-token': 0x80,
# 'Insert-address-token': 0x81}
# content_types = {'application/vnd.wap.multipart.related': 0xb3}
# Create an array of 8-bit values
message_header = array.array('B')
headers_to_encode = self._mms_message.headers
# If the user added any of these to the message manually
# (X- prefix) use those instead
for hdr in ('X-Mms-Message-Type', 'X-Mms-Transaction-Id',
'X-Mms-Version'):
if hdr in headers_to_encode:
if hdr == 'X-Mms-Version':
clean_header = 'MMS-Version'
else:
clean_header = hdr.replace('X-Mms-', '', 1)
headers_to_encode[clean_header] = headers_to_encode[hdr]
del headers_to_encode[hdr]
# First 3 headers (in order), according to [4]:
################################################
# - X-Mms-Message-Type
# - X-Mms-Transaction-ID
# - X-Mms-Version
### Start of Message-Type verification
if 'Message-Type' not in headers_to_encode:
# Default to 'm-retrieve-conf'; we don't need a To/CC field for
# this (see WAP-209, section 6.3, table 5)
headers_to_encode['Message-Type'] = 'm-retrieve-conf'
# See if the chosen message type is valid, given the message's
# other headers. NOTE: we only distinguish between 'm-send-req'
# (requires a destination number) and 'm-retrieve-conf'
# (requires no destination number) - if "Message-Type" is
# something else, we assume the message creator knows
# what she is doing
if headers_to_encode['Message-Type'] == 'm-send-req':
found_dest_address = False
for address_type in ('To', 'Cc', 'Bc'):
if address_type in headers_to_encode:
found_dest_address = True
break
if not found_dest_address:
headers_to_encode['Message-Type'] = 'm-retrieve-conf'
### End of Message-Type verification
### Start of Transaction-Id verification
if 'Transaction-Id' not in headers_to_encode:
trans_id = str(random.randint(1000, 9999))
headers_to_encode['Transaction-Id'] = trans_id
### End of Transaction-Id verification
### Start of MMS-Version verification
if 'MMS-Version' not in headers_to_encode:
headers_to_encode['MMS-Version'] = '1.0'
# Encode the first three headers, in correct order
for hdr in ('Message-Type', 'Transaction-Id', 'MMS-Version'):
message_header.extend(
MMSEncoder.encode_header(hdr, headers_to_encode[hdr]))
del headers_to_encode[hdr]
# Encode all remaining MMS message headers, except "Content-Type"
# -- this needs to be added last, according [2] and [4]
for hdr in headers_to_encode:
if hdr != 'Content-Type':
message_header.extend(
MMSEncoder.encode_header(hdr, headers_to_encode[hdr]))
# Ok, now only "Content-type" should be left
content_type, ct_parameters = headers_to_encode['Content-Type']
message_header.extend(MMSEncoder.encode_mms_field_name('Content-Type'))
ret = MMSEncoder.encode_content_type_value(content_type, ct_parameters)
message_header.extend(flatten_list(ret))
return message_header
def encode_message_body(self):
"""
Binary-encodes the MMS body data
The MMS body's header should not be confused with the actual
MMS header, as returned by :func:`encode_header`.
The encoding used for the MMS body is specified in [5],
section 8.5. It is only referenced in [4], however [2]
provides a good example of how this ties in with the MMS
header encoding.
The MMS body is of type `application/vnd.wap.multipart` ``mixed``
or ``related``. As such, its structure is divided into a header, and
the data entries/parts::
[ header ][ entries ]
^^^^^^^^^^^^^^^^^^^^^
MMS Body
The MMS Body header consists of one entry[5]::
name type purpose
------- ------- -----------
num_entries uint_var num of entries in the multipart entity
The MMS body's multipart entries structure::
name type purpose
------- ----- -----------
HeadersLen uint_var length of the ContentType and
Headers fields combined
DataLen uint_var length of the Data field
ContentType Multiple octets the content type of the data
Headers (<HeadersLen>
- length of
<ContentType>) octets the part's headers
Data <DataLen> octets the part's data
:return: The binary-encoded MMS PDU body, as an array of bytes
:rtype: array.array('B')
"""
message_body = array.array('B')
#TODO: enable encoding of MMSs without SMIL file
########## MMS body: header ##########
# Parts: SMIL file + <number of data elements in each slide>
num_entries = 1
for page in self._mms_message._pages:
num_entries += page.number_of_parts()
for data_part in self._mms_message._data_parts:
num_entries += 1
message_body.extend(self.encode_uint_var(num_entries))
########## MMS body: entries ##########
# For every data "part", we have to add the following sequence:
# <length of content-type + other possible headers>,
# <length of data>,
# <content-type + other possible headers>,
# <data>.
# Gather the data parts, adding the MMS message's SMIL file
smil_part = message.DataPart()
smil = self._mms_message.smil()
smil_part.set_data(smil, 'application/smil')
#TODO: make this dynamic....
smil_part.headers['Content-ID'] = '<0000>'
parts = [smil_part]
for slide in self._mms_message._pages:
for part_tuple in (slide.image, slide.audio, slide.text):
if part_tuple is not None:
parts.append(part_tuple[0])
for part in parts:
name, val_type = part.headers['Content-Type']
part_content_type = self.encode_content_type_value(name, val_type)
encoded_part_headers = []
for hdr in part.headers:
if hdr == 'Content-Type':
continue
encoded_part_headers.extend(
wsp_pdu.Encoder.encode_header(hdr, part.headers[hdr]))
# HeadersLen entry (length of the ContentType and
# Headers fields combined)
headers_len = len(part_content_type) + len(encoded_part_headers)
message_body.extend(self.encode_uint_var(headers_len))
# DataLen entry (length of the Data field)
message_body.extend(self.encode_uint_var(len(part)))
# ContentType entry
message_body.extend(part_content_type)
# Headers
message_body.extend(encoded_part_headers)
# Data (note: we do not null-terminate this)
for char in part.data:
message_body.append(ord(char))
return message_body
@staticmethod
def encode_header(header_field_name, header_value):
"""
Encodes a header entry for an MMS message
The return type of the "header value" depends on the header
itself; it is thus up to the function calling this to determine
what that type is (or at least compensate for possibly different
return value types).
From [4], section 7.1::
Header = MMS-header | Application-header
MMS-header = MMS-field-name MMS-value
MMS-field-name = Short-integer
MMS-value = Bcc-value | Cc-value | Content-location-value | Content-type-value | etc
:raise DecodeError: This uses :func:`decode_mms_header` and
:func:`decode_application_header`, and will raise this
exception under the same circumstances as
:func:`decode_application_header`. ``byte_iter`` will
not be modified in this case.
:return: The decoded header entry from the MMS, in the format:
(<str:header name>, <str/int/float:header value>)
:rtype: tuple
"""
encoded_header = []
# First try encoding the header as a "MMS-header"...
for assigned_number in mms_field_names:
header = mms_field_names[assigned_number][0]
if header == header_field_name:
encoded_header.extend(
wsp_pdu.Encoder.encode_short_integer(assigned_number))
# Now encode the value
expected_type = mms_field_names[assigned_number][1]
try:
ret = getattr(MMSEncoder,
'encode_%s' % expected_type)(header_value)
encoded_header.extend(ret)
except wsp_pdu.EncodeError, msg:
raise wsp_pdu.EncodeError('Error encoding parameter '
'value: %s' % msg)
except:
debug('A fatal error occurred, probably due to an '
'unimplemented encoding operation')
raise
break
# See if the "MMS-header" encoding worked
if not len(encoded_header):
# ...it didn't. Use "Application-header" encoding
header_name = wsp_pdu.Encoder.encode_token_text(header_field_name)
encoded_header.extend(header_name)
# Now add the value
encoded_header.extend(
wsp_pdu.Encoder.encode_text_string(header_value))
return encoded_header
@staticmethod
def encode_mms_field_name(field_name):
"""
Encodes an MMS header field name
From [4], section 7.1::
MMS-field-name = Short-integer
:raise EncodeError: The specified header field name is not a
well-known MMS header.
:param field_name: The header field name to encode
:type field_name: str
:return: The encoded header field name, as a sequence of bytes
:rtype: list
"""
encoded_mms_field_name = []
for assigned_number in mms_field_names:
if mms_field_names[assigned_number][0] == field_name:
encoded_mms_field_name.extend(
wsp_pdu.Encoder.encode_short_integer(assigned_number))
break
if not len(encoded_mms_field_name):
raise wsp_pdu.EncodeError('The specified header field name is not '
'a well-known MMS header field name')
return encoded_mms_field_name
@staticmethod
def encode_from_value(from_value=''):
"""
Encodes the "From" address value
From [4], section 7.2.11::
From-value = Value-length (Address-present-token Encoded-string-value | Insert-address-token )
Address-present-token = <Octet 128>
Insert-address-token = <Octet 129>
:param from_value: The "originator" of the MMS message. This may be an
empty string, in which case a token will be encoded
informing the MMSC to insert the address of the
device that sent this message (default).
:type from_value: str
:return: The encoded "From" address value, as a sequence of bytes
:rtype: list
"""
encoded_from_value = []
if len(from_value) == 0:
value_length = wsp_pdu.Encoder.encode_value_length(1)
encoded_from_value.extend(value_length)
encoded_from_value.append(129) # Insert-address-token
else:
encoded_address = MMSEncoder.encode_encoded_string_value(from_value)
# the "+1" is for the Address-present-token
length = len(encoded_address) + 1
value_length = wsp_pdu.Encoder.encode_value_length(length)
encoded_from_value.extend(value_length)
encoded_from_value.append(128) # Address-present-token
encoded_from_value.extend(encoded_address)
return encoded_from_value
@staticmethod
def encode_encoded_string_value(string_value):
"""
Encodes ``string_value``
This is a simple wrapper to :func:`encode_text_string`
From [4], section 7.2.9::
Encoded-string-value = Text-string | Value-length Char-set Text-string
The Char-set values are registered by IANA as MIBEnum value.
:param string_value: The text string to encode
:type string_value: str
:return: The encoded string value, as a sequence of bytes
:rtype: list
"""
return wsp_pdu.Encoder.encode_text_string(string_value)
@staticmethod
def encode_message_type_value(message_type):
"""
Encodes the Message-Type value ``message_type``
Unknown message types are discarded; thus they will be encoded
as 0x80 ("m-send-req") by this function
Defined in [4], section 7.2.14.
:param message_type: The MMS message type to encode
:type message_type: str
:return: The encoded message type, as a sequence of bytes
:rtype: list
"""
message_types = {
'm-send-req': 0x80,
'm-send-conf': 0x81,
'm-notification-ind': 0x82,
'm-notifyresp-ind': 0x83,
'm-retrieve-conf': 0x84,
'm-acknowledge-ind': 0x85,
'm-delivery-ind': 0x86,
}
return [message_types.get(message_type, 0x80)]
@staticmethod
def encode_status_value(status_value):
status_values = {
'Expired': 0x80,
'Retrieved': 0x81,
'Rejected': 0x82,
'Deferred': 0x83,
'Unrecognised': 0x84,
}
# Return an unrecognised state if it couldn't be decoded
return [status_values.get(status_value, 'Unrecognised')]

2055
messaging/mms/wsp_pdu.py
File diff suppressed because it is too large
View File

7
messaging/sms/__init__.py

@ -0,0 +1,7 @@
# See LICENSE
from messaging.sms.submit import SmsSubmit
from messaging.sms.deliver import SmsDeliver
from messaging.sms.gsm0338 import is_gsm_text
__all__ = ["SmsSubmit", "SmsDeliver", "is_gsm_text"]

14
messaging/sms/base.py

@ -0,0 +1,14 @@
# see LICENSE
class SmsBase(object):
def __init__(self):
self.udh = None
self.number = None
self.text = None
self.fmt = None
self.dcs = None
self.pid = None
self.csca = None
self.type = None

17
messaging/sms/consts.py

@ -0,0 +1,17 @@
# see LICENSE
SEVENBIT_SIZE = 160
EIGHTBIT_SIZE = 140
UCS2_SIZE = 70
SEVENBIT_MP_SIZE = SEVENBIT_SIZE - 7
EIGHTBIT_MP_SIZE = EIGHTBIT_SIZE - 6
UCS2_MP_SIZE = UCS2_SIZE - 3
# address type
UNKNOWN = 0
INTERNATIONAL = 1
NATIONAL = 2
NETWORK_SPECIFIC = 3
SUBSCRIBER = 4
ALPHANUMERIC = 5
ABBREVIATED = 6
RESERVED = 7

264
messaging/sms/deliver.py

@ -0,0 +1,264 @@
# see LICENSE
"""Classes for processing received SMS"""
from datetime import datetime, timedelta
from messaging.utils import (swap, swap_number, encode_bytes, debug,
unpack_msg, unpack_msg2, to_array)
from messaging.sms import consts
from messaging.sms.base import SmsBase
from messaging.sms.udh import UserDataHeader
class SmsDeliver(SmsBase):
"""I am a delivered SMS in your Inbox"""
def __init__(self, pdu, strict=True):
super(SmsDeliver, self).__init__()
self._pdu = None
self._strict = strict
self.date = None
self.mtype = None
self.sr = None
self.pdu = pdu
@property
def data(self):
"""
Returns a dict populated with the SMS attributes
It mimics the old API to ease the port to the new API
"""
ret = {
'text': self.text,
'pid': self.pid,
'dcs': self.dcs,
'csca': self.csca,
'number': self.number,
'type': self.type,
'date': self.date,
'fmt': self.fmt,
'sr': self.sr,
}
if self.udh is not None:
if self.udh.concat is not None:
ret.update({
'ref': self.udh.concat.ref,
'cnt': self.udh.concat.cnt,
'seq': self.udh.concat.seq,
})
return ret
def _set_pdu(self, pdu):
if not self._strict and len(pdu) % 2:
# if not strict and PDU-length is odd, remove the last character
# and make it even. See the discussion of this bug at
# http://github.com/pmarti/python-messaging/issues#issue/7
pdu = pdu[:-1]
if len(pdu) % 2:
raise ValueError("Can not decode an odd-length pdu")
# XXX: Should we keep the original PDU or the modified one?
self._pdu = pdu
data = to_array(self._pdu)
# Service centre address
smscl = data.pop(0)
if smscl > 0:
smscertype = data.pop(0)
smscl -= 1
self.csca = swap_number(encode_bytes(data[:smscl]))
if (smscertype >> 4) & 0x07 == consts.INTERNATIONAL:
self.csca = '+%s' % self.csca
data = data[smscl:]
else:
self.csca = None
# 1 byte(octet) == 2 char
# Message type TP-MTI bits 0,1
# More messages to send/deliver bit 2
# Status report request indicated bit 5
# User Data Header Indicator bit 6
# Reply path set bit 7
try:
self.mtype = data.pop(0)
except TypeError:
raise ValueError("Decoding this type of SMS is not supported yet")
mtype = self.mtype & 0x03
if mtype == 0x02:
return self._decode_status_report_pdu(data)
if mtype == 0x01:
raise ValueError("Cannot decode a SmsSubmitReport message yet")
sndlen = data.pop(0)
if sndlen % 2:
sndlen += 1
sndlen = int(sndlen / 2.0)
sndtype = (data.pop(0) >> 4) & 0x07
if sndtype == consts.ALPHANUMERIC:
# coded according to 3GPP TS 23.038 [9] GSM 7-bit default alphabet
sender = unpack_msg2(data[:sndlen]).decode("gsm0338")
else:
# Extract phone number of sender
sender = swap_number(encode_bytes(data[:sndlen]))
if sndtype == consts.INTERNATIONAL:
sender = '+%s' % sender
self.number = sender
data = data[sndlen:]
# 1 byte TP-PID (Protocol IDentifier)
self.pid = data.pop(0)
# 1 byte TP-DCS (Data Coding Scheme)
self.dcs = data.pop(0)
if self.dcs & (0x04 | 0x08) == 0:
self.fmt = 0x00
elif self.dcs & 0x04:
self.fmt = 0x04
elif self.dcs & 0x08:
self.fmt = 0x08
datestr = ''
# Get date stamp (sender's local time)
date = list(encode_bytes(data[:6]))
for n in range(1, len(date), 2):
date[n - 1], date[n] = date[n], date[n - 1]
data = data[6:]
# Get sender's offset from GMT (TS 23.040 TP-SCTS)
tz = data.pop(0)
offset = ((tz & 0x07) * 10 + ((tz & 0xf0) >> 4)) * 15
if (tz & 0x08):
offset = offset * -1
# 02/08/26 19:37:41
datestr = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date)
outputfmt = '%y/%m/%d %H:%M:%S'
sndlocaltime = datetime.strptime(datestr, outputfmt)
sndoffset = timedelta(minutes=offset)
# date as UTC
self.date = sndlocaltime - sndoffset
self._process_message(data)
def _process_message(self, data):
# Now get message body
msgl = data.pop(0)
msg = encode_bytes(data[:msgl])
# check for header
headlen = ud_len = 0
if self.mtype & 0x40: # UDHI present
ud_len = data.pop(0)
self.udh = UserDataHeader.from_bytes(data[:ud_len])
headlen = (ud_len + 1) * 8
if self.fmt == 0x00:
while headlen % 7:
headlen += 1
headlen /= 7
headlen = int(headlen)
if self.fmt == 0x00:
# XXX: Use unpack_msg2
data = data[ud_len:].tolist()
#self.text = unpack_msg2(data).decode("gsm0338")
self.text = unpack_msg(msg)[headlen:msgl].decode("gsm0338")
elif self.fmt == 0x04:
self.text = data[ud_len:].tostring()
elif self.fmt == 0x08:
data = data[ud_len:].tolist()
_bytes = [int("%02X%02X" % (data[i], data[i + 1]), 16)
for i in range(0, len(data), 2)]
self.text = ''.join(list(map(chr, _bytes)))
pdu = property(lambda self: self._pdu, _set_pdu)
def _decode_status_report_pdu(self, data):
self.udh = UserDataHeader.from_status_report_ref(data.pop(0))
sndlen = data.pop(0)
if sndlen % 2:
sndlen += 1
sndlen = int(sndlen / 2.0)
sndtype = data.pop(0)
recipient = swap_number(encode_bytes(data[:sndlen]))
if (sndtype >> 4) & 0x07 == consts.INTERNATIONAL:
recipient = '+%s' % recipient
data = data[sndlen:]
date = swap(list(encode_bytes(data[:7])))
try:
scts_str = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date[0:12])
self.date = datetime.strptime(scts_str, "%y/%m/%d %H:%M:%S")
except (ValueError, TypeError):
scts_str = ''
debug('Could not decode scts: %s' % date)
data = data[7:]
date = swap(list(encode_bytes(data[:7])))
try:
dt_str = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date[0:12])
dt = datetime.strptime(dt_str, "%y/%m/%d %H:%M:%S")
except (ValueError, TypeError):
dt_str = ''
dt = None
debug('Could not decode date: %s' % date)
data = data[7:]
msg_l = [recipient, scts_str]
try:
status = data.pop(0)
except IndexError:
# Yes it is entirely possible that a status report comes
# with no status at all! I'm faking for now the values and
# set it to SR-UNKNOWN as that's all we can do
_status = None
status = 0x1
sender = 'SR-UNKNOWN'
msg_l.append(dt_str)
else:
_status = status
if status == 0x00:
msg_l.append(dt_str)
else:
msg_l.append('')
if status == 0x00:
sender = "SR-OK"
elif status == 0x1:
sender = "SR-UNKNOWN"
elif status == 0x30:
sender = "SR-STORED"
else:
sender = "SR-UNKNOWN"
self.number = sender
self.text = "|".join(msg_l)
self.fmt = 0x08 # UCS2
self.type = 0x03 # status report
self.sr = {
'recipient': recipient,
'scts': self.date,
'dt': dt,
'status': _status
}

292
messaging/sms/gsm0338.py

@ -0,0 +1,292 @@
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import codecs
import sys
import traceback
# data from
# http://snoops.roy202.org/testerman/browser/trunk/plugins/codecs/gsm0338.py
# default GSM 03.38 -> unicode
def_regular_decode_dict = {
'\x00': u'\u0040', # COMMERCIAL AT
'\x01': u'\u00A3', # POUND SIGN
'\x02': u'\u0024', # DOLLAR SIGN
'\x03': u'\u00A5', # YEN SIGN
'\x04': u'\u00E8', # LATIN SMALL LETTER E WITH GRAVE
'\x05': u'\u00E9', # LATIN SMALL LETTER E WITH ACUTE
'\x06': u'\u00F9', # LATIN SMALL LETTER U WITH GRAVE
'\x07': u'\u00EC', # LATIN SMALL LETTER I WITH GRAVE
'\x08': u'\u00F2', # LATIN SMALL LETTER O WITH GRAVE
'\x09': u'\u00C7', # LATIN CAPITAL LETTER C WITH CEDILLA
# The Unicode page suggests this is a mistake: but
# it's still in the latest version of the spec and
# our implementation has to be exact.
'\x0A': u'\u000A', # LINE FEED
'\x0B': u'\u00D8', # LATIN CAPITAL LETTER O WITH STROKE
'\x0C': u'\u00F8', # LATIN SMALL LETTER O WITH STROKE
'\x0D': u'\u000D', # CARRIAGE RETURN
'\x0E': u'\u00C5', # LATIN CAPITAL LETTER A WITH RING ABOVE
'\x0F': u'\u00E5', # LATIN SMALL LETTER A WITH RING ABOVE
'\x10': u'\u0394', # GREEK CAPITAL LETTER DELTA
'\x11': u'\u005F', # LOW LINE
'\x12': u'\u03A6', # GREEK CAPITAL LETTER PHI
'\x13': u'\u0393', # GREEK CAPITAL LETTER GAMMA
'\x14': u'\u039B', # GREEK CAPITAL LETTER LAMDA
'\x15': u'\u03A9', # GREEK CAPITAL LETTER OMEGA
'\x16': u'\u03A0', # GREEK CAPITAL LETTER PI
'\x17': u'\u03A8', # GREEK CAPITAL LETTER PSI
'\x18': u'\u03A3', # GREEK CAPITAL LETTER SIGMA
'\x19': u'\u0398', # GREEK CAPITAL LETTER THETA
'\x1A': u'\u039E', # GREEK CAPITAL LETTER XI
'\x1C': u'\u00C6', # LATIN CAPITAL LETTER AE
'\x1D': u'\u00E6', # LATIN SMALL LETTER AE
'\x1E': u'\u00DF', # LATIN SMALL LETTER SHARP S (German)
'\x1F': u'\u00C9', # LATIN CAPITAL LETTER E WITH ACUTE
'\x20': u'\u0020', # SPACE
'\x21': u'\u0021', # EXCLAMATION MARK
'\x22': u'\u0022', # QUOTATION MARK
'\x23': u'\u0023', # NUMBER SIGN
'\x24': u'\u00A4', # CURRENCY SIGN
'\x25': u'\u0025', # PERCENT SIGN
'\x26': u'\u0026', # AMPERSAND
'\x27': u'\u0027', # APOSTROPHE
'\x28': u'\u0028', # LEFT PARENTHESIS
'\x29': u'\u0029', # RIGHT PARENTHESIS
'\x2A': u'\u002A', # ASTERISK
'\x2B': u'\u002B', # PLUS SIGN
'\x2C': u'\u002C', # COMMA
'\x2D': u'\u002D', # HYPHEN-MINUS
'\x2E': u'\u002E', # FULL STOP
'\x2F': u'\u002F', # SOLIDUS
'\x30': u'\u0030', # DIGIT ZERO
'\x31': u'\u0031', # DIGIT ONE
'\x32': u'\u0032', # DIGIT TWO
'\x33': u'\u0033', # DIGIT THREE
'\x34': u'\u0034', # DIGIT FOUR
'\x35': u'\u0035', # DIGIT FIVE
'\x36': u'\u0036', # DIGIT SIX
'\x37': u'\u0037', # DIGIT SEVEN
'\x38': u'\u0038', # DIGIT EIGHT
'\x39': u'\u0039', # DIGIT NINE
'\x3A': u'\u003A', # COLON
'\x3B': u'\u003B', # SEMICOLON
'\x3C': u'\u003C', # LESS-THAN SIGN
'\x3D': u'\u003D', # EQUALS SIGN
'\x3E': u'\u003E', # GREATER-THAN SIGN
'\x3F': u'\u003F', # QUESTION MARK
'\x40': u'\u00A1', # INVERTED EXCLAMATION MARK
'\x41': u'\u0041', # LATIN CAPITAL LETTER A
'\x42': u'\u0042', # LATIN CAPITAL LETTER B
'\x43': u'\u0043', # LATIN CAPITAL LETTER C
'\x44': u'\u0044', # LATIN CAPITAL LETTER D
'\x45': u'\u0045', # LATIN CAPITAL LETTER E
'\x46': u'\u0046', # LATIN CAPITAL LETTER F
'\x47': u'\u0047', # LATIN CAPITAL LETTER G
'\x48': u'\u0048', # LATIN CAPITAL LETTER H
'\x49': u'\u0049', # LATIN CAPITAL LETTER I
'\x4A': u'\u004A', # LATIN CAPITAL LETTER J
'\x4B': u'\u004B', # LATIN CAPITAL LETTER K
'\x4C': u'\u004C', # LATIN CAPITAL LETTER L
'\x4D': u'\u004D', # LATIN CAPITAL LETTER M
'\x4E': u'\u004E', # LATIN CAPITAL LETTER N
'\x4F': u'\u004F', # LATIN CAPITAL LETTER O
'\x50': u'\u0050', # LATIN CAPITAL LETTER P
'\x51': u'\u0051', # LATIN CAPITAL LETTER Q
'\x52': u'\u0052', # LATIN CAPITAL LETTER R
'\x53': u'\u0053', # LATIN CAPITAL LETTER S
'\x54': u'\u0054', # LATIN CAPITAL LETTER T
'\x55': u'\u0055', # LATIN CAPITAL LETTER U
'\x56': u'\u0056', # LATIN CAPITAL LETTER V
'\x57': u'\u0057', # LATIN CAPITAL LETTER W
'\x58': u'\u0058', # LATIN CAPITAL LETTER X
'\x59': u'\u0059', # LATIN CAPITAL LETTER Y
'\x5A': u'\u005A', # LATIN CAPITAL LETTER Z
'\x5B': u'\u00C4', # LATIN CAPITAL LETTER A WITH DIAERESIS
'\x5C': u'\u00D6', # LATIN CAPITAL LETTER O WITH DIAERESIS
'\x5D': u'\u00D1', # LATIN CAPITAL LETTER N WITH TILDE
'\x5E': u'\u00DC', # LATIN CAPITAL LETTER U WITH DIAERESIS
'\x5F': u'\u00A7', # SECTION SIGN
'\x60': u'\u00BF', # INVERTED QUESTION MARK
'\x61': u'\u0061', # LATIN SMALL LETTER A
'\x62': u'\u0062', # LATIN SMALL LETTER B
'\x63': u'\u0063', # LATIN SMALL LETTER C
'\x64': u'\u0064', # LATIN SMALL LETTER D
'\x65': u'\u0065', # LATIN SMALL LETTER E
'\x66': u'\u0066', # LATIN SMALL LETTER F
'\x67': u'\u0067', # LATIN SMALL LETTER G
'\x68': u'\u0068', # LATIN SMALL LETTER H
'\x69': u'\u0069', # LATIN SMALL LETTER I
'\x6A': u'\u006A', # LATIN SMALL LETTER J
'\x6B': u'\u006B', # LATIN SMALL LETTER K
'\x6C': u'\u006C', # LATIN SMALL LETTER L
'\x6D': u'\u006D', # LATIN SMALL LETTER M
'\x6E': u'\u006E', # LATIN SMALL LETTER N
'\x6F': u'\u006F', # LATIN SMALL LETTER O
'\x70': u'\u0070', # LATIN SMALL LETTER P
'\x71': u'\u0071', # LATIN SMALL LETTER Q
'\x72': u'\u0072', # LATIN SMALL LETTER R
'\x73': u'\u0073', # LATIN SMALL LETTER S
'\x74': u'\u0074', # LATIN SMALL LETTER T
'\x75': u'\u0075', # LATIN SMALL LETTER U
'\x76': u'\u0076', # LATIN SMALL LETTER V
'\x77': u'\u0077', # LATIN SMALL LETTER W
'\x78': u'\u0078', # LATIN SMALL LETTER X
'\x79': u'\u0079', # LATIN SMALL LETTER Y
'\x7A': u'\u007A', # LATIN SMALL LETTER Z
'\x7B': u'\u00E4', # LATIN SMALL LETTER A WITH DIAERESIS
'\x7C': u'\u00F6', # LATIN SMALL LETTER O WITH DIAERESIS
'\x7D': u'\u00F1', # LATIN SMALL LETTER N WITH TILDE
'\x7E': u'\u00FC', # LATIN SMALL LETTER U WITH DIAERESIS
'\x7F': u'\u00E0', # LATIN SMALL LETTER A WITH GRAVE
}
# default GSM 03.38 escaped characters -> unicode
def_escape_decode_dict = {
'\x0A': u'\u000C', # FORM FEED
'\x14': u'\u005E', # CIRCUMFLEX ACCENT
'\x28': u'\u007B', # LEFT CURLY BRACKET
'\x29': u'\u007D', # RIGHT CURLY BRACKET
'\x2F': u'\u005C', # REVERSE SOLIDUS
'\x3C': u'\u005B', # LEFT SQUARE BRACKET
'\x3D': u'\u007E', # TILDE
'\x3E': u'\u005D', # RIGHT SQUARE BRACKET
'\x40': u'\u007C', # VERTICAL LINE
'\x65': u'\u20AC', # EURO SIGN
}
# Replacement characters, default is question mark. Used when it is not too
# important to ensure exact UTF-8 -> GSM -> UTF-8 equivilence, such as when
# humans read and write SMS. But for USSD and other M2M applications it's
# important to ensure the conversion is exact.
def_replace_encode_dict = {
u'\u00E7': '\x09', # LATIN SMALL LETTER C WITH CEDILLA
u'\u0391': '\x41', # GREEK CAPITAL LETTER ALPHA
u'\u0392': '\x42', # GREEK CAPITAL LETTER BETA
u'\u0395': '\x45', # GREEK CAPITAL LETTER EPSILON
u'\u0397': '\x48', # GREEK CAPITAL LETTER ETA
u'\u0399': '\x49', # GREEK CAPITAL LETTER IOTA
u'\u039A': '\x4B', # GREEK CAPITAL LETTER KAPPA
u'\u039C': '\x4D', # GREEK CAPITAL LETTER MU
u'\u039D': '\x4E', # GREEK CAPITAL LETTER NU
u'\u039F': '\x4F', # GREEK CAPITAL LETTER OMICRON
u'\u03A1': '\x50', # GREEK CAPITAL LETTER RHO
u'\u03A4': '\x54', # GREEK CAPITAL LETTER TAU
u'\u03A7': '\x58', # GREEK CAPITAL LETTER CHI
u'\u03A5': '\x59', # GREEK CAPITAL LETTER UPSILON
u'\u0396': '\x5A', # GREEK CAPITAL LETTER ZETA
}
QUESTION_MARK = chr(0x3f)
# unicode -> default GSM 03.38
def_regular_encode_dict = \
dict((u, g) for g, u in iter(def_regular_decode_dict.items()))
# unicode -> default escaped GSM 03.38 characters
def_escape_encode_dict = \
dict((u, g) for g, u in iter(def_escape_decode_dict.items()))
def encode(input_, errors='strict'):
"""
:type input_: unicode
:return: string
"""
result = []
for c in input_:
try:
result.append(def_regular_encode_dict[c])
except KeyError:
if c in def_escape_encode_dict:
# OK, let's encode it as an escaped characters
result.append('\x1b')
result.append(def_escape_encode_dict[c])
else:
if errors == 'strict':
raise UnicodeError("Invalid GSM character")
elif errors == 'replace':
result.append(
def_replace_encode_dict.get(c, QUESTION_MARK))
elif errors == 'ignore':
pass
else:
raise UnicodeError("Unknown error handling")
ret = ''.join(result)
return ret, len(ret)
def decode(input_, errors='strict'):
"""
:type input_: str
:return: unicode
"""
result = []
index = 0
while index < len(input_):
c = input_[index]
index += 1
if c == '\x1b':
if index < len(input_):
c = input_[index]
index += 1
result.append(def_escape_decode_dict.get(c, u'\xa0'))
else:
result.append(u'\xa0')
else:
try:
result.append(def_regular_decode_dict[c])
except KeyError:
# error handling: unassigned byte, must be > 0x7f
if errors == 'strict':
raise UnicodeError("Unrecognized GSM character")
elif errors == 'replace':
result.append('?')
elif errors == 'ignore':
pass
else:
raise UnicodeError("Unknown error handling")
ret = u''.join(result)
return ret, len(ret)
# encodings module API
def getregentry(encoding):
if encoding == 'gsm0338':
return codecs.CodecInfo(name='gsm0338',
encode=encode,
decode=decode)
# Codec registration
codecs.register(getregentry)
def is_gsm_text(text):
"""Returns True if ``text`` can be encoded as gsm text"""
try:
text.encode("gsm0338")
except UnicodeError:
return False
except:
traceback.print_exc(file=sys.stdout)
return False
return True

10
messaging/sms/pdu.py

@ -0,0 +1,10 @@
# see LICENSE
class Pdu(object):
def __init__(self, pdu, len_smsc, cnt=1, seq=1):
self.pdu = pdu.upper()
self.length = len(pdu) / 2 - len_smsc
self.cnt = cnt
self.seq = seq

330
messaging/sms/submit.py

@ -0,0 +1,330 @@
# See LICENSE
"""Classes for sending SMS"""
from datetime import datetime, timedelta
import re
from messaging.sms import consts
from messaging.utils import (debug, encode_str, clean_number,
pack_8bits_to_ucs2, pack_8bits_to_7bits,
pack_8bits_to_8bit,
timedelta_to_relative_validity,
datetime_to_absolute_validity)
from messaging.sms.base import SmsBase
from messaging.sms.gsm0338 import is_gsm_text
from messaging.sms.pdu import Pdu
VALID_NUMBER = re.compile("^\+?\d{3,20}$")
class SmsSubmit(SmsBase):
"""I am a SMS ready to be sent"""
def __init__(self, number, text):
super(SmsSubmit, self).__init__()
self._number = None
self._csca = None
self._klass = None
self._validity = None
self.request_status = False
self.ref = None
self.rand_id = None
self.id_list = range(0, 255)
self.msgvp = 0xaa
self.pid = 0x00
self.number = number
self.text = text
self.text_gsm = None
def _set_number(self, number):
if number and not VALID_NUMBER.match(number):
raise ValueError("Invalid number format: %s" % number)
self._number = number
number = property(lambda self: self._number, _set_number)
def _set_csca(self, csca):
if csca and not VALID_NUMBER.match(csca):
raise ValueError("Invalid csca format: %s" % csca)
self._csca = csca
csca = property(lambda self: self._csca, _set_csca)
def _set_validity(self, validity):
if validity is None or isinstance(validity, (timedelta, datetime)):
# valid values are None, timedelta and datetime
self._validity = validity
else:
raise TypeError("Don't know what to do with %s" % validity)
validity = property(lambda self: self._validity, _set_validity)
def _set_klass(self, klass):
if not isinstance(klass, int):
raise TypeError("_set_klass only accepts int objects")
if klass not in [0, 1, 2, 3]:
raise ValueError("class must be between 0 and 3")
self._klass = klass
klass = property(lambda self: self._klass, _set_klass)
def to_pdu(self):
"""Returns a list of :class:`~messaging.pdu.Pdu` objects"""
smsc_pdu = self._get_smsc_pdu()
sms_submit_pdu = self._get_sms_submit_pdu()
tpmessref_pdu = self._get_tpmessref_pdu()
sms_phone_pdu = self._get_phone_pdu()
tppid_pdu = self._get_tppid_pdu()
sms_msg_pdu = self._get_msg_pdu()
if len(sms_msg_pdu) == 1:
pdu = smsc_pdu
len_smsc = len(smsc_pdu) / 2
pdu += sms_submit_pdu
pdu += tpmessref_pdu
pdu += sms_phone_pdu
pdu += tppid_pdu
pdu += sms_msg_pdu[0]
debug("smsc_pdu: %s" % smsc_pdu)
debug("sms_submit_pdu: %s" % sms_submit_pdu)
debug("tpmessref_pdu: %s" % tpmessref_pdu)
debug("sms_phone_pdu: %s" % sms_phone_pdu)
debug("tppid_pdu: %s" % tppid_pdu)
debug("sms_msg_pdu: %s" % sms_msg_pdu)
debug("-" * 20)
debug("full_pdu: %s" % pdu)
debug("full_text: %s" % self.text)
debug("-" * 20)
return [Pdu(pdu, len_smsc)]
# multipart SMS
sms_submit_pdu = self._get_sms_submit_pdu(udh=True)
pdu_list = []
cnt = len(sms_msg_pdu)
for i, sms_msg_pdu_item in enumerate(sms_msg_pdu):
pdu = smsc_pdu
len_smsc = len(smsc_pdu) / 2
pdu += sms_submit_pdu
pdu += tpmessref_pdu
pdu += sms_phone_pdu
pdu += tppid_pdu
pdu += sms_msg_pdu_item
debug("smsc_pdu: %s" % smsc_pdu)
debug("sms_submit_pdu: %s" % sms_submit_pdu)
debug("tpmessref_pdu: %s" % tpmessref_pdu)
debug("sms_phone_pdu: %s" % sms_phone_pdu)
debug("tppid_pdu: %s" % tppid_pdu)
debug("sms_msg_pdu: %s" % sms_msg_pdu_item)
debug("-" * 20)
debug("full_pdu: %s" % pdu)
debug("full_text: %s" % self.text)
debug("-" * 20)
pdu_list.append(Pdu(pdu, len_smsc, cnt=cnt, seq=i + 1))
return pdu_list
def _get_smsc_pdu(self):
if not self.csca or not self.csca.strip():
return "00"
number = clean_number(self.csca)
ptype = 0x81 # set to unknown number by default
if number[0] == '+':
number = number[1:]
ptype = 0x91
if len(number) % 2:
number += 'F'
ps = chr(ptype)
for n in range(0, len(number), 2):
num = number[n + 1] + number[n]
ps += chr(int(num, 16))
pl = len(ps)
ps = chr(pl) + ps
return encode_str(ps)
def _get_tpmessref_pdu(self):
if self.ref is None:
self.ref = self._get_rand_id()
self.ref &= 0xFF
return encode_str(chr(self.ref))
def _get_phone_pdu(self):
number = clean_number(self.number)
ptype = 0x81
if number[0] == '+':
number = number[1:]
ptype = 0x91
pl = len(number)
if len(number) % 2:
number += 'F'
ps = chr(ptype)
for n in range(0, len(number), 2):
num = number[n + 1] + number[n]
ps += chr(int(num, 16))
ps = chr(pl) + ps
return encode_str(ps)
def _get_tppid_pdu(self):
return encode_str(chr(self.pid))
def _get_sms_submit_pdu(self, udh=False):
sms_submit = 0x1
if self.validity is None:
# handle no validity
pass
elif isinstance(self.validity, datetime):
# handle absolute validity
sms_submit |= 0x18
elif isinstance(self.validity, timedelta):
# handle relative validity
sms_submit |= 0x10
if self.request_status:
sms_submit |= 0x20
if udh:
sms_submit |= 0x40
return encode_str(chr(sms_submit))
def _get_msg_pdu(self):
# Data coding scheme
if self.fmt is None:
if is_gsm_text(self.text):
self.fmt = 0x00
else:
self.fmt = 0x08
self.dcs = self.fmt
if self.klass is not None:
if self.klass == 0:
self.dcs |= 0x10
elif self.klass == 1:
self.dcs |= 0x11
elif self.klass == 2:
self.dcs |= 0x12
elif self.klass == 3:
self.dcs |= 0x13
dcs_pdu = encode_str(chr(self.dcs))
# Validity period
msgvp_pdu = ""
if self.validity is None:
# handle no validity
pass
elif isinstance(self.validity, timedelta):
# handle relative
msgvp = timedelta_to_relative_validity(self.validity)
msgvp_pdu = encode_str(chr(msgvp))
elif isinstance(self.validity, datetime):
# handle absolute
msgvp = datetime_to_absolute_validity(self.validity)
msgvp_pdu = ''.join(map(encode_str, map(chr, msgvp)))
# UDL + UD
message_pdu = ""
if self.fmt == 0x00:
self.text_gsm = self.text.encode("gsm0338")
if len(self.text_gsm) <= consts.SEVENBIT_SIZE:
message_pdu = [pack_8bits_to_7bits(self.text_gsm)]
else:
message_pdu = self._split_sms_message(self.text_gsm)
elif self.fmt == 0x04:
if len(self.text) <= consts.EIGHTBIT_SIZE:
message_pdu = [pack_8bits_to_8bit(self.text)]
else:
message_pdu = self._split_sms_message(self.text)
elif self.fmt == 0x08:
if len(self.text) <= consts.UCS2_SIZE:
message_pdu = [pack_8bits_to_ucs2(self.text)]
else:
message_pdu = self._split_sms_message(self.text)
else:
raise ValueError("Unknown data coding scheme: %d" % self.fmt)
ret = []
for msg in message_pdu:
ret.append(dcs_pdu + msgvp_pdu + msg)
return ret
def _split_sms_message(self, text):
if self.fmt == 0x00:
len_without_udh = consts.SEVENBIT_MP_SIZE
limit = consts.SEVENBIT_SIZE
packing_func = pack_8bits_to_7bits
total_len = len(self.text_gsm)
elif self.fmt == 0x04:
len_without_udh = consts.EIGHTBIT_MP_SIZE
limit = consts.EIGHTBIT_SIZE
packing_func = pack_8bits_to_8bit
total_len = len(self.text)
elif self.fmt == 0x08:
len_without_udh = consts.UCS2_MP_SIZE
limit = consts.UCS2_SIZE
packing_func = pack_8bits_to_ucs2
total_len = len(self.text)
msgs = []
pi, pe = 0, len_without_udh
while pi < total_len:
if text[pi:pe][-1] == '\x1b':
pe -= 1
msgs.append(text[pi:pe])
pi = pe
pe += len_without_udh
pdu_msgs = []
udh_len = 0x05
mid = 0x00
data_len = 0x03
sms_ref = self._get_rand_id() if self.rand_id is None else self.rand_id
sms_ref &= 0xFF
for i, msg in enumerate(msgs):
i += 1
total_parts = len(msgs)
if limit == consts.SEVENBIT_SIZE:
udh = (chr(udh_len) + chr(mid) + chr(data_len) +
chr(sms_ref) + chr(total_parts) + chr(i))
padding = " "
else:
udh = (unichr(int("%04x" % ((udh_len << 8) | mid), 16)) +
unichr(int("%04x" % ((data_len << 8) | sms_ref), 16)) +
unichr(int("%04x" % ((total_parts << 8) | i), 16)))
padding = ""
pdu_msgs.append(packing_func(padding + msg, udh))
return pdu_msgs
def _get_rand_id(self):
if not self.id_list:
self.id_list = range(0, 255)
return list(self.id_list).pop(0)

79
messaging/sms/udh.py

@ -0,0 +1,79 @@
# See LICENSE
class PortAddress(object):
def __init__(self, dest_port, orig_port, eight_bits):
self.dest_port = dest_port
self.orig_port = orig_port
self.eight_bits = eight_bits
def __repr__(self):
args = (self.dest_port, self.orig_port)
return "<PortAddress dest_port: %d orig_port: %d>" % args
class ConcatReference(object):
def __init__(self, ref, cnt, seq, eight_bits):
self.ref = ref
self.cnt = cnt
self.seq = seq
self.eight_bits = eight_bits
def __repr__(self):
args = (self.ref, self.cnt, self.seq)
return "<ConcatReference ref: %d cnt: %d seq: %d>" % args
class UserDataHeader(object):
def __init__(self):
self.concat = None
self.ports = None
self.headers = {}
def __repr__(self):
args = (self.headers, self.concat, self.ports)
return "<UserDataHeader data: %s concat: %s ports: %s>" % args
@classmethod
def from_status_report_ref(cls, ref):
udh = cls()
udh.concat = ConcatReference(ref, 0, 0, True)
return udh
@classmethod
def from_bytes(cls, data):
udh = cls()
while len(data):
iei = data.pop(0)
ie_len = data.pop(0)
ie_data = data[:ie_len]
data = data[ie_len:]
udh.headers[iei] = ie_data
if iei == 0x00:
# process SM concatenation 8bit ref.
ref, cnt, seq = ie_data
udh.concat = ConcatReference(ref, cnt, seq, True)
if iei == 0x08:
# process SM concatenation 16bit ref.
ref = ie_data[0] << 8 | ie_data[1]
cnt = ie_data[2]
seq = ie_data[3]
udh.concat = ConcatReference(ref, cnt, seq, False)
elif iei == 0x04:
# process App port addressing 8bit
dest_port, orig_port = ie_data
udh.ports = PortAddress(dest_port, orig_port, False)
elif iei == 0x05:
# process App port addressing 16bit
dest_port = ie_data[0] << 8 | ie_data[1]
orig_port = ie_data[2] << 8 | ie_data[3]
udh.ports = PortAddress(dest_port, orig_port, False)
return udh

38
messaging/sms/wap.py

@ -0,0 +1,38 @@
# See LICENSE
from array import array
from messaging.mms.mms_pdu import MMSDecoder
def is_a_wap_push_notification(s):
if not isinstance(s, str):
raise TypeError("data must be an array.array serialised to string")
data = array("B", s)
try:
return data[1] == 0x06
except IndexError:
return False
def extract_push_notification(s):
data = array("B", s)
wap_push, offset = data[1:3]
assert wap_push == 0x06
offset += 3
data = data[offset:]
# XXX: Not all WAP pushes are MMS
return MMSDecoder().decode_data(data)
def is_mms_notification(push):
# XXX: Pretty poor, but until we decode generic WAP pushes
# it will have to suffice. Ideally we would read the
# content-type from the WAP push header and test
return (push.headers.get('From') is not None and
push.headers.get('Content-Location') is not None)

0
messaging/test/__init__.py

BIN
messaging/test/mms-data/27d0a048cd79555de05283a22372b0eb.mms

BIN
messaging/test/mms-data/BTMMS.MMS

BIN
messaging/test/mms-data/NOWMMS.MMS

BIN
messaging/test/mms-data/SEC-SGHS300M.mms

BIN
messaging/test/mms-data/SIMPLE.MMS

BIN
messaging/test/mms-data/SonyEricssonT310-R201.mms

BIN
messaging/test/mms-data/TOMSLOT.MMS

BIN
messaging/test/mms-data/gallery2test.mms

BIN
messaging/test/mms-data/iPhone.mms

BIN
messaging/test/mms-data/images_are_cut_off_debug.mms

BIN
messaging/test/mms-data/m.mms

BIN
messaging/test/mms-data/openwave.mms

BIN
messaging/test/mms-data/projekt_exempel.mms

267
messaging/test/test_gsm_encoding.py

@ -0,0 +1,267 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2011 Sphere Systems Ltd
# Author: Andrew Bird
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Unittests for the gsm encoding/decoding module"""
import unittest
import messaging.sms.gsm0338 # imports GSM7 codec
# Reversed from: ftp://ftp.unicode.org/Public/MAPPINGS/ETSI/GSM0338.TXT
MAP = {
# unichr(0x0000): (0x0000, 0x00), # Null
'@': (0x0040, 0x00),
'£': (0x00a3, 0x01),
'$': (0x0024, 0x02),
'¥': (0x00a5, 0x03),
'è': (0x00e8, 0x04),
'é': (0x00e9, 0x05),
'ù': (0x00f9, 0x06),
'ì': (0x00ec, 0x07),
'ò': (0x00f2, 0x08),
'Ç': (0x00c7, 0x09), # LATIN CAPITAL LETTER C WITH CEDILLA
chr(0x000a): (0x000a, 0x0a), # Linefeed
'Ø': (0x00d8, 0x0b),
'ø': (0x00f8, 0x0c),
chr(0x000d): (0x000d, 0x0d), # Carriage return
'Å': (0x00c5, 0x0e),
'å': (0x00e5, 0x0f),
'Δ': (0x0394, 0x10),
'_': (0x005f, 0x11),
'Φ': (0x03a6, 0x12),
'Γ': (0x0393, 0x13),
'Λ': (0x039b, 0x14),
'Ω': (0x03a9, 0x15),
'Π': (0x03a0, 0x16),
'Ψ': (0x03a8, 0x17),
'Σ': (0x03a3, 0x18),
'Θ': (0x0398, 0x19),
'Ξ': (0x039e, 0x1a),
chr(0x00a0): (0x00a0, 0x1b), # Escape to extension table (displayed
# as NBSP, on decode of invalid escape
# sequence)
'Æ': (0x00c6, 0x1c),
'æ': (0x00e6, 0x1d),
'ß': (0x00df, 0x1e),
'É': (0x00c9, 0x1f),
' ': (0x0020, 0x20),
'!': (0x0021, 0x21),
'"': (0x0022, 0x22),
'#': (0x0023, 0x23),
'¤': (0x00a4, 0x24),
'%': (0x0025, 0x25),
'&': (0x0026, 0x26),
'\'': (0x0027, 0x27),
'{': (0x007b, 0x1b28),
'}': (0x007d, 0x1b29),
'*': (0x002a, 0x2a),
'+': (0x002b, 0x2b),
',': (0x002c, 0x2c),
'-': (0x002d, 0x2d),
'.': (0x002e, 0x2e),
'\\': (0x005c, 0x1b2f),
'0': (0x0030, 0x30),
'1': (0x0031, 0x31),
'2': (0x0032, 0x32),
'3': (0x0033, 0x33),
'4': (0x0034, 0x34),
'5': (0x0035, 0x35),
'6': (0x0036, 0x36),
'7': (0x0037, 0x37),
'8': (0x0038, 0x38),
'9': (0x0039, 0x39),
':': (0x003a, 0x3a),
';': (0x003b, 0x3b),
'[': (0x005b, 0x1b3c),
chr(0x000c): (0x000c, 0x1b0a), # Formfeed
']': (0x005d, 0x1b3e),
'?': (0x003f, 0x3f),
'|': (0x007c, 0x1b40),
'A': (0x0041, 0x41),
'B': (0x0042, 0x42),
'C': (0x0043, 0x43),
'D': (0x0044, 0x44),
'E': (0x0045, 0x45),
'F': (0x0046, 0x46),
'G': (0x0047, 0x47),
'H': (0x0048, 0x48),
'I': (0x0049, 0x49),
'J': (0x004a, 0x4a),
'K': (0x004b, 0x4b),
'L': (0x004c, 0x4c),
'M': (0x004d, 0x4d),
'N': (0x004e, 0x4e),
'O': (0x004f, 0x4f),
'P': (0x0050, 0x50),
'Q': (0x0051, 0x51),
'R': (0x0052, 0x52),
'S': (0x0053, 0x53),
'T': (0x0054, 0x54),
'U': (0x0055, 0x55),
'V': (0x0056, 0x56),
'W': (0x0057, 0x57),
'X': (0x0058, 0x58),
'Y': (0x0059, 0x59),
'Z': (0x005a, 0x5a),
'Ä': (0x00c4, 0x5b),
'Ö': (0x00d6, 0x5c),
'Ñ': (0x00d1, 0x5d),
'Ü': (0x00dc, 0x5e),
'§': (0x00a7, 0x5f),
'¿': (0x00bf, 0x60),
'a': (0x0061, 0x61),
'b': (0x0062, 0x62),
'c': (0x0063, 0x63),
'd': (0x0064, 0x64),
'': (0x20ac, 0x1b65),
'f': (0x0066, 0x66),
'g': (0x0067, 0x67),
'h': (0x0068, 0x68),
'<': (0x003c, 0x3c),
'j': (0x006a, 0x6a),
'k': (0x006b, 0x6b),
'l': (0x006c, 0x6c),
'm': (0x006d, 0x6d),
'n': (0x006e, 0x6e),
'~': (0x007e, 0x1b3d),
'p': (0x0070, 0x70),
'q': (0x0071, 0x71),
'r': (0x0072, 0x72),
's': (0x0073, 0x73),
't': (0x0074, 0x74),
'>': (0x003e, 0x3e),
'v': (0x0076, 0x76),
'i': (0x0069, 0x69),
'x': (0x0078, 0x78),
'^': (0x005e, 0x1b14),
'z': (0x007a, 0x7a),
'ä': (0x00e4, 0x7b),
'ö': (0x00f6, 0x7c),
'ñ': (0x00f1, 0x7d),
'ü': (0x00fc, 0x7e),
'à': (0x00e0, 0x7f),
'¡': (0x00a1, 0x40),
'/': (0x002f, 0x2f),
'o': (0x006f, 0x6f),
'u': (0x0075, 0x75),
'w': (0x0077, 0x77),
'y': (0x0079, 0x79),
'e': (0x0065, 0x65),
'=': (0x003d, 0x3d),
'(': (0x0028, 0x28),
')': (0x0029, 0x29),
}
GREEK_MAP = { # Note: these might look like Latin uppercase, but they aren't
'Α': (0x0391, 0x41),
'Β': (0x0392, 0x42),
'Ε': (0x0395, 0x45),
'Η': (0x0397, 0x48),
'Ι': (0x0399, 0x49),
'Κ': (0x039a, 0x4b),
'Μ': (0x039c, 0x4d),
'Ν': (0x039d, 0x4e),
'Ο': (0x039f, 0x4f),
'Ρ': (0x03a1, 0x50),
'Τ': (0x03a4, 0x54),
'Χ': (0x03a7, 0x58),
'Υ': (0x03a5, 0x59),
'Ζ': (0x0396, 0x5a),
}
QUIRK_MAP = {
'ç': (0x00e7, 0x09),
}
BAD = -1
class TestEncodingFunctions(unittest.TestCase):
def test_encoding_supported_unicode_gsm(self):
for key in list(MAP.keys()):
# Use 'ignore' so that we see the code tested, not an exception
s_gsm = key.encode('gsm0338', 'ignore')
if len(s_gsm) == 1:
i_gsm = ord(s_gsm)
elif len(s_gsm) == 2:
i_gsm = (ord(s_gsm[0]) << 8) + ord(s_gsm[1])
else:
i_gsm = BAD # so we see the comparison, not an exception
# We shouldn't generate an invalid escape sequence
if key == chr(0x00a0):
self.assertEqual(BAD, i_gsm)
else:
self.assertEqual(MAP[key][1], i_gsm)
def test_encoding_supported_greek_unicode_gsm(self):
# Note: Conversion is one way, hence no corresponding decode test
for key in list(GREEK_MAP.keys()):
# Use 'replace' so that we trigger the mapping
s_gsm = key.encode('gsm0338', 'replace')
if len(s_gsm) == 1:
i_gsm = ord(s_gsm)
else:
i_gsm = BAD # so we see the comparison, not an exception
self.assertEqual(GREEK_MAP[key][1], i_gsm)
def test_encoding_supported_quirk_unicode_gsm(self):
# Note: Conversion is one way, hence no corresponding decode test
for key in list(QUIRK_MAP.keys()):
# Use 'replace' so that we trigger the mapping
s_gsm = key.encode('gsm0338', 'replace')
if len(s_gsm) == 1:
i_gsm = ord(s_gsm)
else:
i_gsm = BAD # so we see the comparison, not an exception
self.assertEqual(QUIRK_MAP[key][1], i_gsm)
def test_decoding_supported_unicode_gsm(self):
for key in list(MAP.keys()):
i_gsm = MAP[key][1]
if i_gsm <= 0xff:
s_gsm = chr(i_gsm)
elif i_gsm <= 0xffff:
s_gsm = chr((i_gsm & 0xff00) >> 8)
s_gsm += chr(i_gsm & 0x00ff)
s_unicode = s_gsm.decode('gsm0338', 'strict')
self.assertEqual(MAP[key][0], ord(s_unicode))
def test_is_gsm_text_true(self):
for key in list(MAP.keys()):
if key == chr(0x00a0):
continue
self.assertEqual(messaging.sms.gsm0338.is_gsm_text(key), True)
def test_is_gsm_text_false(self):
self.assertEqual(
messaging.sms.gsm0338.is_gsm_text(chr(0x00a0)), False)
for i in range(1, 0xffff + 1):
if chr(i) not in MAP:
# Note: it's a little odd, but on error we want to see values
if messaging.sms.gsm0338.is_gsm_text(chr(i)) is not False:
self.assertEqual(BAD, i)

378
messaging/test/test_mms.py

@ -0,0 +1,378 @@
# -*- coding: utf-8 -*-
from array import array
import datetime
import os
import unittest
from messaging.mms.message import MMSMessage
# test data extracted from heyman's
# http://github.com/heyman/mms-decoder
DATA_DIR = os.path.join(os.path.dirname(__file__), 'mms-data')
class TestMmsDecoding(unittest.TestCase):
def test_decoding_from_data(self):
path = os.path.join(DATA_DIR, 'iPhone.mms')
data = array("B", open(path, 'rb').read())
mms = MMSMessage.from_data(data)
headers = {
'From': '<not inserted>', 'Transaction-Id': '1262957356-3',
'MMS-Version': '1.2', 'To': '1337/TYPE=PLMN',
'Message-Type': 'm-send-req',
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '0.smil', 'Type': 'application/smil'}),
}
self.assertEqual(mms.headers, headers)
def test_decoding_iPhone_mms(self):
path = os.path.join(DATA_DIR, 'iPhone.mms')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'From': '<not inserted>', 'Transaction-Id': '1262957356-3',
'MMS-Version': '1.2', 'To': '1337/TYPE=PLMN',
'Message-Type': 'm-send-req',
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '0.smil', 'Type': 'application/smil'}),
}
smil_data = '<smil>\n<head>\n<layout>\n <root-layout/>\n<region id="Text" top="70%" left="0%" height="30%" width="100%" fit="scroll"/>\n<region id="Image" top="0%" left="0%" height="70%" width="100%" fit="meet"/>\n</layout>\n</head>\n<body>\n<par dur="10s">\n<img src="IMG_6807.jpg" region="Image"/>\n</par>\n</body>\n</smil>\n'
self.assertEqual(mms.headers, headers)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.related')
self.assertEqual(len(mms.data_parts), 2)
self.assertEqual(mms.data_parts[0].content_type, 'application/smil')
self.assertEqual(mms.data_parts[0].data, smil_data)
self.assertEqual(mms.data_parts[1].content_type, 'image/jpeg')
self.assertEqual(mms.data_parts[1].content_type_parameters,
{'Name': 'IMG_6807.jpg'})
def test_decoding_SIMPLE_mms(self):
path = os.path.join(DATA_DIR, 'SIMPLE.MMS')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'Transaction-Id': '1234', 'MMS-Version': '1.0',
'Message-Type': 'm-retrieve-conf',
'Date': datetime.datetime(2002, 12, 20, 21, 26, 56),
'Content-Type': ('application/vnd.wap.multipart.related', {}),
'Subject': 'Simple message',
}
text_data = "This is a simple MMS message with a single text body part."
self.assertEqual(mms.headers, headers)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.related')
self.assertEqual(len(mms.data_parts), 1)
self.assertEqual(mms.data_parts[0].content_type, 'text/plain')
self.assertEqual(mms.data_parts[0].data, text_data)
def test_decoding_BTMMS_mms(self):
path = os.path.join(DATA_DIR, 'BTMMS.MMS')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'Transaction-Id': '1234', 'MMS-Version': '1.0',
'Message-Type': 'm-retrieve-conf',
'Date': datetime.datetime(2003, 1, 21, 1, 57, 4),
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '<btmms.smil>', 'Type': 'application/smil'}),
'Subject': 'BT Ignite MMS',
}
smil_data = '<smil><head><layout><root-layout/>\r\n<region id="Image" top="0" left="0" height="50%" width="100%" fit="hidden"/>\r\n<region id="Text" top="50%" left="0" height="50%" width="100%" fit="hidden"/>\r\n</layout>\r\n</head>\r\n<body><par dur="6825ms"><img src="btlogo.gif" region="Image"></img>\r\n<audio src="catchy_g.amr" begin="350ms" end="6350ms"></audio>\r\n</par>\r\n<par dur="3000ms"><text src="btmms.txt" region="Text"><param name="foreground-color" value="#000000"/>\r\n</text>\r\n</par>\r\n</body>\r\n</smil>\r\n\r\n'
text_data = 'BT Ignite\r\n\r\nMMS Services'
self.assertEqual(mms.headers, headers)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.related')
self.assertEqual(len(mms.data_parts), 4)
self.assertEqual(mms.data_parts[0].content_type, 'application/smil')
self.assertEqual(mms.data_parts[0].data, smil_data)
self.assertEqual(mms.data_parts[1].content_type, 'image/gif')
self.assertEqual(mms.data_parts[2].content_type, 'audio/amr')
self.assertEqual(mms.data_parts[3].content_type, 'text/plain')
self.assertEqual(mms.data_parts[3].data, text_data)
def test_decoding_TOMSLOT_mms(self):
path = os.path.join(DATA_DIR, 'TOMSLOT.MMS')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'From': '616c6c616e40746f6d736c6f742e636f6d'.decode('hex'),
'Transaction-Id': '1234',
'MMS-Version': '1.0', 'Message-Type': 'm-retrieve-conf',
'Date': datetime.datetime(2003, 2, 16, 3, 48, 33),
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '<tomslot.smil>', 'Type': 'application/smil'}),
'Subject': 'Tom Slot Band',
}
smil_data = '<smil>\r\n\t<head>\r\n\t\t<meta name="title" content="smil"/>\r\n\t\t<meta name="author" content="PANASONIC"/>\r\n\t\t<layout>\r\n\t\t\t<root-layout background-color="#FFFFFF" width="132" height="176"/>\r\n\t\t\t<region id="Image" width="132" height="100" left="0" top="0" fit="meet"/>\r\n\t\t\t<region id="Text" width="132" height="34" left="0" top="100" fit="scroll"/>\r\n\t\t</layout>\r\n\t</head>\r\n\t<body>\r\n\t\t<par dur="1000ms">\r\n\t\t\t<img src="img00.jpg" region="Image"/>\r\n\t\t</par>\r\n\t\t<par dur="1000ms">\r\n\t\t\t<img src="img01.jpg" region="Image"/>\r\n\t\t</par>\r\n\t\t<par dur="1000ms">\r\n\t\t\t<img src="img02.jpg" region="Image"/>\r\n\t\t</par>\r\n\t\t<par dur="1000ms">\r\n\t\t\t<img src="img03.jpg" region="Image"/>\r\n\t\t</par>\r\n\t\t<par dur="22000ms">\r\n\t\t\t<img src="img04.jpg" region="Image"/>\r\n\t\t\t<text src="txt04.txt" region="Text">\r\n\t\t\t\t<param name="foreground-color" value="#0000F8"/>\r\n\t\t\t</text>\r\n\t\t\t<audio src="aud04.amr"/>\r\n\t\t</par>\r\n\t</body>\r\n</smil>\r\n'
text_data = 'Presented by NowMMS\r\n'
self.assertEqual(mms.headers, headers)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.related')
self.assertEqual(len(mms.data_parts), 8)
self.assertEqual(mms.data_parts[0].content_type, 'application/smil')
self.assertEqual(mms.data_parts[0].data, smil_data)
self.assertEqual(mms.data_parts[1].content_type, 'image/jpeg')
self.assertEqual(mms.data_parts[2].content_type, 'image/jpeg')
self.assertEqual(mms.data_parts[3].content_type, 'image/jpeg')
self.assertEqual(mms.data_parts[4].content_type, 'image/jpeg')
self.assertEqual(mms.data_parts[5].content_type, 'image/jpeg')
self.assertEqual(mms.data_parts[6].content_type, 'text/plain')
self.assertEqual(mms.data_parts[6].data, text_data)
self.assertEqual(mms.data_parts[7].content_type, 'audio/amr')
def test_decoding_images_are_cut_off_debug_mms(self):
path = os.path.join(DATA_DIR, 'images_are_cut_off_debug.mms')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'From': '<not inserted>', 'Read-Reply': False,
'Transaction-Id': '2112410527', 'MMS-Version': '1.0',
'To': '7464707440616a616a672e63646d'.decode('hex'),
'Delivery-Report': False,
'Message-Type': 'm-send-req',
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '<SMIL.TXT>', 'Type': 'application/smil'}),
'Subject': 'Picture3',
}
smil_data = '<smil><head><layout><root-layout height="160px" width="128px"/><region id="Top" top="0" left="0" height="50%" width="100%" /><region id="Bottom" top="50%" left="0" height="50%" width="100%" /></layout></head><body><par dur="5s"><img src="cid:Picture3.jpg" region="Top" begin="0s" end="5s"></img></par></body></smil>'
self.assertEqual(mms.headers, headers)
self.assertEqual(len(mms.data_parts), 2)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.related')
self.assertEqual(mms.data_parts[0].content_type, 'image/jpeg')
self.assertEqual(mms.data_parts[0].content_type_parameters,
{'Name': 'Picture3.jpg'})
self.assertEqual(mms.data_parts[1].content_type, 'application/smil')
self.assertEqual(mms.data_parts[1].data, smil_data)
def test_decoding_openwave_mms(self):
path = os.path.join(DATA_DIR, 'openwave.mms')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'From': '2b31363530353535303030302f545950453d504c4d4e'.decode('hex'),
'Message-Class': 'Personal',
'Transaction-Id': '1067263672', 'MMS-Version': '1.0',
'Priority': 'Normal', 'To': '112/TYPE=PLMN',
'Delivery-Report': False, 'Message-Type': 'm-send-req',
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '<smil_0>', 'Type': 'application/smil'}),
'Subject': 'rubrik',
}
smil_data = '<smil>\n <head>\n <layout>\n <root-layout width="100%" height="100%" />\n <region id="Text" left="0%" top="0%" width="100%" height="50%" />\n <region id="Image" left="0%" top="50%" width="100%" height="50%" />\n </layout>\n </head>\n <body>\n <par dur="30000ms">\n <text src="cid:text_0" region="Text" />\n </par>\n </body>\n</smil>\n'
text_data = 'rubrik'
self.assertEqual(mms.headers, headers)
self.assertEqual(len(mms.data_parts), 2)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.related')
self.assertEqual(mms.data_parts[0].content_type, 'application/smil')
self.assertEqual(mms.data_parts[0].data, smil_data)
self.assertEqual(mms.data_parts[1].data, text_data)
def test_decoding_SonyEricssonT310_R201_mms(self):
path = os.path.join(DATA_DIR, 'SonyEricssonT310-R201.mms')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'Sender-Visibility': 'Show', 'From': '<not inserted>',
'Read-Reply': False, 'Message-Class': 'Personal',
'Transaction-Id': '1-8db', 'MMS-Version': '1.0',
'Priority': 'Normal', 'To': '55225/TYPE=PLMN',
'Delivery-Report': False, 'Message-Type': 'm-send-req',
'Date': datetime.datetime(2004, 3, 18, 7, 30, 34),
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '<AAAA>', 'Type': 'application/smil'}),
}
text_data = 'Hej hopp'
smil_data = '<smil><head><layout><root-layout height="240px" width="160px"/>\r\n<region id="Image" top="0" left="0" height="50%" width="100%" fit="hidden"/>\r\n<region id="Text" top="50%" left="0" height="50%" width="100%" fit="hidden"/>\r\n</layout>\r\n</head>\r\n<body><par dur="2000ms"><img src="Tony.gif" region="Image"></img>\r\n<text src="mms.txt" region="Text"></text>\r\n<audio src="OldhPhone.mid"></audio>\r\n</par>\r\n</body>\r\n</smil>\r\n'
self.assertEqual(mms.headers, headers)
self.assertEqual(len(mms.data_parts), 4)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.related')
self.assertEqual(mms.data_parts[0].content_type, 'image/gif')
self.assertEqual(mms.data_parts[0].content_type_parameters,
{'Name': 'Tony.gif'})
self.assertEqual(mms.data_parts[1].content_type, 'text/plain')
self.assertEqual(mms.data_parts[1].data, text_data)
self.assertEqual(mms.data_parts[2].content_type, 'audio/midi')
self.assertEqual(mms.data_parts[2].content_type_parameters,
{'Name': 'OldhPhone.mid'})
self.assertEqual(mms.data_parts[3].content_type, 'application/smil')
self.assertEqual(mms.data_parts[3].data, smil_data)
def test_decoding_gallery2test_mms(self):
path = os.path.join(DATA_DIR, 'gallery2test.mms')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'From': '2b31363530353535303030302f545950453d504c4d4e'.decode('hex'),
'Message-Class': 'Personal',
'Transaction-Id': '1118775337', 'MMS-Version': '1.0',
'Priority': 'Normal', 'To': 'Jg', 'Delivery-Report': False,
'Message-Type': 'm-send-req',
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '<smil_0>', 'Type': 'application/smil'}),
'Subject': 'Jgj',
}
text_data = 'Jgj'
smil_data = '<smil>\n <head>\n <layout>\n <root-layout width="100%" height="100%" />\n <region id="Text" left="0%" top="0%" width="100%" height="50%" />\n <region id="Image" left="0%" top="50%" width="100%" height="50%" />\n </layout>\n </head>\n <body>\n <par dur="30000ms">\n <img src="cid:image_0" region="Image" alt="gnu-head" />\n <text src="cid:text_0" region="Text" />\n </par>\n </body>\n</smil>\n'
self.assertEqual(mms.headers, headers)
self.assertEqual(len(mms.data_parts), 3)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.related')
self.assertEqual(mms.data_parts[0].content_type, 'application/smil')
self.assertEqual(mms.data_parts[0].data, smil_data)
self.assertEqual(mms.data_parts[1].content_type, 'text/plain')
self.assertEqual(mms.data_parts[1].data, text_data)
self.assertEqual(mms.data_parts[2].content_type, 'image/jpeg')
# XXX: Shouldn't it be 'Name' instead ?
self.assertEqual(mms.data_parts[2].content_type_parameters,
{'name': 'gnu-head.jpg'})
def test_decoding_projekt_exempel_mms(self):
path = os.path.join(DATA_DIR, 'projekt_exempel.mms')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'Sender-Visibility': 'Show', 'From': '<not inserted>',
'Read-Reply': False, 'Message-Class': 'Personal',
'Transaction-Id': '4-fc60', 'MMS-Version': '1.0',
'Priority': 'Normal', 'To': '12345/TYPE=PLMN',
'Delivery-Report': False, 'Message-Type': 'm-send-req',
'Date': datetime.datetime(2004, 5, 23, 15, 13, 40),
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '<AAAA>', 'Type': 'application/smil'}),
'Subject': 'Hej',
}
smil_data = '<smil><head><layout><root-layout height="240px" width="160px"/>\r\n<region id="Image" top="0" left="0" height="50%" width="100%" fit="hidden"/>\r\n<region id="Text" top="50%" left="0" height="50%" width="100%" fit="hidden"/>\r\n</layout>\r\n</head>\r\n<body><par dur="2000ms"><text src="mms.txt" region="Text"></text>\r\n<img src="SonyhEr.gif" region="Image"></img>\r\n</par>\r\n</body>\r\n</smil>\r\n'
text_data = 'Jonatan \xc3\xa4r en GNU'
self.assertEqual(mms.headers, headers)
self.assertEqual(len(mms.data_parts), 3)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.related')
self.assertEqual(mms.data_parts[0].content_type, 'text/plain')
self.assertEqual(mms.data_parts[0].data, text_data)
self.assertEqual(mms.data_parts[1].content_type, 'image/gif')
self.assertEqual(mms.data_parts[2].content_type, 'application/smil')
self.assertEqual(mms.data_parts[2].data, smil_data)
self.assertEqual(mms.data_parts[2].content_type_parameters,
{'Charset': 'utf-8', 'Name': 'mms.smil'})
def test_decoding_m_mms(self):
path = os.path.join(DATA_DIR, 'm.mms')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'From': '676f6c64706f737440686f746d61696c2e636f6d'.decode('hex'),
'Transaction-Id': '0000000001',
'MMS-Version': '1.0', 'Message-Type': 'm-retrieve-conf',
'Date': datetime.datetime(2002, 8, 9, 13, 8, 2),
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '<A0>', 'Type': 'application/smil'}),
'Subject': 'GOLD',
}
text_data1 = 'Audio'
text_data2 = 'Text +'
text_data3 = 'tagtag.com/gold\r\n'
text_data4 = 'globalisierunglobalisierunglobalisierunglobalisierunglobalisierunglobalisierunglobalisierungnureisilabolg'
text_data5 = 'KLONE\r\nKLONE\r\n'
text_data6 = 'pr\xe4sentiert..'
text_data7 = 'GOLD'
smil_data = '<smil><head><layout><root-layout background-color="#000000"/>\r\n<region id="text" top="0" left="0" height="100%" width="100%"/>\r\n</layout>\r\n</head>\r\n<body>\r\n<par dur="3000ms">\r\n<text src="Text0000.txt" region="text">\r\n <param name="foreground-color" value="#ffff00"/>\r\n <param name="textsize" value="large"/>\r\n</text>\r\n</par>\r\n<par dur="2000ms">\r\n<text src="Text0001.txt" region="text">\r\n <param name="foreground-color" value="#ffff00"/>\r\n <param name="textsize" value="small"/>\r\n</text>\r\n</par>\r\n<par dur="2000ms">\r\n<text src="Text0007.txt" region="text">\r\n <param name="foreground-color" value="#ffff00"/>\r\n <param name="textsize" value="normal"/>\r\n</text>\r\n</par>\r\n<par dur="6000ms">\r\n<text src="Text0008.txt" region="text">\r\n <param name="foreground-color" value="#ffff00"/>\r\n <param name="textsize" value="normal"/>\r\n</text>\r\n<audio src="gold102.amr" start="1000ms"/>\r\n</par>\r\n<seq repeatcount="4">\r\n<par dur="1500ms">\r\n<text src="Text0002.txt" region="text">\r\n <param name="foreground-color" value="#ff0080"/>\r\n <param name="textsize" value="normal"/>\r\n</text>\r\n</par>\r\n<par dur="1500ms">\r\n<text src="Text0003.txt" region="text">\r\n <param name="foreground-color" value="#00ff00"/>\r\n <param name="textsize" value="normal"/>\r\n</text>\r\n</par>\r\n</seq>\r\n<par dur="10000ms">\r\n<text src="Text0006.txt" region="text">\r\n <param name="foreground-color" value="#ffff00"/>\r\n <param name="textsize" value="normal"/>\r\n</text>\r\n</par>\r\n</body></smil>'
self.assertEqual(mms.headers, headers)
self.assertEqual(len(mms.data_parts), 9)
self.assertEqual(mms.content_type, 'application/vnd.wap.multipart.related')
self.assertEqual(mms.data_parts[0].content_type, 'text/plain')
self.assertEqual(mms.data_parts[0].data, text_data1)
self.assertEqual(mms.data_parts[0].content_type_parameters,
{'Charset': 'us-ascii'})
self.assertEqual(mms.data_parts[1].content_type, 'application/smil')
self.assertEqual(mms.data_parts[1].data, smil_data)
self.assertEqual(mms.data_parts[1].content_type_parameters,
{'Charset': 'us-ascii'})
self.assertEqual(mms.data_parts[2].content_type, 'text/plain')
self.assertEqual(mms.data_parts[2].data, text_data2)
self.assertEqual(mms.data_parts[2].content_type_parameters,
{'Charset': 'us-ascii'})
self.assertEqual(mms.data_parts[3].content_type, 'text/plain')
self.assertEqual(mms.data_parts[3].data, text_data3)
self.assertEqual(mms.data_parts[3].content_type_parameters,
{'Charset': 'us-ascii'})
self.assertEqual(mms.data_parts[4].content_type, 'audio/amr')
self.assertEqual(mms.data_parts[5].content_type, 'text/plain')
self.assertEqual(mms.data_parts[5].data, text_data4)
self.assertEqual(mms.data_parts[5].content_type_parameters,
{'Charset': 'us-ascii'})
self.assertEqual(mms.data_parts[6].content_type, 'text/plain')
self.assertEqual(mms.data_parts[6].data, text_data5)
self.assertEqual(mms.data_parts[6].content_type_parameters,
{'Charset': 'us-ascii'})
self.assertEqual(mms.data_parts[7].content_type, 'text/plain')
self.assertEqual(mms.data_parts[7].data, text_data6)
self.assertEqual(mms.data_parts[7].content_type_parameters,
{'Charset': 'us-ascii'})
self.assertEqual(mms.data_parts[8].content_type, 'text/plain')
self.assertEqual(mms.data_parts[8].data, text_data7)
self.assertEqual(mms.data_parts[8].content_type_parameters,
{'Charset': 'us-ascii'})
def test_decoding_27d0a048cd79555de05283a22372b0eb_mms(self):
path = os.path.join(DATA_DIR, '27d0a048cd79555de05283a22372b0eb.mms')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'Sender-Visibility': 'Show', 'From': '<not inserted>',
'Read-Reply': False, 'Message-Class': 'Personal',
'Transaction-Id': '3-31cb', 'MMS-Version': '1.0',
'Priority': 'Normal', 'To': '123/TYPE=PLMN',
'Delivery-Report': False, 'Message-Type': 'm-send-req',
'Date': datetime.datetime(2004, 5, 23, 14, 14, 58),
'Content-Type': ('application/vnd.wap.multipart.related', {'Start': '<AAAA>', 'Type': 'application/smil'}),
'Subject': 'Angående art-tillhörighet',
#'Subject': 'Ang\xc3\xa5ende art-tillh\xc3\xb6righet',
}
smil_data = '<smil><head><layout><root-layout height="240px" width="160px"/>\r\n<region id="Image" top="0" left="0" height="50%" width="100%" fit="hidden"/>\r\n<region id="Text" top="50%" left="0" height="50%" width="100%" fit="hidden"/>\r\n</layout>\r\n</head>\r\n<body><par dur="2000ms"><img src="Rain.wbmp" region="Image"></img>\r\n<text src="mms.txt" region="Text"></text>\r\n</par>\r\n</body>\r\n</smil>\r\n'
text_data = 'Jonatan \xc3\xa4r en gnu.'
self.assertEqual(mms.headers, headers)
self.assertEqual(len(mms.data_parts), 3)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.related')
self.assertEqual(mms.data_parts[0].content_type, 'image/vnd.wap.wbmp')
self.assertEqual(mms.data_parts[0].content_type_parameters,
{'Name': 'Rain.wbmp'})
self.assertEqual(mms.data_parts[1].content_type, 'text/plain')
self.assertEqual(mms.data_parts[1].data, text_data)
self.assertEqual(mms.data_parts[1].content_type_parameters,
{'Charset': 'utf-8', 'Name': 'mms.txt'})
self.assertEqual(mms.data_parts[2].content_type, 'application/smil')
self.assertEqual(mms.data_parts[2].data, smil_data)
self.assertEqual(mms.data_parts[2].content_type_parameters,
{'Charset': 'utf-8', 'Name': 'mms.smil'})
def test_decoding_SEC_SGHS300M(self):
path = os.path.join(DATA_DIR, 'SEC-SGHS300M.mms')
mms = MMSMessage.from_file(path)
self.assertTrue(isinstance(mms, MMSMessage))
headers = {
'Sender-Visibility': 'Show', 'From': '<not inserted>',
'Read-Reply': False, 'Message-Class': 'Personal',
'Transaction-Id': '31887', 'MMS-Version': '1.0',
'To': '303733383334353636342f545950453d504c4d4e'.decode('hex'),
'Delivery-Report': False,
'Message-Type': 'm-send-req', 'Subject': 'IL',
'Content-Type': ('application/vnd.wap.multipart.mixed', {}),
}
text_data = 'HV'
self.assertEqual(mms.headers, headers)
self.assertEqual(len(mms.data_parts), 1)
self.assertEqual(mms.content_type,
'application/vnd.wap.multipart.mixed')
self.assertEqual(mms.data_parts[0].content_type, 'text/plain')
self.assertEqual(mms.data_parts[0].data, text_data)
self.assertEqual(mms.data_parts[0].content_type_parameters,
{'Charset': 'utf-8'})
def test_encoding_m_sendnotifyresp_ind(self):
message = MMSMessage()
message.headers['Transaction-Id'] = 'NOK5AIdhfTMYSG4JeIgAAsHtp72AGAAAAAAAA'
message.headers['Message-Type'] = 'm-notifyresp-ind'
message.headers['Status'] = 'Retrieved'
data = [
140, 131, 152, 78, 79, 75, 53, 65, 73, 100, 104, 102, 84, 77,
89, 83, 71, 52, 74, 101, 73, 103, 65, 65, 115, 72, 116, 112,
55, 50, 65, 71, 65, 65, 65, 65, 65, 65, 65, 65, 0, 141, 144,
149, 129, 132, 163, 1, 35, 129]
self.assertEqual(list(message.encode()[:50]), data)

481
messaging/test/test_sms.py

@ -0,0 +1,481 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
try:
import unittest2 as unittest
except ImportError:
import unittest
from messaging.sms import SmsSubmit, SmsDeliver
from messaging.utils import (timedelta_to_relative_validity as to_relative,
datetime_to_absolute_validity as to_absolute,
FixedOffset)
class TestEncodingFunctions(unittest.TestCase):
def test_converting_timedelta_to_validity(self):
self.assertRaises(ValueError, to_relative, timedelta(minutes=4))
self.assertRaises(ValueError, to_relative, timedelta(weeks=64))
self.assertTrue(isinstance(to_relative(timedelta(hours=6)), int))
self.assertTrue(isinstance(to_relative(timedelta(hours=18)), int))
self.assertTrue(isinstance(to_relative(timedelta(days=15)), int))
self.assertTrue(isinstance(to_relative(timedelta(weeks=31)), int))
self.assertEqual(to_relative(timedelta(minutes=5)), 0)
self.assertEqual(to_relative(timedelta(minutes=6)), 0)
self.assertEqual(to_relative(timedelta(minutes=10)), 1)
self.assertEqual(to_relative(timedelta(hours=12)), 143)
self.assertEqual(to_relative(timedelta(hours=13)), 145)
self.assertEqual(to_relative(timedelta(hours=24)), 167)
self.assertEqual(to_relative(timedelta(days=2)), 168)
self.assertEqual(to_relative(timedelta(days=30)), 196)
def test_converting_datetime_to_validity(self):
# http://www.dreamfabric.com/sms/scts.html
# 12. Feb 1999 05:57:30 GMT+3
when = datetime(1999, 2, 12, 5, 57, 30, 0,
FixedOffset(3 * 60, "GMT+3"))
expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x21]
self.assertEqual(to_absolute(when, "GMT+3"), expected)
when = datetime(1999, 2, 12, 5, 57, 30, 0)
expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x0]
self.assertEqual(to_absolute(when, "UTC"), expected)
when = datetime(1999, 2, 12, 5, 57, 30, 0,
FixedOffset(-3 * 60, "GMT-3"))
expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x29]
self.assertEqual(to_absolute(when, "GMT-3"), expected)
class TestSmsSubmit(unittest.TestCase):
def test_encoding_validity(self):
# no validity
number = '2b3334363136353835313139'.decode('hex')
text = "hola"
expected = "0001000B914316565811F9000004E8373B0C"
sms = SmsSubmit(number, text)
sms.ref = 0x0
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected)
# absolute validity
number = '2b3334363136353835313139'.decode('hex')
text = "hola"
expected = "0019000B914316565811F900000170520251930004E8373B0C"
sms = SmsSubmit(number, text)
sms.ref = 0x0
sms.validity = datetime(2010, 7, 25, 20, 15, 39)
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected)
# relative validity
number = '2b3334363136353835313139'.decode('hex')
text = "hola"
expected = "0011000B914316565811F90000AA04E8373B0C"
expected_len = 18
sms = SmsSubmit(number, text)
sms.ref = 0x0
sms.validity = timedelta(days=4)
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected)
self.assertEqual(pdu.length, expected_len)
def test_encoding_csca(self):
number = '2b3334363136353835313139'.decode('hex')
text = "hola"
csca = "+34646456456"
expected = "07914346466554F601000B914316565811F9000004E8373B0C"
expected_len = 17
sms = SmsSubmit(number, text)
sms.csca = csca
sms.ref = 0x0
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected)
self.assertEqual(pdu.length, expected_len)
self.assertEqual(pdu.cnt, 1)
self.assertEqual(pdu.seq, 1)
def test_encoding_class(self):
number = '2b3334363534313233343536'.decode('hex')
text = "hey yo"
expected_0 = "0001000B914356143254F6001006E8721E947F03"
expected_1 = "0001000B914356143254F6001106E8721E947F03"
expected_2 = "0001000B914356143254F6001206E8721E947F03"
expected_3 = "0001000B914356143254F6001306E8721E947F03"
sms = SmsSubmit(number, text)
sms.ref = 0x0
sms.klass = 0
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected_0)
sms.klass = 1
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected_1)
sms.klass = 2
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected_2)
sms.klass = 3
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected_3)
def test_encoding_request_status(self):
# tested with pduspy.exe and http://www.rednaxela.net/pdu.php
number = '2b3334363534313233343536'.decode('hex')
text = "hey yo"
expected = "0021000B914356143254F6000006E8721E947F03"
sms = SmsSubmit(number, text)
sms.ref = 0x0
sms.request_status = True
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected)
def test_encoding_message_with_latin1_chars(self):
# tested with pduspy.exe
number = '2b3334363534313233343536'.decode('hex')
text = u"Hölä"
expected = "0011000B914356143254F60000AA04483E7B0F"
sms = SmsSubmit(number, text)
sms.ref = 0x0
sms.validity = timedelta(days=4)
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected)
# tested with pduspy.exe
number = '2b3334363534313233343536'.decode('hex')
text = u"BÄRÇA äñ@"
expected = "0001000B914356143254F6000009C2AD341104EDFB00"
sms = SmsSubmit(number, text)
sms.ref = 0x0
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected)
def test_encoding_8bit_message(self):
number = "01000000000"
csca = "+44000000000"
text = "Hi there..."
expected = "07914400000000F001000B811000000000F000040B48692074686572652E2E2E"
sms = SmsSubmit(number, text)
sms.ref = 0x0
sms.csca = csca
sms.fmt = 0x04 # 8 bits
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected)
def test_encoding_ucs2_message(self):
number = '2b3334363136353835313139'.decode('hex')
text = u'あ叶葉'
csca = '+34646456456'
expected = "07914346466554F601000B914316565811F9000806304253F68449"
sms = SmsSubmit(number, text)
sms.ref = 0x0
sms.csca = csca
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected)
text = u"Русский"
number = '363535333435363738'.decode('hex')
expected = "001100098156355476F80008AA0E0420044304410441043A04380439"
sms = SmsSubmit(number, text)
sms.ref = 0x0
sms.validity = timedelta(days=4)
pdu = sms.to_pdu()[0]
self.assertEqual(pdu.pdu, expected)
def test_encoding_multipart_7bit(self):
# text encoded with umts-tools
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"
number = '363535333435363738'.decode('hex')
expected = [
"005100098156355476F80000AAA00500038803019E72D03DCC5E83EE693A1AB44CBBCF73500BE47ECB41ECF7BC0CA2A3CBA0F1BBDD7EBB41F4777D8C6681D26690BB9CA6A3CB7290F95D9E83DC6F3988FDB6A7DD6790599E2EBBC973D038EC06A1EB723A28FFAEB340493328CC6683DA653768FCAEBBE9A07B9A8E06E5DF7516485CA783DC6F7719447FBF41EDFA18BD0325CDA0FCBB0E1A87DD",
"005100098156355476F80000AAA005000388030240E6349B0DA2A3CBA0BADBFC969FD3F6B4FB0C6AA7DD757A19744DD3D1A0791A4FCF83E6E5F1DB4D9E9F40F7B79C8E06BDCD20727A4E0FBBC76590BCEE6681B2EFBA7C0E4ACF41747419540CCBE96850D84D0695ED65799E8E4EBBCF203A3A4C9F83D26E509ACE0205DD64500B7447A7C768507A0E6ABFE565500B947FD741F7349B0D129741",
"005100098156355476F80000AA14050003880303C2A066D8CD02B5F3A0F9DB0D",
]
sms = SmsSubmit(number, text)
sms.ref = 0x0
sms.rand_id = 136
sms.validity = timedelta(days=4)
ret = sms.to_pdu()
cnt = len(ret)
for i, pdu in enumerate(ret):
self.assertEqual(pdu.pdu, expected[i])
self.assertEqual(pdu.seq, i + 1)
self.assertEqual(pdu.cnt, cnt)
def test_encoding_bad_number_raises_error(self):
self.assertRaises(ValueError, SmsSubmit, "032BADNUMBER", "text")
def test_encoding_bad_csca_raises_error(self):
sms = SmsSubmit("54342342", "text")
self.assertRaises(ValueError, setattr, sms, 'csca', "1badcsca")
class TestSubmitPduCounts(unittest.TestCase):
DEST = "+3530000000"
GSM_CHAR = "x"
EGSM_CHAR = u""
UNICODE_CHAR = u"ő"
def test_gsm_1(self):
sms = SmsSubmit(self.DEST, self.GSM_CHAR * 160)
self.assertEqual(len(sms.to_pdu()), 1)
def test_gsm_2(self):
sms = SmsSubmit(self.DEST, self.GSM_CHAR * 161)
self.assertEqual(len(sms.to_pdu()), 2)
def test_gsm_3(self):
sms = SmsSubmit(self.DEST, self.GSM_CHAR * 153 * 2)
self.assertEqual(len(sms.to_pdu()), 2)
def test_gsm_4(self):
sms = SmsSubmit(self.DEST,
self.GSM_CHAR * 153 * 2 + self.GSM_CHAR)
self.assertEqual(len(sms.to_pdu()), 3)
def test_gsm_5(self):
sms = SmsSubmit(self.DEST, self.GSM_CHAR * 153 * 3)
self.assertEqual(len(sms.to_pdu()), 3)
def test_gsm_6(self):
sms = SmsSubmit(self.DEST,
self.GSM_CHAR * 153 * 3 + self.GSM_CHAR)
self.assertEqual(len(sms.to_pdu()), 4)
def test_egsm_1(self):
sms = SmsSubmit(self.DEST, self.EGSM_CHAR * 80)
self.assertEqual(len(sms.to_pdu()), 1)
def test_egsm_2(self):
sms = SmsSubmit(self.DEST,
self.EGSM_CHAR * 79 + self.GSM_CHAR)
self.assertEqual(len(sms.to_pdu()), 1)
def test_egsm_3(self):
sms = SmsSubmit(self.DEST, self.EGSM_CHAR * 153) # 306 septets
self.assertEqual(len(sms.to_pdu()), 3)
def test_egsm_4(self):
sms = SmsSubmit(self.DEST,
self.EGSM_CHAR * 229 + self.GSM_CHAR) # 459 septets
self.assertEqual(len(sms.to_pdu()), 4)
def test_unicode_1(self):
sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 70)
self.assertEqual(len(sms.to_pdu()), 1)
def test_unicode_2(self):
sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 70 + self.GSM_CHAR)
self.assertEqual(len(sms.to_pdu()), 2)
def test_unicode_3(self):
sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 2)
self.assertEqual(len(sms.to_pdu()), 2)
def test_unicode_4(self):
sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 2 + self.GSM_CHAR)
self.assertEqual(len(sms.to_pdu()), 3)
def test_unicode_5(self):
sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 3)
self.assertEqual(len(sms.to_pdu()), 3)
def test_unicode_6(self):
sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 3 + self.GSM_CHAR)
self.assertEqual(len(sms.to_pdu()), 4)
class TestSmsDeliver(unittest.TestCase):
def test_decoding_7bit_pdu(self):
pdu = "07911326040000F0040B911346610089F60000208062917314080CC8F71D14969741F977FD07"
text = "How are you?"
csca = "+31624000000"
number = '2b3331363431363030393836'.decode('hex')
sms = SmsDeliver(pdu)
self.assertEqual(sms.text, text)
self.assertEqual(sms.csca, csca)
self.assertEqual(sms.number, number)
def test_decoding_ucs2_pdu(self):
pdu = "07914306073011F0040B914316709807F2000880604290224080084E2D5174901A8BAF"
text = u"中兴通讯"
csca = "+34607003110"
number = '2b3334363130373839373032'.decode('hex')
sms = SmsDeliver(pdu)
self.assertEqual(sms.text, text)
self.assertEqual(sms.csca, csca)
self.assertEqual(sms.number, number)
def test_decoding_7bit_pdu_data(self):
pdu = "07911326040000F0040B911346610089F60000208062917314080CC8F71D14969741F977FD07"
text = "How are you?"
csca = "+31624000000"
number = '2b3331363431363030393836'.decode('hex')
data = SmsDeliver(pdu).data
self.assertEqual(data['text'], text)
self.assertEqual(data['csca'], csca)
self.assertEqual(data['number'], number)
self.assertEqual(data['pid'], 0)
self.assertEqual(data['fmt'], 0)
self.assertEqual(data['date'], datetime(2002, 8, 26, 19, 37, 41))
def test_decoding_datetime_gmtplusone(self):
pdu = "0791447758100650040C914497716247010000909010711423400A2050EC468B81C4733A"
text = " 1741 bst"
number = '2b343437393137323637343130'.decode('hex')
date = datetime(2009, 9, 1, 16, 41, 32)
sms = SmsDeliver(pdu)
self.assertEqual(sms.text, text)
self.assertEqual(sms.number, number)
self.assertEqual(sms.date, date)
def test_decoding_datetime_gmtminusthree(self):
pdu = "0791553001000001040491578800000190115101112979CF340B342F9FEBE536E83D0791C3E4F71C440E83E6F53068FE66A7C7697A781C7EBB4050F99BFE1EBFD96F1D48068BC16030182E66ABD560B41988FC06D1D3F03768FA66A7C7697A781C7E83CCEF34282C2ECBE96F50B90D8AC55EB0DC4B068BC140B1994E16D3D1622E"
date = datetime(2010, 9, 11, 18, 10, 11) # 11/09/10 15:10 GMT-3.00
sms = SmsDeliver(pdu)
self.assertEqual(sms.date, date)
def test_decoding_number_alphanumeric(self):
# Odd length test
pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701B"
number = "FONIC"
text = "Lieber FONIC Kunde, die Tarifoption Internet-Tagesflatrate wurde aktiviert. Internet-Nutzung wird jetzt pro Nutzungstag abgerechnet. Ihr FONIC Team"
csca = "+491760000443"
sms = SmsDeliver(pdu)
self.assertEqual(sms.text, text)
self.assertEqual(sms.csca, csca)
self.assertEqual(sms.number, number)
# Even length test
pdu = "07919333852804000412D0F7FBDD454FB75D693A0000903002801153402BCD301E9F0605D9E971191483C140412A35690D52832063D2F9040599A058EE05A3BD6430580E"
number = "www.tim.it"
text = 'Maxxi Alice 100 ATTIVATA FINO AL 19/04/2009'
csca = '+393358824000'
sms = SmsDeliver(pdu)
self.assertEqual(sms.text, text)
self.assertEqual(sms.csca, csca)
self.assertEqual(sms.number, number)
def test_decode_sms_confirmation(self):
pdu = "07914306073011F006270B913426565711F7012081111345400120811174054043"
csca = "+34607003110"
date = datetime(2010, 2, 18, 11, 31, 54)
number = "SR-UNKNOWN"
# XXX: the number should be +344626575117, is the prefix flipped ?
text = "+43626575117|10/02/18 11:31:54|"
sms = SmsDeliver(pdu)
self.assertEqual(sms.text, text)
self.assertEqual(sms.csca, csca)
self.assertEqual(sms.number, number)
self.assertEqual(sms.date, date)
def test_decode_weird_multipart_german_pdu(self):
pdus = [
"07919471227210244405852122F039F101506271217180A005000319020198E9B2B82C0759DFE4B0F9ED2EB7967537B9CC02B5D37450122D2FCB41EE303DFD7687D96537881A96A7CD6F383DFD7683F46134BBEC064DD36550DA0D22A7CBF3721BE42CD3F5A0198B56036DCA20B8FC0D6A0A4170767D0EAAE540433A082E7F83A6E5F93CFD76BB40D7B2DB0D9AA6CB2072BA3C2F83926EF31BE44E8FD17450BB8C9683CA",
"07919471227210244405852122F039F1015062712181804F050003190202E4E8309B5E7683DAFC319A5E76B340F73D9A5D7683A6E93268FD9ED3CB6EF67B0E5AD172B19B2C2693C9602E90355D6683A6F0B007946E8382F5393BEC26BB00",
]
texts = [
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",
u"rhalten möchten, wählen Sie kostenlos +4917212220. Viel Spaß im Ausland.",
]
for i, sms in enumerate(map(SmsDeliver, pdus)):
self.assertEqual(sms.text, texts[i])
self.assertEqual(sms.udh.concat.cnt, len(pdus))
self.assertEqual(sms.udh.concat.seq, i + 1)
self.assertEqual(sms.udh.concat.ref, 25)
def test_decoding_odd_length_pdu_strict_raises_valueerror(self):
# same pdu as in test_decoding_number_alpha1 minus last char
pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701"
self.assertRaises(ValueError, SmsDeliver, pdu)
def test_decoding_odd_length_pdu_no_strict(self):
# same pdu as in test_decoding_number_alpha1 minus last char
pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701"
text = "Lieber FONIC Kunde, die Tarifoption Internet-Tagesflatrate wurde aktiviert. Internet-Nutzung wird jetzt pro Nutzungstag abgerechnet. Ihr FONIC Tea"
sms = SmsDeliver(pdu, strict=False)
self.assertEqual(sms.text, text)
def test_decoding_delivery_status_report(self):
pdu = "0791538375000075061805810531F1019082416500400190824165004000"
sr = {
'status': 0,
'scts': datetime(2010, 9, 28, 14, 56),
'dt': datetime(2010, 9, 28, 14, 56),
'recipient': '50131'
}
sms = SmsDeliver(pdu)
self.assertEqual(sms.csca, "+353857000057")
data = sms.data
self.assertEqual(data['ref'], 24)
self.assertEqual(sms.sr, sr)
def test_decoding_delivery_status_report_without_smsc_address(self):
pdu = "00060505810531F1010150610000400101506100004000"
sr = {
'status': 0,
'scts': datetime(2010, 10, 5, 16, 0),
'dt': datetime(2010, 10, 5, 16, 0),
'recipient': '50131'
}
sms = SmsDeliver(pdu)
self.assertEqual(sms.csca, None)
data = sms.data
self.assertEqual(data['ref'], 5)
self.assertEqual(sms.sr, sr)
# XXX: renable when support added
# def test_decoding_submit_status_report(self):
# # sent from SMSC to indicate submission failed or additional info
# pdu = "07914306073011F001000B914306565711F9000007F0B2FC0DCABF01"
# csca = "+34607003110"
# number = "SR-UNKNOWN"
#
# sms = SmsDeliver(pdu)
# self.assertEqual(sms.csca, csca)
# self.assertEqual(sms.number, number)

24
messaging/test/test_udh.py

@ -0,0 +1,24 @@
import unittest
from messaging.sms.udh import UserDataHeader
from messaging.utils import to_array
class TestUserDataHeader(unittest.TestCase):
def test_user_data_header(self):
data = to_array("08049f8e020105040b8423f0")
udh = UserDataHeader.from_bytes(data)
self.assertEqual(udh.concat.seq, 1)
self.assertEqual(udh.concat.cnt, 2)
self.assertEqual(udh.concat.ref, 40846)
self.assertEqual(udh.ports.dest_port, 2948)
self.assertEqual(udh.ports.orig_port, 9200)
data = to_array("0003190201")
udh = UserDataHeader.from_bytes(data)
self.assertEqual(udh.concat.seq, 1)
self.assertEqual(udh.concat.cnt, 2)
self.assertEqual(udh.concat.ref, 25)

140
messaging/test/test_wap.py

@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
from array import array
import unittest
from messaging.sms import SmsDeliver
from messaging.sms.wap import (is_a_wap_push_notification as is_push,
is_mms_notification,
extract_push_notification)
def list_to_str(l):
a = array("B", l)
return a.tostring()
class TestSmsWapPush(unittest.TestCase):
data = [1, 6, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111,
110, 47, 118, 110, 100, 46, 119, 97, 112, 46, 109, 109, 115, 45,
109, 101, 115, 115, 97, 103, 101, 0, 175, 132, 140, 130, 152, 78,
79, 75, 53, 67, 105, 75, 99, 111, 84, 77, 89, 83, 71, 52, 77, 66,
83, 119, 65, 65, 115, 75, 118, 49, 52, 70, 85, 72, 65, 65, 65, 65,
65, 65, 65, 65, 0, 141, 144, 137, 25, 128, 43, 52, 52, 55, 55, 56,
53, 51, 52, 50, 55, 52, 57, 47, 84, 89, 80, 69, 61, 80, 76, 77, 78,
0, 138, 128, 142, 2, 116, 0, 136, 5, 129, 3, 1, 25, 64, 131, 104,
116, 116, 112, 58, 47, 47, 112, 114, 111, 109, 109, 115, 47, 115,
101, 114, 118, 108, 101, 116, 115, 47, 78, 79, 75, 53, 67, 105, 75,
99, 111, 84, 77, 89, 83, 71, 52, 77, 66, 83, 119, 65, 65, 115, 75,
118, 49, 52, 70, 85, 72, 65, 65, 65, 65, 65, 65, 65, 65, 0]
def test_is_a_wap_push_notification(self):
self.assertTrue(is_push(list_to_str(self.data)))
self.assertTrue(is_push(list_to_str([1, 6, 57, 92, 45])))
self.assertFalse(is_push(list_to_str([4, 5, 57, 92, 45])))
self.assertRaises(TypeError, is_push, 1)
def test_decoding_m_notification_ind(self):
pdus = [
"0791447758100650400E80885810000000810004016082415464408C0C08049F8E020105040B8423F00106226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82984E4F4B3543694B636F544D595347344D4253774141734B7631344655484141414141414141008D908919802B3434373738353334323734392F545950453D504C4D4E008A808E0274008805810301194083687474703A2F",
"0791447758100650440E8088581000000081000401608241547440440C08049F8E020205040B8423F02F70726F6D6D732F736572766C6574732F4E4F4B3543694B636F544D595347344D4253774141734B763134465548414141414141414100",
]
number = '3838383530313030303030303138'.decode('hex')
csca = "+447785016005"
data = ""
sms = SmsDeliver(pdus[0])
self.assertEqual(sms.udh.concat.ref, 40846)
self.assertEqual(sms.udh.concat.cnt, 2)
self.assertEqual(sms.udh.concat.seq, 1)
self.assertEqual(sms.number, number)
self.assertEqual(sms.csca, csca)
data += sms.text
sms = SmsDeliver(pdus[1])
self.assertEqual(sms.udh.concat.ref, 40846)
self.assertEqual(sms.udh.concat.cnt, 2)
self.assertEqual(sms.udh.concat.seq, 2)
self.assertEqual(sms.number, number)
data += sms.text
mms = extract_push_notification(data)
self.assertEqual(is_mms_notification(mms), True)
self.assertEqual(mms.headers['Message-Type'], 'm-notification-ind')
self.assertEqual(mms.headers['Transaction-Id'],
'NOK5CiKcoTMYSG4MBSwAAsKv14FUHAAAAAAAA')
self.assertEqual(mms.headers['MMS-Version'], '1.0')
self.assertEqual(mms.headers['From'],
'2b3434373738353334323734392f545950453d504c4d4e'.decode('hex'))
self.assertEqual(mms.headers['Message-Class'], 'Personal')
self.assertEqual(mms.headers['Message-Size'], 29696)
self.assertEqual(mms.headers['Expiry'], 72000)
self.assertEqual(mms.headers['Content-Location'],
'http://promms/servlets/NOK5CiKcoTMYSG4MBSwAAsKv14FUHAAAAAAAA')
pdus = [
"0791447758100650400E80885810000000800004017002314303408C0C0804DFD3020105040B8423F00106226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82984E4F4B3541315A6446544D595347344F3356514141734A763934476F4E4141414141414141008D908919802B3434373731373237353034392F545950453D504C4D4E008A808E0274008805810303F47F83687474703A2F",
"0791447758100650440E8088581000000080000401700231431340440C0804DFD3020205040B8423F02F70726F6D6D732F736572766C6574732F4E4F4B3541315A6446544D595347344F3356514141734A763934476F4E414141414141414100",
]
number = "88850100000008"
data = ""
sms = SmsDeliver(pdus[0])
self.assertEqual(sms.udh.concat.ref, 57299)
self.assertEqual(sms.udh.concat.cnt, 2)
self.assertEqual(sms.udh.concat.seq, 1)
self.assertEqual(sms.number, number)
data += sms.text
sms = SmsDeliver(pdus[1])
self.assertEqual(sms.udh.concat.ref, 57299)
self.assertEqual(sms.udh.concat.cnt, 2)
self.assertEqual(sms.udh.concat.seq, 2)
self.assertEqual(sms.number, number)
data += sms.text
mms = extract_push_notification(data)
self.assertEqual(is_mms_notification(mms), True)
self.assertEqual(mms.headers['Message-Type'], 'm-notification-ind')
self.assertEqual(mms.headers['Transaction-Id'],
'NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA')
self.assertEqual(mms.headers['MMS-Version'], '1.0')
self.assertEqual(mms.headers['From'],
'2b3434373731373237353034392f545950453d504c4d4e'.decode('hex'))
self.assertEqual(mms.headers['Message-Class'], 'Personal')
self.assertEqual(mms.headers['Message-Size'], 29696)
self.assertEqual(mms.headers['Expiry'], 259199)
self.assertEqual(mms.headers['Content-Location'],
'http://promms/servlets/NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA')
def test_decoding_generic_wap_push(self):
pdus = [
"0791947122725014440C8500947122921105F5112042519582408C0B05040B8423F0000396020101060B03AE81EAC3958D01A2B48403056A0A20566F6461666F6E650045C60C037761702E6D65696E63616C6C79612E64652F000801035A756D206B6F7374656E6C6F73656E20506F7274616C20224D65696E0083000322202D2065696E66616368206175662064656E20666F6C67656E64656E204C696E6B206B6C69636B656E",
"0791947122725014440C8500947122921105F5112042519592403C0B05040B8423F00003960202206F6465722064696520536569746520646972656B7420617566727566656E2E2049687200830003205465616D000101",
]
number = '303034393137323232393131'.decode('hex')
csca = "+491722270541"
data = ""
sms = SmsDeliver(pdus[0])
self.assertEqual(sms.udh.concat.ref, 150)
self.assertEqual(sms.udh.concat.cnt, 2)
self.assertEqual(sms.udh.concat.seq, 1)
self.assertEqual(sms.number, number)
self.assertEqual(sms.csca, csca)
data += sms.text
sms = SmsDeliver(pdus[1])
self.assertEqual(sms.udh.concat.ref, 150)
self.assertEqual(sms.udh.concat.cnt, 2)
self.assertEqual(sms.udh.concat.seq, 2)
self.assertEqual(sms.number, number)
data += sms.text
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')
push = extract_push_notification(data)
self.assertEqual(is_mms_notification(push), False)

267
messaging/utils.py

@ -0,0 +1,267 @@
from array import array
from datetime import timedelta, tzinfo
from math import floor
import sys
class FixedOffset(tzinfo):
"""Fixed offset in minutes east from UTC."""
def __init__(self, offset, name):
if isinstance(offset, timedelta):
self.offset = offset
else:
self.offset = timedelta(minutes=offset)
self.__name = name
@classmethod
def from_timezone(cls, tz_str, name):
# no timezone, GMT+3, GMT-3
# '', '+0330', '-0300'
if not tz_str:
return cls(timedelta(0), name)
sign = 1 if '+' in tz_str else -1
offset = tz_str.replace('+', '').replace('-', '')
hours, minutes = int(offset[:2]), int(offset[2:])
minutes += hours * 60
if sign == 1:
td = timedelta(minutes=minutes)
elif sign == -1:
td = timedelta(days=-1, minutes=minutes)
return cls(td, name)
def utcoffset(self, dt):
return self.offset
def tzname(self, dt):
return self.__name
def dst(self, dt):
return timedelta(0)
def bytes_to_str(b):
if sys.version_info >= (3,):
return b.decode('latin1')
return b
def to_array(pdu):
return array('B', [int(pdu[i:i + 2], 16) for i in range(0, len(pdu), 2)])
def to_bytes(s):
if sys.version_info >= (3,):
return bytes(s)
return ''.join(map(chr, s))
def debug(s):
# set this to True if you want to poke at PDU encoding/decoding
if False:
print(s)
def swap(s):
"""Swaps ``s`` according to GSM 23.040"""
what = s[:]
for n in range(1, len(what), 2):
what[n - 1], what[n] = what[n], what[n - 1]
return what
def swap_number(n):
data = swap(list(n.replace('f', '')))
return ''.join(data)
def clean_number(n):
return n.strip().replace(' ', '')
def encode_str(s):
"""Returns the hexadecimal representation of ``s``"""
return ''.join(["%02x" % ord(n) for n in s])
def encode_bytes(b):
return ''.join(["%02x" % n for n in b])
def pack_8bits_to_7bits(message, udh=None):
pdu = ""
txt = bytes_to_str(message)
if udh is None:
tl = len(txt)
txt += '\x00'
msgl = int(len(txt) * 7 / 8)
op = [-1] * msgl
c = shift = 0
for n in range(msgl):
if shift == 6:
c += 1
shift = n % 7
lb = ord(txt[c]) >> shift
hb = (ord(txt[c + 1]) << (7 - shift) & 255)
op[n] = lb + hb
c += 1
pdu = chr(tl) + ''.join(map(chr, op))
else:
txt = "\x00\x00\x00\x00\x00\x00" + txt
tl = len(txt)
txt += '\x00'
msgl = int(len(txt) * 7 / 8)
op = [-1] * msgl
c = shift = 0
for n in range(msgl):
if shift == 6:
c += 1
shift = n % 7
lb = ord(txt[c]) >> shift
hb = (ord(txt[c + 1]) << (7 - shift) & 255)
op[n] = lb + hb
c += 1
for i, char in enumerate(udh):
op[i] = ord(char)
pdu = chr(tl) + ''.join(map(chr, op))
return encode_str(pdu)
def pack_8bits_to_8bit(message, udh=None):
text = message
if udh is not None:
text = udh + text
mlen = len(text)
message = chr(mlen) + message
return encode_str(message)
def pack_8bits_to_ucs2(message, udh=None):
# XXX: This does not control the size respect to UDH
text = message
nmesg = ''
if udh is not None:
text = udh + text
for n in text:
nmesg += chr(ord(n) >> 8) + chr(ord(n) & 0xFF)
mlen = len(text) * 2
message = chr(mlen) + nmesg
return encode_str(message)
def unpack_msg(pdu):
"""Unpacks ``pdu`` into septets and returns the decoded string"""
# Taken/modified from Dave Berkeley's pysms package
count = last = 0
result = []
for i in range(0, len(pdu), 2):
byte = int(pdu[i:i + 2], 16)
mask = 0x7F >> count
out = ((byte & mask) << count) + last
last = byte >> (7 - count)
result.append(out)
if len(result) >= 0xa0:
break
if count == 6:
result.append(last)
last = 0
count = (count + 1) % 7
return to_bytes(result)
def unpack_msg2(pdu):
"""Unpacks ``pdu`` into septets and returns the decoded string"""
# Taken/modified from Dave Berkeley's pysms package
count = last = 0
result = []
for byte in pdu:
mask = 0x7F >> count
out = ((byte & mask) << count) + last
last = byte >> (7 - count)
result.append(out)
if len(result) >= 0xa0:
break
if count == 6:
result.append(last)
last = 0
count = (count + 1) % 7
return to_bytes(result)
def timedelta_to_relative_validity(t):
"""
Convert ``t`` to its relative validity period
In case the resolution of ``t`` is too small for a time unit,
it will be floor-rounded to the previous sane value
:type t: datetime.timedelta
:return int
"""
if t < timedelta(minutes=5):
raise ValueError("Min resolution is five minutes")
if t > timedelta(weeks=63):
raise ValueError("Max validity is 63 weeks")
if t <= timedelta(hours=12):
return int(floor(t.seconds / (60 * 5))) - 1
if t <= timedelta(hours=24):
t -= timedelta(hours=12)
return int(floor(t.seconds / (60 * 30))) + 143
if t <= timedelta(days=30):
return t.days + 166
if t <= timedelta(weeks=63):
return int(floor(t.days / 7)) + 192
def datetime_to_absolute_validity(d, tzname='Unknown'):
"""Convert ``d`` to its integer representation"""
n = d.strftime("%y %m %d %H %M %S %z").split(" ")
# compute offset
offset = FixedOffset.from_timezone(n[-1], tzname).offset
# one unit is 15 minutes
s = "%02d" % int(floor(offset.seconds / (60 * 15)))
if offset.days < 0:
# set MSB to 1
s = "%02x" % ((int(s[0]) << 4) | int(s[1]) | 0x80)
n[-1] = s
return [int(c[::-1], 16) for c in n]

1
requirements.txt

@ -18,3 +18,4 @@ pinax-theme-bootstrap
django-bootstrap3
requests
webdavclient
pyst2

12
systemd_units/djing_dial.service

@ -0,0 +1,12 @@
[Unit]
Description=Dialing inbox sms unit
[Service]
Type=simple
ExecStart=/usr/bin/python3 dialing.py
WorkingDirectory=/srv/http/djing
User=http
Group=http
[Install]
WantedBy=multi-user.target
Loading…
Cancel
Save