46 changed files with 6660 additions and 30 deletions
-
141dialing.py
-
77dialing_app/locale/ru/LC_MESSAGES/django.po
-
35dialing_app/migrations/0002_auto_20171229_1353.py
-
18dialing_app/models.py
-
9dialing_app/templates/ext.html
-
30dialing_app/templates/inbox_sms.html
-
3dialing_app/urls.py
-
11dialing_app/views.py
-
2djing/settings_example.py
-
3messaging/__init__.py
-
63messaging/mms/__init__.py
-
71messaging/mms/iterator.py
-
555messaging/mms/message.py
-
996messaging/mms/mms_pdu.py
-
2055messaging/mms/wsp_pdu.py
-
7messaging/sms/__init__.py
-
14messaging/sms/base.py
-
17messaging/sms/consts.py
-
264messaging/sms/deliver.py
-
292messaging/sms/gsm0338.py
-
10messaging/sms/pdu.py
-
330messaging/sms/submit.py
-
79messaging/sms/udh.py
-
38messaging/sms/wap.py
-
0messaging/test/__init__.py
-
BINmessaging/test/mms-data/27d0a048cd79555de05283a22372b0eb.mms
-
BINmessaging/test/mms-data/BTMMS.MMS
-
BINmessaging/test/mms-data/NOWMMS.MMS
-
BINmessaging/test/mms-data/SEC-SGHS300M.mms
-
BINmessaging/test/mms-data/SIMPLE.MMS
-
BINmessaging/test/mms-data/SonyEricssonT310-R201.mms
-
BINmessaging/test/mms-data/TOMSLOT.MMS
-
BINmessaging/test/mms-data/gallery2test.mms
-
BINmessaging/test/mms-data/iPhone.mms
-
BINmessaging/test/mms-data/images_are_cut_off_debug.mms
-
BINmessaging/test/mms-data/m.mms
-
BINmessaging/test/mms-data/openwave.mms
-
BINmessaging/test/mms-data/projekt_exempel.mms
-
267messaging/test/test_gsm_encoding.py
-
378messaging/test/test_mms.py
-
481messaging/test/test_sms.py
-
24messaging/test/test_udh.py
-
140messaging/test/test_wap.py
-
267messaging/utils.py
-
1requirements.txt
-
12systemd_units/djing_dial.service
@ -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() |
||||
@ -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}, |
||||
|
), |
||||
|
] |
||||
@ -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 %} |
||||
@ -0,0 +1,3 @@ |
|||||
|
# see LICENSE |
||||
|
|
||||
|
VERSION = (0, 5, 12) |
||||
@ -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} |
||||
|
""" |
||||
@ -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 |
||||
@ -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 '' |
||||
@ -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
File diff suppressed because it is too large
View File
@ -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"] |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
|
} |
||||
|
|
||||
@ -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 |
||||
@ -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 |
||||
@ -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) |
||||
@ -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 |
||||
@ -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,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) |
||||
@ -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) |
||||
@ -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) |
||||
@ -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) |
||||
@ -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) |
||||
@ -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] |
||||
@ -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 |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue