From {{ msg.who }}
+ {{ msg.when|date:'d M, H:i:s' }} via {{ msg.dev }}
+
+
+
{{ msg.text }}
+
+
+ {% empty %}
+
+
{% trans 'Message history is empty' %}
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/dialing_app/urls.py b/dialing_app/urls.py
index 6dc3fe8..ce611b4 100644
--- a/dialing_app/urls.py
+++ b/dialing_app/urls.py
@@ -7,5 +7,6 @@ urlpatterns = [
url(r'^filter$', views.vfilter, name='vfilter'),
url(r'^to_abon(?P\+?\d+)$', views.to_abon, name='to_abon'),
url(r'^requests$', views.vmail_request, name='vmail_request'),
- url(r'^reports$', views.vmail_report, name='vmail_report')
+ url(r'^reports$', views.vmail_report, name='vmail_report'),
+ url(r'^sms/in$', views.inbox_sms, name='inbox_sms')
]
diff --git a/dialing_app/views.py b/dialing_app/views.py
index 4386a07..c8c16b8 100644
--- a/dialing_app/views.py
+++ b/dialing_app/views.py
@@ -7,7 +7,7 @@ from django.db.models import Q
from abonapp.models import Abon
from mydefs import only_admins, pag_mn
-from .models import AsteriskCDR
+from .models import AsteriskCDR, SMSModel
@login_required
@@ -75,3 +75,12 @@ def vfilter(request):
's': s
})
+
+@login_required
+@permission_required('dialing_app.can_view_sms')
+def inbox_sms(request):
+ msgs = SMSModel.objects.all()
+ msgs = pag_mn(request, msgs)
+ return render(request, 'inbox_sms.html', {
+ 'sms_messages': msgs
+ })
diff --git a/djing/settings_example.py b/djing/settings_example.py
index 0931626..8a8a929 100644
--- a/djing/settings_example.py
+++ b/djing/settings_example.py
@@ -181,3 +181,5 @@ DEFAULT_SNMP_PASSWORD = 'public'
TELEGRAM_BOT_TOKEN = 'bot token'
TELEPHONE_REGEXP = r'^\+[7,8,9,3]\d{10,11}$'
+
+DJING_USERNAME_PASSWORD = ('username', 'secret')
diff --git a/messaging/__init__.py b/messaging/__init__.py
new file mode 100644
index 0000000..5e9a3bc
--- /dev/null
+++ b/messaging/__init__.py
@@ -0,0 +1,3 @@
+# see LICENSE
+
+VERSION = (0, 5, 12)
diff --git a/messaging/mms/__init__.py b/messaging/mms/__init__.py
new file mode 100644
index 0000000..738c8ee
--- /dev/null
+++ b/messaging/mms/__init__.py
@@ -0,0 +1,63 @@
+# This library is free software.
+#
+# It was originally distributed under the terms of the GNU Lesser
+# General Public License Version 2.
+#
+# python-messaging opts to apply the terms of the ordinary GNU
+# General Public License v2, as permitted by section 3 of the LGPL
+# v2.1. This re-licensing allows the entirety of python-messaging to
+# be distributed according to the terms of GPL-2.
+#
+# See the COPYING file included in this archive
+#
+# Copyright (C) 2007 Francois Aucamp
+#
+"""
+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{}
+@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}
+"""
diff --git a/messaging/mms/iterator.py b/messaging/mms/iterator.py
new file mode 100644
index 0000000..cd49505
--- /dev/null
+++ b/messaging/mms/iterator.py
@@ -0,0 +1,71 @@
+# This library is free software.
+#
+# It was originally distributed under the terms of the GNU Lesser
+# General Public License Version 2.
+#
+# python-messaging opts to apply the terms of the ordinary GNU
+# General Public License v2, as permitted by section 3 of the LGPL
+# v2.1. This re-licensing allows the entirety of python-messaging to
+# be distributed according to the terms of GPL-2.
+#
+# See the COPYING file included in this archive
+#
+# The docstrings in this module contain epytext markup; API documentation
+# may be created by processing this file with epydoc: http://epydoc.sf.net
+"""Iterator with "value preview" capability."""
+
+
+class PreviewIterator(object):
+ """An ``iter`` wrapper class providing a "previewable" iterator.
+
+ This "preview" functionality allows the iterator to return successive
+ values from its ``iterable`` object, without actually mvoving forward
+ itself. This is very usefuly if the next item(s) in an iterator must
+ be used for something, after which the iterator should "undo" those
+ read operations, so that they can be read again by another function.
+
+ From the user point of view, this class supersedes the builtin iter()
+ function: like iter(), it is called as PreviewIter(iterable).
+ """
+ def __init__(self, data):
+ self._it = iter(data)
+ self._cached_values = []
+ self._preview_pos = 0
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ self.reset_preview()
+ if len(self._cached_values) > 0:
+ return self._cached_values.pop(0)
+ else:
+ return self._it.next()
+
+ def preview(self):
+ """
+ Return the next item in the ``iteratable`` object
+
+ But it does not modify the actual iterator (i.e. do not
+ intefere with :func:`next`.
+
+ Successive calls to :func:`preview` will return successive values from
+ the ``iterable`` object, exactly in the same way :func:`next` does.
+
+ However, :func:`preview` will always return the next item from
+ ``iterable`` after the item returned by the previous :func:`preview`
+ or :func:`next` call, whichever was called the most recently.
+ To force the "preview() iterator" to synchronize with the "next()
+ iterator" (without calling :func:`next`), use :func:`reset_preview`.
+ """
+ if self._preview_pos < len(self._cached_values):
+ value = self._cached_values[self._preview_pos]
+ else:
+ value = self._it.next()
+ self._cached_values.append(value)
+
+ self._preview_pos += 1
+ return value
+
+ def reset_preview(self):
+ self._preview_pos = 0
diff --git a/messaging/mms/message.py b/messaging/mms/message.py
new file mode 100644
index 0000000..6cff8a0
--- /dev/null
+++ b/messaging/mms/message.py
@@ -0,0 +1,555 @@
+# This library is free software.
+#
+# It was originally distributed under the terms of the GNU Lesser
+# General Public License Version 2.
+#
+# python-messaging opts to apply the terms of the ordinary GNU
+# General Public License v2, as permitted by section 3 of the LGPL
+# v2.1. This re-licensing allows the entirety of python-messaging to
+# be distributed according to the terms of GPL-2.
+#
+# See the COPYING file included in this archive
+#
+# The docstrings in this module contain epytext markup; API documentation
+# may be created by processing this file with epydoc: http://epydoc.sf.net
+"""High-level MMS message classes"""
+
+from __future__ import with_statement
+import array
+import mimetypes
+import os
+import xml.dom.minidom
+
+
+class MMSMessage:
+ """
+ I am an MMS message
+
+ References used in this class: [1][2][3][4][5]
+ """
+ def __init__(self):
+ self._pages = []
+ self._data_parts = []
+ self._metaTags = {}
+ self._mms_message = None
+ self.headers = {
+ 'Message-Type': 'm-send-req',
+ 'Transaction-Id': '1234',
+ 'MMS-Version': '1.0',
+ 'Content-Type': ('application/vnd.wap.multipart.mixed', {}),
+ }
+ self.width = 176
+ self.height = 220
+ self.transactionID = '12345'
+ self.subject = 'test'
+
+ @property
+ def content_type(self):
+ """
+ Returns the Content-Type of this data part header
+
+ No parameter information is returned; to get that, access the
+ "Content-Type" header directly (which has a tuple value) from
+ the message's ``headers`` attribute.
+
+ This is equivalent to calling DataPart.headers['Content-Type'][0]
+ """
+ return self.headers['Content-Type'][0]
+
+ def add_page(self, page):
+ """
+ Adds `page` to the message
+
+ :type page: MMSMessagePage
+ :param page: The message slide/page to add
+ """
+ if self.content_type != 'application/vnd.wap.multipart.related':
+ value = ('application/vnd.wap.multipart.related', {})
+ self.headers['Content-Type'] = value
+
+ self._pages.append(page)
+
+ @property
+ def pages(self):
+ """Returns a list of all the pages in this message"""
+ return self._pages
+
+ def add_data_part(self, data_part):
+ """Adds a single data part (DataPart object) to the message, without
+ connecting it to a specific slide/page in the message.
+
+ A data part encapsulates some form of attachment, e.g. an image, audio
+ etc. It is not necessary to explicitly add data parts to the message
+ using this function if :func:`add_page` is used; this method is mainly
+ useful if you want to create MMS messages without SMIL support,
+ i.e. messages of type "application/vnd.wap.multipart.mixed"
+
+ :param data_part: The data part to add
+ :type data_part: DataPart
+ """
+ self._data_parts.append(data_part)
+
+ @property
+ def data_parts(self):
+ """
+ Returns a list of all the data parts in this message
+
+ including data parts that were added to slides in this message"""
+ parts = []
+ if len(self._pages):
+ parts.append(self.smil())
+ for slide in self._mms_message._pages:
+ parts.extend(slide.data_parts())
+
+ parts.extend(self._data_parts)
+ return parts
+
+ def smil(self):
+ """Returns the text of the message's SMIL file"""
+ impl = xml.dom.minidom.getDOMImplementation()
+ smil_doc = impl.createDocument(None, "smil", None)
+
+ # Create the SMIL header
+ head_node = smil_doc.createElement('head')
+ # Add metadata to header
+ for tag_name in self._metaTags:
+ meta_node = smil_doc.createElement('meta')
+ meta_node.setAttribute(tag_name, self._metaTags[tag_name])
+ head_node.appendChild(meta_node)
+
+ # Add layout info to header
+ layout_node = smil_doc.createElement('layout')
+ root_layout_node = smil_doc.createElement('root-layout')
+ root_layout_node.setAttribute('width', str(self.width))
+ root_layout_node.setAttribute('height', str(self.height))
+ layout_node.appendChild(root_layout_node)
+
+ areas = (('Image', '0', '0', '176', '144'),
+ ('Text', '176', '144', '176', '76'))
+
+ for region_id, left, top, width, height in areas:
+ region_node = smil_doc.createElement('region')
+ region_node.setAttribute('id', region_id)
+ region_node.setAttribute('left', left)
+ region_node.setAttribute('top', top)
+ region_node.setAttribute('width', width)
+ region_node.setAttribute('height', height)
+ layout_node.appendChild(region_node)
+
+ head_node.appendChild(layout_node)
+ smil_doc.documentElement.appendChild(head_node)
+
+ # Create the SMIL body
+ body_node = smil_doc.createElement('body')
+ # Add pages to body
+ for page in self._pages:
+ par_node = smil_doc.createElement('par')
+ par_node.setAttribute('duration', str(page.duration))
+ # Add the page content information
+ if page.image is not None:
+ #TODO: catch unpack exception
+ part, begin, end = page.image
+ if 'Content-Location' in part.headers:
+ src = part.headers['Content-Location']
+ elif 'Content-ID' in part.headers:
+ src = part.headers['Content-ID']
+ else:
+ src = part.data
+
+ image_node = smil_doc.createElement('img')
+ image_node.setAttribute('src', src)
+ image_node.setAttribute('region', 'Image')
+ if begin > 0 or end > 0:
+ if end > page.duration:
+ end = page.duration
+
+ image_node.setAttribute('begin', str(begin))
+ image_node.setAttribute('end', str(end))
+
+ par_node.appendChild(image_node)
+
+ if page.text is not None:
+ part, begin, end = page.text
+ src = part.data
+ text_node = smil_doc.createElement('text')
+ text_node.setAttribute('src', src)
+ text_node.setAttribute('region', 'Text')
+ if begin > 0 or end > 0:
+ if end > page.duration:
+ end = page.duration
+
+ text_node.setAttribute('begin', str(begin))
+ text_node.setAttribute('end', str(end))
+
+ par_node.appendChild(text_node)
+
+ if page.audio is not None:
+ part, begin, end = page.audio
+ if 'Content-Location' in part.headers:
+ src = part.headers['Content-Location']
+ elif 'Content-ID' in part.headers:
+ src = part.headers['Content-ID']
+ else:
+ src = part.data
+
+ audio_node = smil_doc.createElement('audio')
+ audio_node.setAttribute('src', src)
+ if begin > 0 or end > 0:
+ if end > page.duration:
+ end = page.duration
+
+ audio_node.setAttribute('begin', str(begin))
+ audio_node.setAttribute('end', str(end))
+
+ par_node.appendChild(text_node)
+ par_node.appendChild(audio_node)
+
+ body_node.appendChild(par_node)
+
+ smil_doc.documentElement.appendChild(body_node)
+ return smil_doc.documentElement.toprettyxml()
+
+ def encode(self):
+ """
+ Return a binary representation of this MMS message
+
+ This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally
+
+ :return: The binary-encoded MMS data, as an array of bytes
+ :rtype: array.array('B')
+ """
+ from messaging.mms import mms_pdu
+ encoder = mms_pdu.MMSEncoder()
+ return encoder.encode(self)
+
+ def to_file(self, filename):
+ """
+ Writes this MMS message to `filename` in binary-encoded form
+
+ This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally
+
+ :param filename: The path where to store the message data
+ :type filename: str
+
+ :rtype array.array('B')
+ :return: The binary-encode MMS data, as an array of bytes
+ """
+ with open(filename, 'wb') as f:
+ self.encode().tofile(f)
+
+ @staticmethod
+ def from_data(data):
+ """
+ Returns a new `:class:MMSMessage` out of ``data``
+
+ This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally
+
+ :param data: The data to load
+ :type filename: array.array
+ """
+ from messaging.mms import mms_pdu
+ decoder = mms_pdu.MMSDecoder()
+ return decoder.decode_data(data)
+
+ @staticmethod
+ def from_file(filename):
+ """
+ Returns a new `:class:MMSMessage` out of file ``filename``
+
+ This uses the `~:class:messaging.mms.mms_pdu.MMSEncoder` internally
+
+ :param filename: The name of the file to load
+ :type filename: str
+ """
+ from messaging.mms import mms_pdu
+ decoder = mms_pdu.MMSDecoder()
+ return decoder.decode_file(filename)
+
+
+class MMSMessagePage:
+ """
+ A single page/slide in an MMS Message.
+
+ In order to ensure that the MMS message can be correctly displayed by most
+ terminals, each page's content is limited to having 1 image, 1 audio clip
+ and 1 block of text, as stated in [1].
+
+ The default slide duration is set to 4 seconds; use :func:`set_duration`
+ to change this.
+ """
+ def __init__(self):
+ self.duration = 4000
+ self.image = None
+ self.audio = None
+ self.text = None
+
+ @property
+ def data_parts(self):
+ """Returns a list of the data parst in this slide"""
+ return [part for part in (self.image, self.audio, self.text)
+ if part is not None]
+
+ def number_of_parts(self):
+ """
+ Returns the number of data parts in this slide
+
+ @rtype: int
+ """
+ num_parts = 0
+ for item in (self.image, self.audio, self.text):
+ if item is not None:
+ num_parts += 1
+
+ return num_parts
+
+ #TODO: find out what the "ref" element in SMIL does
+ #TODO: add support for "alt" element; also make sure what it does
+ def add_image(self, filename, time_begin=0, time_end=0):
+ """
+ Adds an image to this slide.
+
+ :param filename: The name of the image file to add. Supported formats
+ are JPEG, GIF and WBMP.
+ :type filename: str
+ :param time_begin: The time (in milliseconds) during the duration of
+ this slide to begin displaying the image. If this is
+ 0 or less, the image will be displayed from the
+ moment the slide is opened.
+ :type time_begin: int
+ :param time_end: The time (in milliseconds) during the duration of this
+ slide at which to stop showing (i.e. hide) the image.
+ If this is 0 or less, or if it is greater than the
+ actual duration of this slide, it will be shown until
+ the next slide is accessed.
+ :type time_end: int
+
+ :raise TypeError: An inappropriate variable type was passed in of the
+ parameters
+ """
+ if not isinstance(filename, str):
+ raise TypeError("filename must be a string")
+
+ if not isinstance(time_begin, int) or not isinstance(time_end, int):
+ raise TypeError("time_begin and time_end must be ints")
+
+ if not os.path.isfile(filename):
+ raise OSError("filename must be a file")
+
+ if time_end > 0 and time_end < time_begin:
+ raise ValueError('time_end cannot be lower than time_begin')
+
+ self.image = (DataPart(filename), time_begin, time_end)
+
+ def add_audio(self, filename, time_begin=0, time_end=0):
+ """
+ Adds an audio clip to this slide.
+
+ :param filename: The name of the audio file to add. Currently the only
+ supported format is AMR.
+ :type filename: str
+ :param time_begin: The time (in milliseconds) during the duration of
+ this slide to begin playback of the audio clip. If
+ this is 0 or less, the audio clip will be played the
+ moment the slide is opened.
+ :type time_begin: int
+ :param time_end: The time (in milliseconds) during the duration of this
+ slide at which to stop playing (i.e. mute) the audio
+ clip. If this is 0 or less, or if it is greater than
+ the actual duration of this slide, the entire audio
+ clip will be played, or until the next slide is
+ accessed.
+ :type time_end: int
+ :raise TypeError: An inappropriate variable type was passed in of the
+ parameters
+ """
+ if not isinstance(filename, str):
+ raise TypeError("filename must be a string")
+
+ if not isinstance(time_begin, int) or not isinstance(time_end, int):
+ raise TypeError("time_begin and time_end must be ints")
+
+ if not os.path.isfile(filename):
+ raise OSError("filename must be a file")
+
+ if time_end > 0 and time_end < time_begin:
+ raise ValueError('time_end cannot be lower than time_begin')
+
+ self.audio = (DataPart(filename), time_begin, time_end)
+
+ def add_text(self, text, time_begin=0, time_end=0):
+ """
+ Adds a block of text to this slide.
+
+ :param text: The text to add to the slide.
+ :type text: str
+ :param time_begin: The time (in milliseconds) during the duration of
+ this slide to begin displaying the text. If this is
+ 0 or less, the text will be displayed from the
+ moment the slide is opened.
+ :type time_begin: int
+ :param time_end: The time (in milliseconds) during the duration of this
+ slide at which to stop showing (i.e. hide) the text.
+ If this is 0 or less, or if it is greater than the
+ actual duration of this slide, it will be shown until
+ the next slide is accessed.
+ :type time_end: int
+
+ :raise TypeError: An inappropriate variable type was passed in of the
+ parameters
+ """
+ if not isinstance(text, str):
+ raise TypeError("Text must be a string")
+
+ if not isinstance(time_begin, int) or not isinstance(time_end, int):
+ raise TypeError("time_begin and time_end must be ints")
+
+ if time_end > 0 and time_end < time_begin:
+ raise ValueError('time_end cannot be lower than time_begin')
+
+ time_data = DataPart()
+ time_data.set_text(text)
+ self.text = (time_data, time_begin, time_end)
+
+ def set_duration(self, duration):
+ """ Sets the maximum duration of this slide (i.e. how long this slide
+ should be displayed)
+
+ @param duration: the maxium slide duration, in milliseconds
+ @type duration: int
+
+ @raise TypeError: 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 ''
diff --git a/messaging/mms/mms_pdu.py b/messaging/mms/mms_pdu.py
new file mode 100644
index 0000000..ed44c46
--- /dev/null
+++ b/messaging/mms/mms_pdu.py
@@ -0,0 +1,996 @@
+# This library is free software.
+#
+# It was originally distributed under the terms of the GNU Lesser
+# General Public License Version 2.
+#
+# python-messaging opts to apply the terms of the ordinary GNU
+# General Public License v2, as permitted by section 3 of the LGPL
+# v2.1. This re-licensing allows the entirety of python-messaging to
+# be distributed according to the terms of GPL-2.
+#
+# See the COPYING file included in this archive
+"""MMS Data Unit structure encoding and decoding classes"""
+
+from __future__ import with_statement
+import array
+import os
+import random
+
+from messaging.utils import debug
+from messaging.mms import message, wsp_pdu
+from messaging.mms.iterator import PreviewIterator
+
+
+def flatten_list(x):
+ """Flattens ``x`` into a single list"""
+ result = []
+ for el in x:
+ if hasattr(el, "__iter__") and not isinstance(el, basestring):
+ result.extend(flatten_list(el))
+ else:
+ result.append(el)
+ return result
+
+
+mms_field_names = {
+ 0x01: ('Bcc', 'encoded_string_value'),
+ 0x02: ('Cc', 'encoded_string_value'),
+ 0x03: ('Content-Location', 'uri_value'),
+ 0x04: ('Content-Type', 'content_type_value'),
+ 0x05: ('Date', 'date_value'),
+ 0x06: ('Delivery-Report', 'boolean_value'),
+ 0x07: ('Delivery-Time', 'delivery_time_value'),
+ 0x08: ('Expiry', 'expiry_value'),
+ 0x09: ('From', 'from_value'),
+ 0x0a: ('Message-Class', 'message_class_value'),
+ 0x0b: ('Message-ID', 'text_string'),
+ 0x0c: ('Message-Type', 'message_type_value'),
+ 0x0d: ('MMS-Version', 'version_value'),
+ 0x0e: ('Message-Size', 'long_integer'),
+ 0x0f: ('Priority', 'priority_value'),
+ 0x10: ('Read-Reply', 'boolean_value'),
+ 0x11: ('Report-Allowed', 'boolean_value'),
+ 0x12: ('Response-Status', 'response_status_value'),
+ 0x13: ('Response-Text', 'encoded_string_value'),
+ 0x14: ('Sender-Visibility', 'sender_visibility_value'),
+ 0x15: ('Status', 'status_value'),
+ 0x16: ('Subject', 'encoded_string_value'),
+ 0x17: ('To', 'encoded_string_value'),
+ 0x18: ('Transaction-Id', 'text_string'),
+}
+
+
+class MMSDecoder(wsp_pdu.Decoder):
+ """A decoder for MMS messages"""
+
+ def __init__(self, filename=None):
+ """
+ :param filename: If specified, decode the content of the MMS
+ message file with this name
+ :type filename: str
+ """
+ self._mms_data = array.array('B')
+ self._mms_message = message.MMSMessage()
+ self._parts = []
+
+ def decode_file(self, filename):
+ """
+ Load the data contained in the specified ``filename``, and decode it.
+
+ :param filename: The name of the MMS message file to open
+ :type filename: str
+
+ :raise OSError: The filename is invalid
+
+ :return: The decoded MMS data
+ :rtype: MMSMessage
+ """
+ num_bytes = os.stat(filename)[6]
+ data = array.array('B')
+
+ with open(filename, 'rb') as f:
+ data.fromfile(f, num_bytes)
+
+ return self.decode_data(data)
+
+ def decode_data(self, data):
+ """
+ Decode the specified MMS message data
+
+ :param data: The MMS message data to decode
+ :type data: array.array('B')
+
+ :return: The decoded MMS data
+ :rtype: MMSMessage
+ """
+ self._mms_message = message.MMSMessage()
+ self._mms_data = data
+ body_iter = self.decode_message_header()
+ self.decode_message_body(body_iter)
+ return self._mms_message
+
+ def decode_message_header(self):
+ """
+ Decodes the (full) MMS header data
+
+ This must be called before :func:`_decodeBody`, as it sets
+ certain internal variables relating to data lengths, etc.
+ """
+ data_iter = PreviewIterator(self._mms_data)
+
+ # First 3 headers (in order
+ ############################
+ # - X-Mms-Message-Type
+ # - X-Mms-Transaction-ID
+ # - X-Mms-Version
+ # TODO: reimplement strictness - currently we allow these 3 headers
+ # to be mixed with any of the other headers (this allows the
+ # decoding of "broken" MMSs, but is technically incorrect)
+
+ # Misc headers
+ ##############
+ # The next few headers will not be in a specific order, except for
+ # "Content-Type", which should be the last header
+ # According to [4], MMS header field names will be short integers
+ content_type_found = False
+ header = ''
+ while content_type_found == False:
+ try:
+ header, value = self.decode_header(data_iter)
+ except StopIteration:
+ break
+
+ if header == mms_field_names[0x04][0]:
+ content_type_found = True
+ else:
+ self._mms_message.headers[header] = value
+
+ if header == 'Content-Type':
+ # Otherwise it might break Content-Location
+ # content_type, params = value
+ self._mms_message.headers[header] = value
+
+ return data_iter
+
+ def decode_message_body(self, data_iter):
+ """
+ Decodes the MMS message body
+
+ :param data_iter: an iterator over the sequence of bytes of the MMS
+ body
+ :type data_iter: iter
+ """
+ ######### MMS body: headers ###########
+ # Get the number of data parts in the MMS body
+ try:
+ num_entries = self.decode_uint_var(data_iter)
+ except StopIteration:
+ return
+
+ #print 'Number of data entries (parts) in MMS body:', num_entries
+
+ ########## MMS body: entries ##########
+ # For every data "part", we have to read the following sequence:
+ # ,
+ # ,
+ # ,
+ #
+ 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 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:
+ (, )
+ :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:
+ (, )
+ :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 =
+ No =
+
+ 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 =
+ Insert-address-token =
+
+ :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 ''
+
+ 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 =
+ Advertisement =
+ Informational =
+ Auto =
+
+ 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 ''
+ :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 ''
+
+ @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 =
+ Show =
+
+ :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 =
+ Relative-token =
+
+ :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 (
+ - length of
+ ) octets the part's headers
+ Data 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 +
+ 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:
+ # ,
+ # ,
+ # ,
+ # .
+
+ # 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:
+ (, )
+ :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 =
+ Insert-address-token =
+
+ :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')]
diff --git a/messaging/mms/wsp_pdu.py b/messaging/mms/wsp_pdu.py
new file mode 100644
index 0000000..bacd46d
--- /dev/null
+++ b/messaging/mms/wsp_pdu.py
@@ -0,0 +1,2055 @@
+# 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
+"""
+WSP Data Unit structure encoding and decoding classes
+
+Throughout the classes defined in this module, the following "primitive data
+type" terminology applies, as specified in [5], section 8.1.1::
+
+ Data Type Definition
+ bit 1 bit of data
+ octet 8 bits of opaque data
+ uint8 8-bit unsigned integer
+ uint16 16-bit unsigned integer
+ uint32 32-bit unsigned integer
+ uintvar variable length unsigned integer
+
+This Encoder and Decoder classes provided in this module firstly provides
+public methods for decoding and encoding each of these data primitives (where
+needed).
+
+Next, they provide methods encapsulating the basic WSP Header encoding rules
+as defined in section 8.4.2.1 of [5].
+
+Finally, the classes defined here provide methods for decoding/parsing
+specific WSP header fields.
+
+References used in the code and this document:
+
+[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}
+"""
+
+import array
+from datetime import datetime
+
+from messaging.utils import debug
+from messaging.mms.iterator import PreviewIterator
+
+wsp_pdu_types = {
+ 0x01: 'Connect',
+ 0x02: 'ConnectReply',
+ 0x03: 'Redirect',
+ 0x04: 'Reply',
+ 0x05: 'Disconnect',
+ 0x06: 'Push',
+ 0x07: 'ConfirmedPush',
+ 0x08: 'Suspend',
+ 0x09: 'Resume',
+ 0x40: 'Get',
+ 0x60: 'Post',
+}
+
+# Well-known parameter assignments ([5], table 38)
+well_known_parameters = {
+ 0x00: ('Q', 'q_value'),
+ 0x01: ('Charset', 'well_known_charset'),
+ 0x02: ('Level', 'version_value'),
+ 0x03: ('Type', 'integer_value'),
+ 0x05: ('Name', 'text_string'),
+ 0x06: ('Filename', 'text_string'),
+ 0x07: ('Differences', 'Field-name'),
+ 0x08: ('Padding', 'short_integer'),
+ 0x09: ('Type', 'constrained_encoding'), # encoding version 1.2
+ 0x0a: ('Start', 'text_string'),
+ 0x0b: ('Start-info', 'text_string'),
+ 0x0c: ('Comment', 'text_string'), # encoding version 1.3
+ 0x0d: ('Domain', 'text_string'),
+ 0x0e: ('Max-Age', 'delta_seconds_value'),
+ 0x0f: ('Path', 'text_string'),
+ 0x10: ('Secure', 'no_value'),
+ 0x11: ('SEC', 'short_integer'), # encoding version 1.4
+ 0x12: ('MAC', 'text_value'),
+ 0x13: ('Creation-date', 'date_value'),
+ 0x14: ('Modification-date', 'date_value'),
+ 0x15: ('Read-date', 'date_value'),
+ 0x16: ('Size', 'integer_value'),
+ 0x17: ('Name', 'text_value'),
+ 0x18: ('Filename', 'text_value'),
+ 0x19: ('Start', 'text_value'),
+ 0x1a: ('Start-info', 'text_value'),
+ 0x1b: ('Comment', 'text_value'),
+ 0x1c: ('Domain', 'text_value'),
+ 0x1d: ('Path', 'text_value'),
+}
+
+
+# Content type assignments ([5], table 40)
+well_known_content_types = [
+ '*/*', 'text/*', 'text/html', 'text/plain',
+ 'text/x-hdml', 'text/x-ttml', 'text/x-vCalendar',
+ 'text/x-vCard', 'text/vnd.wap.wml',
+ 'text/vnd.wap.wmlscript', 'text/vnd.wap.wta-event',
+ 'multipart/*', 'multipart/mixed', 'multipart/form-data',
+ 'multipart/byterantes', 'multipart/alternative',
+ 'application/*', 'application/java-vm',
+ 'application/x-www-form-urlencoded',
+ 'application/x-hdmlc', 'application/vnd.wap.wmlc',
+ 'application/vnd.wap.wmlscriptc',
+ 'application/vnd.wap.wta-eventc',
+ 'application/vnd.wap.uaprof',
+ 'application/vnd.wap.wtls-ca-certificate',
+ 'application/vnd.wap.wtls-user-certificate',
+ 'application/x-x509-ca-cert',
+ 'application/x-x509-user-cert',
+ 'image/*', 'image/gif', 'image/jpeg', 'image/tiff',
+ 'image/png', 'image/vnd.wap.wbmp',
+ 'application/vnd.wap.multipart.*',
+ 'application/vnd.wap.multipart.mixed',
+ 'application/vnd.wap.multipart.form-data',
+ 'application/vnd.wap.multipart.byteranges',
+ 'application/vnd.wap.multipart.alternative',
+ 'application/xml', 'text/xml',
+ 'application/vnd.wap.wbxml',
+ 'application/x-x968-cross-cert',
+ 'application/x-x968-ca-cert',
+ 'application/x-x968-user-cert',
+ 'text/vnd.wap.si',
+ 'application/vnd.wap.sic',
+ 'text/vnd.wap.sl',
+ 'application/vnd.wap.slc',
+ 'text/vnd.wap.co',
+ 'application/vnd.wap.coc',
+ 'application/vnd.wap.multipart.related',
+ 'application/vnd.wap.sia',
+ 'text/vnd.wap.connectivity-xml',
+ 'application/vnd.wap.connectivity-wbxml',
+ 'application/pkcs7-mime',
+ 'application/vnd.wap.hashed-certificate',
+ 'application/vnd.wap.signed-certificate',
+ 'application/vnd.wap.cert-response',
+ 'application/xhtml+xml',
+ 'application/wml+xml',
+ 'text/css',
+ 'application/vnd.wap.mms-message',
+ 'application/vnd.wap.rollover-certificate',
+ 'application/vnd.wap.locc+wbxml',
+ 'application/vnd.wap.loc+xml',
+ 'application/vnd.syncml.dm+wbxml',
+ 'application/vnd.syncml.dm+xml',
+ 'application/vnd.syncml.notification',
+ 'application/vnd.wap.xhtml+xml',
+ 'application/vnd.wv.csp.cir',
+ 'application/vnd.oma.dd+xml',
+ 'application/vnd.oma.drm.message',
+ 'application/vnd.oma.drm.content',
+ 'application/vnd.oma.drm.rights+xml',
+ 'application/vnd.oma.drm.rights+wbxml',
+]
+
+# Well-known character sets (table 42 of [5])
+# Format { : }
+# Note that the assigned number is the same as the IANA MIBEnum value
+# "gsm-default-alphabet" is not included, as it is not assigned any
+# value in [5]. Also note, this is by no means a complete list
+well_known_charsets = {
+ 0x07EA: 'big5',
+ 0x03E8: 'iso-10646-ucs-2',
+ 0x04: 'iso-8859-1',
+ 0x05: 'iso-8859-2',
+ 0x06: 'iso-8859-3',
+ 0x07: 'iso-8859-4',
+ 0x08: 'iso-8859-5',
+ 0x09: 'iso-8859-6',
+ 0x0A: 'iso-8859-7',
+ 0x0B: 'iso-8859-8',
+ 0x0C: 'iso-8859-9',
+ 0x11: 'shift_JIS',
+ 0x03: 'us-ascii',
+ 0x6A: 'utf-8',
+}
+
+# Header Field Name assignments ([5], table 39)
+header_field_names = [
+ 'Accept', 'Accept-Charset', 'Accept-Encoding',
+ 'Accept-Language', 'Accept-Ranges', 'Age',
+ 'Allow', 'Authorization', 'Cache-Control',
+ 'Connection', 'Content-Base', 'Content-Encoding',
+ 'Content-Language', 'Content-Length',
+ 'Content-Location', 'Content-MD5', 'Content-Range',
+ 'Content-Type', 'Date', 'Etag', 'Expires', 'From',
+ 'Host', 'If-Modified-Since', 'If-Match',
+ 'If-None-Match', 'If-Range', 'If-Unmodified-Since',
+ 'Location', 'Last-Modified', 'Max-Forwards', 'Pragma',
+ 'Proxy-Authenticate', 'Proxy-Authorization', 'Public',
+ 'Range', 'Referer', 'Retry-After', 'Server',
+ 'Transfer-Encoding', 'Upgrade', 'User-Agent',
+ 'Vary', 'Via', 'Warning', 'WWW-Authenticate',
+ 'Content-Disposition',
+ # encoding version 1.2
+ 'X-Wap-Application-Id', 'X-Wap-Content-URI',
+ 'X-Wap-Initiator-URI', 'Accept-Application',
+ 'Bearer-Indication', 'Push-Flag', 'Profile',
+ 'Profile-Diff', 'Profile-Warning',
+ # encoding version 1.3
+ 'Expect', 'TE', 'Trailer', 'Accept-Charset',
+ 'Accept-Encoding', 'Cache-Control',
+ 'Content-Range', 'X-Wap-Tod', 'Content-ID',
+ 'Set-Cookie', 'Cookie', 'Encoding-Version',
+ # encoding version 1.4
+ 'Profile-Warning', 'Content-Disposition',
+ 'X-WAP-Security', 'Cache-Control',
+]
+
+
+# TODO: combine this dict with the header_field_names table (same as well
+# known parameter assignments)
+# Temporary fix to allow different types of header field values to be
+# dynamically decoded
+header_field_encodings = {'Accept': 'accept_value', 'Pragma': 'pragma_value'}
+
+
+def get_header_field_names(version='1.2'):
+ """
+ Formats list of assigned values for header field names, for the
+ specified WSP encoding version.
+
+ :param version: The WSP encoding version to use. This defaults
+ to "1.2", but may be "1.1", "1.2", "1.3" or
+ "1.4" (see table 39 in [5] for details).
+ :type version: str
+
+ :raise ValueError: The specified encoding version is invalid.
+
+ :return: A list containing the WSP header field names with assigned
+ numbers for the specified encoding version (and lower).
+ :rtype: list
+ """
+ if version not in ('1.1', '1.2', '1.3', '1.4'):
+ raise ValueError('version must be "1.1",'
+ '"1.2", "1.3" or "1.4"')
+
+ version = int(version.split('.')[1])
+
+ versioned_field_names = header_field_names[:]
+ if version == 3:
+ versioned_field_names = versioned_field_names[:0x44]
+ elif version == 2:
+ versioned_field_names = versioned_field_names[:0x38]
+ elif version == 1:
+ versioned_field_names = versioned_field_names[:0x2f]
+
+ return versioned_field_names
+
+
+def get_well_known_parameters(version='1.2'):
+ """
+ Return a list of assigned values for parameter names for ``version``
+
+ Formats list of assigned values for well-known parameter names,
+ for the specified WSP encoding version.
+
+ :param version: The WSP encoding version to use. This defaults
+ to "1.2", but may be "1.1", "1.2", "1.3" or
+ "1.4" (see table 38 in [5] for details).
+ :type version: str
+
+ :raise ValueError: The specified encoding version is invalid.
+
+ :return: A dictionary containing the well-known parameters with
+ assigned numbers for the specified encoding version (and
+ lower). Entries in this dict follow the format::
+
+ : (, )
+ :rtype: dict
+ """
+ if version not in ('1.1', '1.2', '1.3', '1.4'):
+ raise ValueError('version must be "1.1",'
+ '"1.2", "1.3" or "1.4"')
+ else:
+ version = int(version.split('.')[1])
+
+ versioned_params = well_known_parameters.copy()
+ if version <= 3:
+ for assigned_number in range(0x11, 0x1e):
+ del versioned_params[assigned_number]
+
+ if version <= 2:
+ for assigned_number in range(0x0c, 0x11):
+ del versioned_params[assigned_number]
+
+ if version == 1:
+ for assigned_number in range(0x09, 0x0c):
+ del versioned_params[assigned_number]
+
+ return versioned_params
+
+
+class DecodeError(Exception):
+ """
+ Raised when a decoding operation failed
+
+ Most probably due to an invalid byte in the sequence provided for decoding
+ """
+
+
+class EncodeError(Exception):
+ """
+ Raised when an encoding operation failed
+
+ Most probably due to an invalid value provided for encoding
+ """
+
+
+class Decoder:
+ """A WSP Data unit decoder"""
+
+ @staticmethod
+ def decode_uint_8(byte_iter):
+ """
+ Decodes an 8-bit uint from the byte pointed to by ``byte_iter``
+
+ This function will move the iterator passed as ``byte_iter`` one
+ byte forward.
+
+ :param byte_iter: an iterator over a sequence of bytes
+ :type byte_iter: iter
+
+ :return: the decoded 8-bit unsigned integer
+ :rtype: int
+ """
+ # Make the byte unsigned
+ return next(byte_iter) & 0xf
+
+ @staticmethod
+ def decode_uint_var(byte_iter):
+ """
+ Decodes the uint starting at the byte pointed to by ``byte_iter``
+
+ See :func:`wsp.Encoder.encode_uint_var` for a detailed description of
+ the encoding scheme used for ``uint_var`` sequences.
+
+ This function will move the iterator passed as ``byte_iter`` to
+ the last octet in the uintvar sequence; thus, after calling this,
+ that iterator's `next()` function will return the first byte
+ **after** the uintvar sequence.
+
+ :param byte_iter: an iterator over a sequence of bytes
+ :type byte_iter: iter
+
+ :return: the decoded unsigned integer
+ :rtype: int
+ """
+ uint = 0
+ byte = next(byte_iter)
+ while (byte >> 7) == 0x01:
+ uint = uint << 7
+ uint |= byte & 0x7f
+ byte = next(byte_iter)
+
+ uint = uint << 7
+ uint |= byte & 0x7f
+ return uint
+
+ @staticmethod
+ def decode_short_integer(byte_iter):
+ """
+ Decodes the short-integer value starting at ``byte_iter``
+
+ The encoding for a long integer is specified in [5], section 8.4.2.1::
+
+ Short-integer = OCTET
+
+ Integers in range 0-127 shall be encoded as a one octet value with
+ the most significant bit set to one (1xxx xxxx) and with the value
+ in the remaining least significant bits.
+
+ :raise DecodeError: Not a valid short-integer; the most significant
+ isn't set to 1. ``byte_iter`` will not be
+ modified if this is raised
+
+ :return: The decoded short integer
+ :rtype: int
+ """
+ byte = byte_iter.preview()
+ if not byte & 0x80:
+ byte_iter.reset_preview()
+ raise DecodeError('Not a valid short-integer: MSB not set')
+
+ byte = next(byte_iter)
+ return byte & 0x7f
+
+ @staticmethod
+ def decode_short_integer_from_byte(byte):
+ """
+ Decodes the short-integer value contained in the specified byte value
+
+ :param byte: the byte value to decode
+ :type byte: int
+
+ :raise DecodeError: Not a valid short-integer; the MSB isn't set to 1.
+ :return: The decoded short integer
+ :rtype: int
+ """
+ if not byte & 0x80:
+ raise DecodeError('Not a valid short-integer: MSB not set')
+
+ return byte & 0x7f
+
+ @staticmethod
+ def decode_long_integer(byte_iter):
+ """
+ Decodes the long int value pointed to by ``byte_iter``
+
+ If this function returns successfully, it will move the
+ iterator passed as ``byte_iter`` to the last octet in the
+ encoded long integer sequence; thus, after calling this, that
+ iterator's `next()` function will return the first byte
+ **after** the encoded long integer sequence.
+
+ The encoding for a long integer is specified in [5], section 8.4.2.1,
+ and follows the form::
+
+ Long-integer = [Short-length] [Multi-octet-integer]
+ ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
+ 1 byte bytes
+
+ The Short-length indicates the length of the Multi-octet-integer.
+
+ :raise DecodeError: The byte pointed to by ``byte_iter.next`` does
+ not indicate the start of a valid long-integer
+ sequence (short-length is invalid). If this is
+ raised, the iterator passed as ``byte_iter`` will
+ not be modified.
+
+ :param byte_iter: an iterator over a sequence of bytes
+ :type byte_iter: iter
+
+ :return: The decoded long integer
+ :rtype: int
+ """
+ try:
+ shortLength = Decoder.decode_short_length(byte_iter)
+ except DecodeError:
+ raise DecodeError('short-length byte is invalid')
+
+ longInt = 0
+ # Decode the Multi-octect-integer
+ for i in range(shortLength):
+ longInt = longInt << 8
+ longInt |= next(byte_iter)
+
+ return longInt
+
+ @staticmethod
+ def decode_text_string(byte_iter):
+ """
+ Decodes the null-terminated, binary-encoded string value starting
+ at the byte pointed to by ``byte_iter``.
+
+ this function will move the iterator passed as ``byte_iter`` to
+ the last octet in the encoded string sequence; thus, after
+ calling this, that iterator's `next()` function will return
+ the first byte **after** the encoded string sequence.
+
+ This follows the basic encoding rules specified in [5], section
+ 8.4.2.1
+
+ :param byte_iter: an iterator over a sequence of bytes
+ :type byte_iter: iter
+
+ :return: The decoded text string
+ :rtype: str
+ """
+ decoded_string = ''
+ byte = next(byte_iter)
+ # Remove Quote character (octet 127), if present
+ if byte == 127:
+ byte = next(byte_iter)
+
+ while byte != 0x00:
+ decoded_string += chr(byte)
+ byte = next(byte_iter)
+
+ return decoded_string
+
+ @staticmethod
+ def decode_quoted_string(byte_iter):
+ """
+ From [5], section 8.4.2.1::
+
+ Quoted-string = *TEXT End-of-string
+
+ The TEXT encodes an RFC2616 Quoted-string with the enclosing
+ quotation-marks <"> removed
+
+ :return: The decoded text string
+ :rtype: str
+ """
+ # look for the quote character
+ byte = byte_iter.preview()
+ if byte != 34:
+ byte_iter.reset_preview()
+ raise DecodeError('Invalid quoted string: must '
+ 'start with ')
+
+ next(byte_iter)
+ # CHECK: should the quotation chars be pre- and appended before
+ # returning *technically* we should not check for quote characters.
+ return Decoder.decode_text_string(byte_iter)
+
+ @staticmethod
+ def decode_token_text(byte_iter):
+ """ From [5], section 8.4.2.1:
+ Token-text = Token End-of-string
+
+ :raise DecodeError: invalid token; byte_iter is not modified
+
+ :return: The token string if successful, otherwise the read byte
+ :rtype: str or int
+ """
+ separators = (11, 32, 40, 41, 44, 47, 58, 59, 60, 61, 62, 63, 64, 91,
+ 92, 93, 123, 125)
+ token = ''
+ byte = byte_iter.preview()
+ if byte <= 31 or byte in separators:
+ byte_iter.reset_preview()
+ raise DecodeError('Invalid token')
+
+ byte = next(byte_iter)
+ while byte > 31 and byte not in separators:
+ token += chr(byte)
+ byte = next(byte_iter)
+
+ return token
+
+ @staticmethod
+ def decode_extension_media(byte_iter):
+ """
+ Decode the extension media pointed by ``byte_iter``
+
+ This encoding is used for media values, which have no well-known
+ binary encoding
+
+ From [5], section 8.4.2.1::
+
+ Extension-media = *TEXT End-of-string
+
+
+ :raise DecodeError: The TEXT started with an invalid character.
+ ``byte_iter`` is not modified if this happens.
+
+ :return: The decoded media type value
+ :rtype: str
+ """
+ media_value = ''
+ byte = byte_iter.preview()
+ if byte < 32 or byte == 127:
+ byte_iter.reset_preview()
+ raise DecodeError('Invalid Extension-media: TEXT '
+ 'starts with invalid character: %d' % byte)
+
+ byte = next(byte_iter)
+ while byte != 0x00:
+ media_value += chr(byte)
+ byte = next(byte_iter)
+
+ return media_value
+
+ @staticmethod
+ def decode_constrained_encoding(byte_iter):
+ """Constrained-encoding = Extension-Media --or-- Short-integer
+ This encoding is used for token values, which have no well-known
+ binary encoding, or when the assigned number of the well-known
+ encoding is small enough to fit into Short-integer.
+
+ :return: The decoding constrained-encoding token value
+ :rtype: str or int
+ """
+ result = None
+ try:
+ # First try and see if this is just a short-integer
+ result = Decoder.decode_short_integer(byte_iter)
+ except DecodeError:
+ # Ok, it should be Extension-Media then
+ try:
+ result = Decoder.decode_extension_media(byte_iter)
+ except DecodeError:
+ # Give up
+ raise DecodeError('Not a valid Constrained-encoding sequence')
+
+ return result
+
+ @staticmethod
+ def decode_short_length(byte_iter):
+ """ From [5], section 8.4.2.2:
+ Short-length =
+
+ :raise DecodeError: The byte is not a valid short-length value;
+ it is not in octet range 0-30. In this case, the
+ iterator passed as ``byte_iter`` is not modified.
+
+ :note: If this function returns successfully, the iterator passed as
+ ``byte_iter`` is moved one byte forward.
+
+ :return: The decoded short-length
+ :rtype: int
+ """
+ # Make sure it's a valid short-length
+ byte = byte_iter.preview()
+ if byte > 30:
+ byte_iter.reset_preview()
+ raise DecodeError('Not a valid short-length: '
+ 'should be in octet range 0-30')
+
+ return next(byte_iter)
+
+ @staticmethod
+ def decode_value_length(byte_iter):
+ """
+ Decodes the value length indicator starting at ``byte_iter``
+
+ "Value length" is used to indicate the length of a value to follow, as
+ used in the `Content-Type` header in the MMS body, for example.
+
+ The encoding for a value length indicator is specified in [5],
+ section 8.4.2.2, and follows the form::
+
+ Value-length = [Short-length] --or-- [Length-quote] [Length]
+ ^^^^^^ ^^^^^^ ^^^^^^
+ 1 byte 1 byte x bytes
+ uint_var-integer
+
+ :raise DecodeError: The value_length could not be decoded. If this
+ happens, ``byte_iter`` is not modified.
+
+ :return: The decoded value length indicator
+ :rtype: int
+ """
+ length_value = 0
+ # Check for short-length
+ try:
+ length_value = Decoder.decode_short_length(byte_iter)
+ except DecodeError:
+ byte = byte_iter.preview()
+ # CHECK: this strictness MAY cause issues, but it is correct
+ if byte == 31:
+ next(byte_iter) # skip past the length-quote
+ length_value = Decoder.decode_uint_var(byte_iter)
+ else:
+ byte_iter.reset_preview()
+ raise DecodeError('Invalid Value-length: not short-length, '
+ 'and no length-quote present')
+
+ return length_value
+
+ @staticmethod
+ def decode_integer_value(byte_iter):
+ """
+ Decodes the integer value pointed by ``byte_iter``
+
+ From [5], section 8.4.2.3::
+
+ Integer-Value = Short-integer | Long-integer
+
+ If successful, this function will move the iterator passed as
+ ``byte_iter`` to the last octet in the integer value sequence;
+ thus, after calling this, that iterator's `next()` function
+ will return the first byte **after** the integer value sequence.
+
+ :raise DecodeError: The sequence of bytes starting at ``byte_iter``
+ does not contain a valid integer value. If this
+ is raised, the iterator is not modified.
+
+ :return: The decoded integer value
+ :rtype: int
+ """
+ integer = 0
+ # First try and see if it's a short-integer
+ try:
+ integer = Decoder.decode_short_integer(byte_iter)
+ except DecodeError:
+ try:
+ integer = Decoder.decode_long_integer(byte_iter)
+ except DecodeError:
+ raise DecodeError('Not a valid integer value')
+
+ return integer
+
+ @staticmethod
+ def decode_content_type_value(byte_iter):
+ """
+ Decodes an encoded content type value.
+
+ From [5], section 8.4.2.24::
+
+ Content-type-value = Constrained-media | Content-general-form
+
+ The short form of the Content-type-value MUST only be used when the
+ well-known media is in the range of 0-127 or a text string. In all
+ other cases the general form MUST be used.
+
+ :return: The media type (content type), and a dictionary of
+ parameters to this content type (which is empty if there
+ are no parameters). This parameter dictionary is in the
+ format:
+ {: }.
+ The final returned tuple is in the format:
+ (, )
+ :rtype: tuple
+ """
+ # First try do decode it as Constrained-media
+ content_type = ''
+ params = {}
+ try:
+ content_type = Decoder.decode_constrained_media(byte_iter)
+ except DecodeError:
+ # Try the general form
+ content_type, params = Decoder.decode_content_general_form(byte_iter)
+
+ return content_type, params
+
+ @staticmethod
+ def decode_well_known_media(byte_iter):
+ """
+ Decodes the well known media pointed by ``byte_iter``
+ From [5], section 8.4.2.7::
+
+ Well-known-media = Integer-value
+
+ It is encoded using values from the "Content Type Assignments" table
+ (see [5], table 40).
+
+ :param byte_iter: an iterator over a sequence of bytes
+ :type byte_iter: iter
+
+ :raise DecodeError: This is raised if the integer value representing
+ the well-known media type cannot be decoded
+ correctly, or the well-known media type value
+ could not be found in the table of assigned
+ content types.
+ If this exception is raised, the iterator passed
+ as ``byte_iter`` is not modified.
+
+ If successful, this function will move the iterator passed as
+ ``byte_iter`` to the last octet in the content type value
+ sequence; thus, after calling this, that iterator's `next()`
+ function will return the first byte B{after}the content type
+ value sequence.
+
+ :return: the decoded MIME content type name
+ :rtype: str
+ """
+ try:
+ value = Decoder.decode_integer_value(byte_iter)
+ except DecodeError:
+ raise DecodeError('Invalid well-known media: could not read '
+ 'integer value representing it')
+
+ try:
+ return well_known_content_types[value]
+ except IndexError:
+ raise DecodeError('Invalid well-known media: could not '
+ 'find content type in table of assigned values')
+
+ @staticmethod
+ def decode_media_type(byte_iter):
+ """
+ Decodes the media type pointed by ``byte_iter``
+
+ Used by :func:`decode_content_general_form`
+
+ From [5], section 8.2.4.24::
+
+ Media-type = (Well-known-media | Extension-Media) *(Parameter)
+
+ :param byte_iter: an iterator over a sequence of bytes
+ :type byte_iter: iter
+
+ :return: The decoded media type
+ :rtype: str
+ """
+ try:
+ return Decoder.decode_well_known_media(byte_iter)
+ except DecodeError:
+ return Decoder.decode_extension_media(byte_iter)
+
+ @staticmethod
+ def decode_constrained_media(byte_iter):
+ """
+ Decodes the constrained media pointed pointed by ``byte_iter``
+
+ It is encoded using values from the "Content Type Assignments" table.
+
+ From [5], section 8.4.2.7::
+
+ Constrained-media = Constrained-encoding
+
+ :raise DecodeError: Invalid constrained media sequence
+
+ :return: The decoded media type
+ :rtype: str
+ """
+ try:
+ media_value = Decoder.decode_constrained_encoding(byte_iter)
+ except DecodeError as msg:
+ raise DecodeError('Invalid Constrained-media: %s' % msg)
+
+ if isinstance(media_value, int):
+ try:
+ return well_known_content_types[media_value]
+ except IndexError:
+ raise DecodeError('Invalid constrained media: could not '
+ 'find well-known content type')
+
+ return media_value
+
+ @staticmethod
+ def decode_content_general_form(byte_iter):
+ """
+ Decodes the content general form pointed by ``byte_iter``
+
+ From [5], section 8.4.2.24::
+
+ Content-general-form = Value-length Media-type
+
+ Used in decoding Content-type fields and their parameters;
+ see :func:`decode_content_type_value`. Used by
+ :func:`decode_content_type_value`.
+
+ :return: The media type (content type), and a dictionary of
+ parameters to this content type (which is empty if there
+ are no parameters). This parameter dictionary is in the
+ format:
+ {: }.
+ The final returned tuple is in the format:
+ (, )
+ :rtype: tuple
+ """
+ # This is the length of the (encoded) media-type and all parameters
+ value_length = Decoder.decode_value_length(byte_iter)
+
+ # Read parameters, etc, until is reached
+ ct_field_bytes = array.array('B')
+ for i in range(value_length):
+ ct_field_bytes.append(next(byte_iter))
+
+ ct_iter = PreviewIterator(ct_field_bytes)
+ # Now, decode all the bytes read
+ media_type = Decoder.decode_media_type(ct_iter)
+ # Decode the included paramaters (if any)
+ parameters = {}
+ while True:
+ try:
+ parameter, value = Decoder.decode_parameter(ct_iter)
+ parameters[parameter] = value
+ except StopIteration:
+ break
+
+ return media_type, parameters
+
+ @staticmethod
+ def decode_parameter(byte_iter):
+ """
+ From [5], section 8.4.2.4::
+
+ Parameter = Typed-parameter | Untyped-parameter
+
+ :return: The name of the parameter, and its value, in the format:
+ (, )
+ :rtype: tuple
+ """
+ try:
+ return Decoder.decode_typed_parameter(byte_iter)
+ except DecodeError:
+ return Decoder.decode_untyped_parameter(byte_iter)
+
+ @staticmethod
+ def decode_typed_parameter(byte_iter):
+ """
+ Decodes the typed parameter pointed by ``byte_iter``
+
+ The actual expected type of the value is implied by the well-known
+ parameter.
+
+ This is used in decoding parameters; see :func:`decode_parameter`
+
+ From [5], section 8.4.2.4::
+
+ Typed-parameter = Well-known-parameter-token Typed-value
+
+ :return: The name of the parameter, and its value, in the format:
+ (, )
+ :rtype: tuple
+ """
+ token, value_type = Decoder.decode_well_known_parameter(byte_iter)
+ typed_value = ''
+ try:
+ typed_value = getattr(Decoder, 'decode_%s' % value_type)(byte_iter)
+ except DecodeError as msg:
+ raise DecodeError('Could not decode Typed-parameter: %s' % msg)
+ except:
+ debug('A fatal error occurred, probably due to an '
+ 'unimplemented decoding operation')
+ raise
+
+ return token, typed_value
+
+ @staticmethod
+ def decode_untyped_parameter(byte_iter):
+ """
+ Decodes the untyped parameter pointed by ``byte_iter``
+
+ This is used in decoding parameters; see :func:`decode_parameter`
+
+ The type of the value is unknown, but it shall be encoded as an
+ integer, if that is possible.
+
+ From [5], section 8.4.2.4::
+
+ Untyped-parameter = Token-text Untyped-value
+
+ :return: The name of the parameter, and its value, in the format:
+ (, )
+ :rtype: tuple
+ """
+ token = Decoder.decode_token_text(byte_iter)
+ value = Decoder.decode_untyped_value(byte_iter)
+ return token, value
+
+ @staticmethod
+ def decode_untyped_value(byte_iter):
+ """
+ Decodes the untyped value pointed by ``byte_iter``
+
+ This is used in decoding parameter values; see
+ :func:`decode_untyped_parameter`
+
+ From [5], section 8.4.2.4::
+
+ Untyped-value = Integer-value | Text-value
+
+ :return: The decoded untyped-value
+ :rtype: int or str
+ """
+ try:
+ return Decoder.decode_integer_value(byte_iter)
+ except DecodeError:
+ return Decoder.decode_text_value(byte_iter)
+
+ @staticmethod
+ def decode_well_known_parameter(byte_iter, version='1.2'):
+ """Decodes the name and expected value type of a parameter of (for
+ example) a "Content-Type" header entry, taking into account the WSP
+ short form (assigned numbers) of well-known parameter names, as
+ specified in section 8.4.2.4 and table 38 of [5].
+
+ The code values used for parameters are specified in [5], table 38
+
+ From [5], section 8.4.2.4::
+
+ Well-known-parameter-token = Integer-value
+
+ :raise ValueError: The specified encoding version is invalid.
+ :raise DecodeError: This is raised if the integer value representing
+ the well-known parameter name cannot be decoded
+ correctly, or the well-known paramter token value
+ could not be found in the table of assigned
+ content types.
+ If this exception is raised, the iterator passed
+ as ``byte_iter`` is not modified.
+
+ :param version: The WSP encoding version to use. This defaults
+ to "1.2", but may be "1.1", "1.2", "1.3" or
+ 1.4" (see table 39 in [5] for details).
+ :type version: str
+
+ :return: the decoded parameter name, and its expected value type, in
+ the format (, )
+ :rtype: tuple
+ """
+ parameter_name = expected_value = ''
+ try:
+ parameter_value = Decoder.decode_integer_value(byte_iter)
+ except DecodeError:
+ raise DecodeError('Invalid well-known parameter token: could '
+ 'not read integer value representing it')
+
+ wk_params = get_well_known_parameters(version)
+ if parameter_value in wk_params:
+ parameter_name, expected_value = wk_params[parameter_value]
+ else:
+ #If this is reached, the parameter isn't a WSP well-known one
+ raise DecodeError('Invalid well-known parameter token: could '
+ 'not find in table of assigned numbers '
+ '(encoding version %s)' % version)
+
+ return parameter_name, expected_value
+
+ #TODO: somehow this should be more dynamic; we need to know what type
+ # is EXPECTED (hence the TYPED value)
+ @staticmethod
+ def decode_typed_value(byte_iter):
+ """
+ Decodes the typed value pointed by ``byte_iter``
+
+ In addition to the expected type, there may be no value.
+ If the value cannot be encoded using the expected type, it shall be
+ encoded as text.
+
+ This is used in decoding parameters, see :func:`decode_parameter`
+ From [5], section 8.4.2.4::
+
+ Typed-value = Compact-value | Text-value
+
+ :return: The decoded Parameter Typed-value
+ :rtype: str
+ """
+ typedValue = ''
+ try:
+ typedValue = Decoder.decode_compact_value(byte_iter)
+ except DecodeError:
+ try:
+ typedValue = Decoder.decode_text_value(byte_iter)
+ except DecodeError:
+ raise DecodeError('Could not decode the Parameter Typed-value')
+
+ return typedValue
+
+ # TODO: somehow this should be more dynamic; we need to know what
+ # type is EXPECTED
+ @staticmethod
+ def decode_compact_value(byte_iter):
+ """
+ Decodes the compact value pointed by ``byte_iter``
+
+ This is used in decoding parameters, see :func:`decodeTypeValue`
+
+ From [5], section 8.4.2.4::
+
+ Compact-value = Integer-value | Date-value | Delta-seconds-value | Q-value | Version-value | Uri-value
+
+ :raise DecodeError: Failed to decode the Parameter Compact-value;
+ if this happens, ``byte_iter`` is unmodified
+
+ :return: The decoded Compact-value (this is specific to the
+ parameter type
+ :rtype: str or int
+ """
+ compact_value = None
+ try:
+ # First, see if it's an integer value
+ # This solves the checks for: Integer-value, Date-value,
+ # Delta-seconds-value, Q-value, Version-value
+ compact_value = Decoder.decode_integer_value(byte_iter)
+ except DecodeError:
+ try:
+ # Try parsing it as a Uri-value
+ compact_value = Decoder.decode_uri_value(byte_iter)
+ except DecodeError:
+ raise DecodeError('Could not decode Parameter Compact-value')
+
+ return compact_value
+
+ @staticmethod
+ def decode_date_value(byte_iter):
+ """
+ Decode the data value pointed by ``byte_iter``
+
+ The encoding of dates shall be done in number of seconds from
+ 1970-01-01, 00:00:00 GMT.
+
+ From [5], section 8.4.2.3::
+
+ Date-value = Long-integer
+
+ :raise DecodeError: This method uses `decode_long_integer()`, and thus
+ raises this under the same conditions.
+
+ :rtype: datetime.datetime
+ """
+ return datetime.utcfromtimestamp(Decoder.decode_long_integer(byte_iter))
+
+ @staticmethod
+ def decode_delta_seconds_value(byte_iter):
+ """
+ Decodes the delta seconds value pointed by ``byte_iter``
+
+ From [5], section 8.4.2.3::
+
+ Delta-seconds-value = Integer-value
+
+ :raise DecodeError: This method uses `decode_integer_value`, and thus
+ raises this under the same conditions.
+ :return: the decoded delta-seconds-value
+ :rtype: int
+ """
+ return Decoder.decode_integer_value(byte_iter)
+
+ @staticmethod
+ def decode_q_value(byte_iter):
+ """ From [5], section 8.4.2.1:
+ The encoding is the same as in uint_var-integer, but with restricted
+ size. When quality factor 0 and quality factors with one or two
+ decimal digits are encoded, they shall be multiplied by 100 and
+ incremented by one, so that they encode as a one-octet value in
+ range 1-100, ie, 0.1 is encoded as 11 (0x0B) and 0.99 encoded as
+ 100 (0x64). Three decimal quality factors shall be multiplied with
+ 1000 and incremented by 100, and the result shall be encoded as a
+ one-octet or two-octet uintvar, eg, 0.333 shall be encoded as 0x83
+ 0x31. Quality factor 1 is the default value and shall never be sent.
+
+ :return: The decode quality factor (Q-value)
+ :rtype: float
+ """
+ q_value_int = Decoder.decode_uint_var(byte_iter)
+ # TODO: limit the amount of decimal points
+ if q_value_int > 100:
+ return float(q_value_int - 100) / 1000.0
+
+ return float(q_value_int - 1) / 100.0
+
+ @staticmethod
+ def decode_version_value(byte_iter):
+ """
+ Decodes the version-value.
+
+ From [5], section 8.4.2.3::
+
+ Version-value = Short-integer | Text-string
+
+ :return: the decoded version value in the format, usually in the
+ format: "."
+ :rtype: str
+ """
+ try:
+ byteValue = Decoder.decode_short_integer(byte_iter)
+ major = (byteValue & 0x70) >> 4
+ minor = byteValue & 0x0f
+ return '%d.%d' % (major, minor)
+ except DecodeError:
+ return Decoder.decode_text_string(byte_iter)
+
+ @staticmethod
+ def decode_uri_value(byte_iter):
+ """
+ Stub for Uri-value decoding; see :func:`decode_text_string`
+ """
+ return Decoder.decode_text_string(byte_iter)
+
+ @staticmethod
+ def decode_text_value(byte_iter):
+ """
+ Stub for Parameter Text-value decoding.
+
+ This is used when decoding parameter values; see
+ :func:`decode_typed_value`
+
+ From [5], section 8.4.2.3::
+
+ Text-value = No-value | Token-text | Quoted-string
+
+ :return: The decoded Parameter Text-value
+ :rtype: str
+ """
+ try:
+ return Decoder.decode_token_text(byte_iter)
+ except DecodeError:
+ try:
+ return Decoder.decode_quoted_string(byte_iter)
+ except DecodeError:
+ # Ok, so it's a "No-value"
+ return ''
+
+ @staticmethod
+ def decode_no_value(byte_iter):
+ """
+ Verifies that the byte pointed to by ``byte_iter`` is 0x00.
+
+ If successful, this function will move ``byte_iter`` one byte forward
+
+ :raise DecodeError: If 0x00 is not found; ``byte_iter`` is not modified
+ if this is raised.
+
+ :return: No-value, which is 0x00
+ :rtype: int
+ """
+ byte_iter, local_iter = next(byte_iter)
+ if next(local_iter) != 0x00:
+ raise DecodeError('Expected No-value')
+
+ next(byte_iter)
+ return 0x00
+
+ @staticmethod
+ def decode_accept_value(byte_iter):
+ """
+ most of these things are currently decoded, but discarded (e.g
+ accept-parameters); we only return the media type
+
+ From [5], section 8.4.2.7::
+
+ Accept-value = Constrained-media | Accept-general-form
+ Accept-general-form = Value-length Media-range [Accept-parameters]
+ Media-range = (Well-known-media | Extension-Media) *(Parameter)
+ Accept-parameters = Q-token Q-value *(Accept-extension)
+ Accept-extension = Parameter
+ Q-token =
+
+ :raise DecodeError: The decoding failed. ``byte_iter`` will not be
+ modified in this case.
+ :return: the decoded Accept-value (media/content type)
+ :rtype: str
+ """
+ # Try to use Constrained-media encoding
+ try:
+ accept_value = Decoder.decode_constrained_media(byte_iter)
+ except DecodeError:
+ # ...now try Accept-general-form
+ value_length = Decoder.decode_value_length(byte_iter)
+ try:
+ media = Decoder.decode_well_known_media(byte_iter)
+ except DecodeError:
+ media = Decoder.decode_extension_media(byte_iter)
+
+ # Check for the Q-Token (to see if there are Accept-parameters)
+ if byte_iter.preview() == 128:
+ next(byte_iter)
+ q_value = Decoder.decode_q_value(byte_iter)
+ try:
+ accept_extension = Decoder.decode_parameter(byte_iter)
+ except DecodeError:
+ # Just set an empty iterable
+ accept_extension = []
+
+ byte_iter.reset_preview()
+ accept_value = media
+
+ return accept_value
+
+ @staticmethod
+ def decode_pragma_value(byte_iter):
+ """
+ Decodes the pragma value pointed by ``byte_iter``
+
+ Defined in [5], section 8.4.2.38::
+
+ Pragma-value = No-cache | (Value-length Parameter)
+
+ From [5], section 8.4.2.15::
+
+ No-cache =
+
+ :raise DecodeError: The decoding failed. ``byte_iter`` will not be
+ modified in this case.
+ :return: the decoded Pragma-value, in the format:
+ (, )
+ :rtype: tuple
+ """
+ byte = byte_iter.preview()
+ if byte == 0x80: # No-cache
+ next(byte_iter)
+ # TODO: Not sure if this parameter name (or even usage) is correct
+ name, value = 'Cache-control', 'No-cache'
+ else:
+ byte_iter.reset_preview()
+ value_length = Decoder.decode_value_length(byte_iter)
+ name, value = Decoder.decode_parameter(byte_iter)
+
+ return name, value
+
+ @staticmethod
+ def decode_well_known_charset(byte_iter):
+ """
+ From [5], section 8.4.2.8::
+
+ Well-known-charset = Any-charset | Integer-value
+ Any-charset =
+
+ It is encoded using values from "Character Set Assignments" table.
+
+ Equivalent to the special RFC2616 charset value "*"
+ """
+ # Look for the Any-charset value
+ byte = byte_iter.preview()
+ byte_iter.reset_preview()
+ if byte == 127:
+ next(byte_iter)
+ decoded_charset = '*'
+ else:
+ charset_value = Decoder.decode_integer_value(byte_iter)
+ if charset_value in well_known_charsets:
+ decoded_charset = well_known_charsets[charset_value]
+ else:
+ # This charset is not in our table... so just use the
+ # value (at least for now)
+ decoded_charset = str(charset_value)
+
+ return decoded_charset
+
+ @staticmethod
+ def decode_well_known_header(byte_iter):
+ """
+ Currently, "Wap-value" is decoded as a Text-string in most cases
+
+ From [5], section 8.4.2.6::
+
+ Well-known-header = Well-known-field-name Wap-value
+ Well-known-field-name = Short-integer
+ Wap-value =
+
+
+ :return: The header name, and its value, in the format:
+ (, )
+ :rtype: tuple
+ """
+ field_value = Decoder.decode_short_integer(byte_iter)
+ hdr_fields = get_header_field_names()
+ # TODO: *technically* this can fail, but then we have already
+ # read a byte... should fix?
+ if field_value not in range(len(hdr_fields)):
+ raise DecodeError('Invalid Header Field value: %d' % field_value)
+
+ field_name = hdr_fields[field_value]
+
+ # TODO: make this flow better, and implement it in
+ # decode_application_header also
+ # Currently we decode most headers as text_strings, except
+ # where we have a specific decoding algorithm implemented
+ if field_name in header_field_encodings:
+ wap_value_type = header_field_encodings[field_name]
+ try:
+ decoded_value = getattr(Decoder,
+ 'decode_%s' % wap_value_type)(byte_iter)
+ except DecodeError as msg:
+ raise DecodeError('Could not decode Wap-value: %s' % msg)
+ except:
+ debug('An error occurred, probably due to an '
+ 'unimplemented decoding operation. Tried to '
+ 'decode header: %s' % field_name)
+ raise
+
+ else:
+ decoded_value = Decoder.decode_text_string(byte_iter)
+
+ return field_name, decoded_value
+
+ @staticmethod
+ def decode_application_header(byte_iter):
+ """
+ From [5], section 8.4.2.6::
+
+ Application-header = Token-text Application-specific-value
+
+ From [4], section 7.1::
+ Application-header = Token-text Application-specific-value
+ Application-specific-value = Text-string
+
+ This is used when decoding generic WSP headers; see
+ :func:`decode_header`. We follow [4], and decode the
+ "Application-specific-value" as a Text-string
+
+ :return: The application-header, and its value, in the format:
+ (, )
+ :rtype: tuple
+ """
+ try:
+ app_header = Decoder.decode_token_text(byte_iter)
+ #FNA: added for brute-forcing
+ except DecodeError:
+ app_header = Decoder.decode_text_string(byte_iter)
+
+ app_specific_value = Decoder.decode_text_string(byte_iter)
+ return app_header, app_specific_value
+
+ @staticmethod
+ def decode_header(byte_iter):
+ """
+ Decodes a WSP header entry
+
+ "Shift-sequence" encoding has not been implemented. Currently,
+ almost all header values are treated as text-strings
+
+ From [5], section 8.4.2.6::
+
+ Header = Message-header | Shift-sequence
+ Message-header = Well-known-header | Application-header
+ Well-known-header = Well-known-field-name Wap-value
+ Application-header = Token-text Application-specific-value
+
+
+ :return: The decoded headername, and its value, in the format:
+ (, )
+ :rtype: tuple
+ """
+ # First try decoding the header as a well-known-header
+ try:
+ return Decoder.decode_well_known_header(byte_iter)
+ except DecodeError:
+ # ...now try Application-header encoding
+ return Decoder.decode_application_header(byte_iter)
+
+
+class Encoder:
+ """A WSP Data unit decoder"""
+
+ @staticmethod
+ def encode_uint_8(uint):
+ """
+ Encodes an 8-bit unsigned integer
+
+ :param uint: The integer to encode
+ :type byte_iter: int
+
+ :return: the encoded uint_8, as a sequence of bytes
+ :rtype: list
+ """
+ # Make the byte unsigned
+ return [uint & 0xff]
+
+ @staticmethod
+ def encode_uint_var(uint):
+ """
+ Variable Length Unsigned Integer encoding algorithm
+
+ This binary-encodes the given unsigned integer number as specified
+ in section 8.1.2 of [5]. Basically, each encoded byte has the
+ following structure::
+
+ [0][ Payload ]
+ | ^^^^^^^
+ | 7 bits (actual data)
+ |
+ Continue bit
+
+ The uint is split into 7-bit segments, and the "continue bit" of each
+ used octet is set to '1' to indicate more is to follow; the last used
+ octet's "continue bit" is set to 0.
+
+ :return: the binary-encoded uint_var, as a list of byte values
+ :rtype: list
+ """
+ uint_var = [uint & 0x7f]
+ # Since this is the lowest entry, we do not set the continue bit to 1
+ uint = uint >> 7
+ # ...but for the remaining octets, we have to
+ while uint > 0:
+ uint_var.insert(0, 0x80 | (uint & 0x7f))
+ uint = uint >> 7
+
+ return uint_var
+
+ @staticmethod
+ def encode_text_string(string):
+ """ Encodes a "Text-string" value.
+
+ This follows the basic encoding rules specified in [5], section
+ 8.4.2.1
+
+ :param string: The text string to encode
+ :type string: str
+
+ :return: the null-terminated, binary-encoded version of the
+ specified Text-string, as a list of byte values
+ :rtype: list
+ """
+ encoded_string = [ord(c) for c in string]
+ encoded_string.append(0x00)
+ return encoded_string
+
+ @staticmethod
+ def encode_short_integer(integer):
+ """
+ Encodes the specified short-integer ``integer`` value
+
+ Integers in range 0-127 shall be encoded as a one octet value with
+ the most significant bit set to one (1xxx xxxx) and with the value
+ in the remaining least significant bits.
+
+ The encoding for a long integer is specified in [5], section 8.4.2.1::
+
+ Short-integer = OCTET
+
+ :param integer: The short-integer value to encode
+ :type integer: int
+
+ :raise EncodeError: Not a valid short-integer; the integer must be in
+ the range of 0-127
+
+ :return: The encoded short integer, as a list of byte values
+ :rtype: list
+ """
+ if integer < 0 or integer > 127:
+ raise EncodeError('Short-integer value must be in '
+ 'range 0-127: %d' % integer)
+
+ # Make sure the MSB is set
+ return [integer | 0x80]
+
+ @staticmethod
+ def encode_long_integer(integer):
+ """
+ Encodes a Long-integer value ``integer``
+
+ The encoding for a long integer is specified in [5], section 8.4.2.1;
+ for a description of this encoding scheme, see
+ :func:`wsp.Decoder.decode_long_integer`.
+
+ From [5], section 8.4.2.2::
+
+ Long-integer = Short-length Multi-octet-integer
+ Short-length =
+
+ :raise EncodeError: is not of type "int"
+
+ :param integer: The integer value to encode
+ :type integer: int
+
+ :return: The encoded Long-integer, as a sequence of byte values
+ :rtype: list
+ """
+ if not isinstance(integer, int):
+ raise EncodeError(' must be of type "int"')
+
+ encoded_long_int = []
+ longInt = integer
+ # Encode the Multi-octect-integer
+ while longInt > 0:
+ byte = 0xff & longInt
+ encoded_long_int.append(byte)
+ longInt = longInt >> 8
+
+ # Now add the SHort-length value, and make sure it's ok
+ shortLength = len(encoded_long_int)
+ if shortLength > 30:
+ raise EncodeError('Cannot encode Long-integer value: Short-length '
+ 'is too long; should be in octet range 0-30')
+ encoded_long_int.insert(0, shortLength)
+ return encoded_long_int
+
+ @staticmethod
+ def encode_version_value(version):
+ """
+ Encodes the version-value.
+
+ Example: An MMS version of "1.0" consists of a major version of 1 and a
+ minor version of 0, and would be encoded as 0x90. However, a version
+ of "1.2.4" would be encoded as the Text-string "1.2.4".
+
+ From [5], section 8.4.2.3::
+ Version-value = Short-integer | Text-string
+
+ :param version: The version number to encode, e.g. "1.0"
+ :type version: str
+
+ :raise TypeError: The specified version value was not of type `str`
+
+ :return: the encoded version value, as a list of byte values
+ :rtype: list
+ """
+ if not isinstance(version, str):
+ raise TypeError('Parameter must be of type "str"')
+
+ encoded_version_val = []
+ # First try short-integer encoding
+ try:
+ if len(version.split('.')) <= 2:
+ major_version = int(version.split('.')[0])
+ if major_version < 1 or major_version > 7:
+ raise ValueError('Major version must be in range 1-7')
+
+ major = major_version << 4
+ if len(version.split('.')) == 2:
+ minor_version = int(version.split('.')[1])
+ if minor_version < 0 or minor_version > 14:
+ raise ValueError('Minor version must be in range 0-14')
+ else:
+ minor_version = 15
+
+ minor = minor_version
+ encoded_version_val = Encoder.encode_short_integer(major | minor)
+ except:
+ # The value couldn't be encoded as a short-integer; use
+ # a text-string instead
+ encoded_version_val = Encoder.encode_text_string(version)
+
+ return encoded_version_val
+
+ @staticmethod
+ def encode_media_type(content_type):
+ """
+ Encodes the specified MIME ``content_type`` ("Media-type" value)
+
+ "Well-known-media" takes into account the WSP short form of well-known
+ content types, as specified in section 8.4.2.24 and table 40 of [5].
+
+ From [5], section 8.2.4.24::
+
+ Media-type = (Well-known-media | Extension-Media) *(Parameter)
+
+ :param content_type: The MIME content type to encode
+ :type content_type: str
+
+ :return: The binary-encoded content type, as a list of (integer) byte
+ values
+ :rtype: list
+ """
+ if content_type in well_known_content_types:
+ # Short-integer encoding
+ val = Encoder.encode_short_integer(
+ well_known_content_types.index(content_type))
+ else:
+ val = Encoder.encode_text_string(content_type)
+
+ return [val]
+
+ @staticmethod
+ def encode_parameter(parameter_name, parameter_value, version='1.2'):
+ """
+ Encodes ``parameter_name`` and ``parameter_value`` using ``version``
+
+ Binary-encodes the name of a parameter of -say- a "Content-Type"
+ header entry, taking into account the WSP short form of
+ well-known parameter names, as specified in section 8.4.2.4 and table
+ 38 of [5].
+
+ From [5], section 8.4.2.4::
+
+ Parameter = Typed-parameter | Untyped-parameter
+ Typed-parameter = Well-known-parameter-token Typed-value
+ Untyped-parameter = Token-text Untyped-value
+ Untyped-value = Integer-value | Text-value
+
+ :param parameter_name: The name of the parameter to encode
+ :type parameter_name: str
+ :param parameter_value: The value of the parameter
+ :type parameter_value: str or int
+
+ :param version: The WSP encoding version to use. This defaults
+ to "1.2", but may be "1.1", "1.2", "1.3" or
+ "1.4" (see table 38 in [5] for details).
+ :type version: str
+
+ :raise ValueError: The specified encoding version is invalid.
+
+ :return: The binary-encoded parameter name, as a list of (integer)
+ byte values
+ :rtype: list
+ """
+ wk_params = get_well_known_parameters(version)
+ encoded_parameter = []
+ # Try to encode the parameter using a "Typed-parameter" value
+ wkParamNumbers = list(wk_params.keys())
+ wkParamNumbers.sort(reverse=True)
+ for assigned_number in wkParamNumbers:
+ if wk_params[assigned_number][0] == parameter_name:
+ # Ok, it's a Typed-parameter; encode the parameter name
+ encoded_parameter.extend(
+ Encoder.encode_short_integer(assigned_number))
+ # and now the value
+ expected_type = wk_params[assigned_number][1]
+ try:
+ ret = getattr(Encoder,
+ 'encode_%s' % expected_type)(parameter_value)
+ encoded_parameter.extend(ret)
+ except EncodeError as msg:
+ raise EncodeError('Error encoding param value: %s' % msg)
+ except:
+ debug('A fatal error occurred, probably due to an '
+ 'unimplemented encoding operation')
+ raise
+ break
+
+ # See if the "Typed-parameter" encoding worked
+ if len(encoded_parameter) == 0:
+ # it didn't. Use "Untyped-parameter" encoding
+ encoded_parameter.extend(Encoder.encode_token_text(parameter_name))
+ value = []
+ # First try to encode the untyped-value as an integer
+ try:
+ value = Encoder.encode_integer_value(parameter_value)
+ except EncodeError:
+ value = Encoder.encode_text_string(parameter_value)
+
+ encoded_parameter.extend(value)
+
+ return encoded_parameter
+
+ # TODO: check up on the encoding/decoding of Token-text, in particular,
+ # how does this differ from text-string? does it have 0x00 at the end?
+ @staticmethod
+ def encode_token_text(text):
+ """ From [5], section 8.4.2.1:
+ Token-text = Token End-of-string
+
+ :raise EncodeError: Specified text cannot be encoding as a token
+
+ :return: The encoded token string, as a list of byte values
+ :rtype: list
+ """
+ separators = (11, 32, 40, 41, 44, 47, 58, 59, 60, 61, 62, 63, 64,
+ 91, 92, 93, 123, 125)
+ # Sanity check
+ for char in separators:
+ if chr(char) in text:
+ raise EncodeError('Char "%s" in text string; cannot '
+ 'encode as Token-text' % chr(char))
+
+ return Encoder.encode_text_string(text)
+
+ @staticmethod
+ def encode_integer_value(integer):
+ """Encodes an integer value
+
+ From [5], section 8.4.2.3:
+ Integer-Value = Short-integer | Long-integer
+
+ This function will first try to encode the specified integer value
+ into a short-integer, and failing that, will encode into a
+ long-integer value.
+
+ :param integer: The integer to encode
+ :type integer: int
+
+ :raise EncodeError: The parameter is not of type `int`
+
+ :return: The encoded integer value, as a list of byte values
+ :rtype: list
+ """
+ if not isinstance(integer, int):
+ raise EncodeError(' must be of type "int"')
+
+ # First try and see if it's a short-integer
+ try:
+ return Encoder.encode_short_integer(integer)
+ except EncodeError:
+ return Encoder.encode_long_integer(integer)
+
+ @staticmethod
+ def encode_text_value(text):
+ """Stub for encoding Text-values; see :func:`encode_text_string`"""
+ return Encoder.encode_text_string(text)
+
+ @staticmethod
+ def encode_no_value(value=None):
+ """
+ Encodes a No-value ``value``, which is 0x00
+
+ This function mainly exists for use by automatically-selected
+ encoding routines (see :func:`encode_parameter`) for an example.
+
+ :param value: This value is ignored; it is present so that this
+ method complies with the format of the other `encode`
+ methods.
+
+ :return: A list containing a single "No-value", which is 0x00
+ :rtype: list
+ """
+ return [0x00]
+
+ @staticmethod
+ def encode_header(field_name, value):
+ """
+ Encodes a WSP header entry ``field_name``, and its ``value``
+
+ "Shift-sequence" encoding has not been implemented. Currently,
+ almost all header values are encoded as text-strings
+
+ From [5], section 8.4.2.6::
+
+ Header = Message-header | Shift-sequence
+ Message-header = Well-known-header | Application-header
+ Well-known-header = Well-known-field-name Wap-value
+ Application-header = Token-text Application-specific-value
+
+ :return: The encoded header, and its value, as a sequence of
+ byte values
+ :rtype: list
+ """
+ encoded_header = []
+ # First try encoding the header name as a "well-known-header"...
+ wkHdrFields = get_header_field_names()
+ if field_name in wkHdrFields:
+ header_field_value = Encoder.encode_short_integer(
+ wkHdrFields.index(field_name))
+ encoded_header.extend(header_field_value)
+ else:
+ # otherwise, encode it as an "application header"
+ encoded_header_name = Encoder.encode_token_text(field_name)
+ encoded_header.extend(encoded_header_name)
+
+ # Now add the value
+ # TODO: make this flow better (see also Decoder.decode_header)
+ # most header values are encoded as text_strings, except where we
+ # have a specific Wap-value encoding implementation
+ if field_name in header_field_encodings:
+ wap_value_type = header_field_encodings[field_name]
+ try:
+ ret = getattr(Encoder, 'encode_%s' % wap_value_type)(value)
+ encoded_header.extend(ret)
+ except EncodeError as msg:
+ raise EncodeError('Error encoding Wap-value: %s' % msg)
+ except:
+ debug('A fatal error occurred, probably due to an '
+ 'unimplemented encoding operation')
+ raise
+ else:
+ encoded_header.extend(Encoder.encode_text_string(value))
+
+ return encoded_header
+
+ @staticmethod
+ def encode_content_type_value(media_type, parameters):
+ """
+ Encodes a content type, and its parameters
+
+ The short form of the Content-type-value MUST only be used when the
+ well-known media is in the range of 0-127 or a text string. In all
+ other cases the general form MUST be used.
+
+ From [5], section 8.4.2.24::
+
+ Content-type-value = Constrained-media | Content-general-form
+
+ :return: The encoded Content-type-value (including parameters, if
+ any), as a sequence of bytes
+ :rtype: list
+ """
+ # First try do encode it using Constrained-media encoding
+ try:
+ if len(parameters):
+ raise EncodeError('Need to use '
+ 'Content-general-form for parameters')
+
+ return Encoder.encode_constrained_media(media_type)
+ except EncodeError:
+ # Try the general form
+ return Encoder.encode_content_general_form(media_type, parameters)
+
+ @staticmethod
+ def encode_constrained_media(media_type):
+ """
+ Encodes the constrained media ``media_type``
+
+ It is encoded using values from the "Content Type Assignments" table.
+
+ From [5], section 8.4.2.7::
+
+ Constrained-media = Constrained-encoding
+
+ :param media_type: The media type to encode
+ :type media_type: str
+
+ :raise EncodeError: Media value is unsuitable for Constrained-encoding
+
+ :return: The encoded media type, as a sequence of bytes
+ :rtype: list
+ """
+ # See if this value is in the table of well-known content types
+ if media_type in well_known_content_types:
+ value = well_known_content_types.index(media_type)
+ else:
+ value = media_type
+
+ return Encoder.encode_constrained_encoding(value)
+
+ @staticmethod
+ def encode_constrained_encoding(value):
+ """
+ Constrained-encoding = Extension-Media --or-- Short-integer
+
+ This encoding is used for token values, which have no well-known
+ binary encoding, or when the assigned number of the well-known
+ encoding is small enough to fit into Short-integer.
+
+ :param value: The value to encode
+ :type value: int or str
+
+ :raise EncodeError: cannot be encoded as a
+ Constrained-encoding sequence
+
+ :return: The encoded constrained-encoding token value, as a sequence
+ of bytes
+ :rtype: list
+ """
+ encoded_value = None
+ if isinstance(value, int):
+ # First try and encode the value as a short-integer
+ encoded_value = Encoder.encode_short_integer(value)
+ else:
+ # Ok, it should be Extension-Media then
+ try:
+ encoded_value = Encoder.encode_extension_media(value)
+ except EncodeError:
+ # Give up
+ raise EncodeError('Cannot encode %s as a '
+ 'Constrained-encoding sequence' % str(value))
+
+ return encoded_value
+
+ @staticmethod
+ def encode_extension_media(media_value):
+ """
+ Encodes the extension media ``media_value``
+
+ This encoding is used for media values, which have no well-known
+ binary encoding
+
+ From [5], section 8.4.2.1::
+
+ Extension-media = *TEXT End-of-string
+
+ :param media_value: The media value (string) to encode
+ :type media_value: str
+
+ :raise EncodeError: The value cannot be encoded as TEXT; probably it
+ starts with/contains an invalid character
+
+ :return: The encoded media type value, as a sequence of bytes
+ :rtype: str
+ """
+ if not isinstance(media_value, str):
+ try:
+ media_value = str(media_value)
+ except:
+ raise EncodeError('Invalid Extension-media: Cannot convert '
+ 'value to text string')
+ char = media_value[0]
+ if ord(char) < 32 or ord(char) == 127:
+ raise EncodeError('Invalid Extension-media: TEXT starts with '
+ 'invalid character: %s' % ord(char))
+
+ return Encoder.encode_text_string(media_value)
+
+ @staticmethod
+ def encode_content_general_form(media_type, parameters):
+ """
+ From [5], section 8.4.2.24::
+
+ Content-general-form = Value-length Media-type
+
+ Used in decoding Content-type fields and their parameters;
+ see :func:`decode_content_type_value`. Used by
+ :func:`decode_content_type_value`.
+
+ :return: The encoded Content-general-form, as a sequence of bytes
+ :rtype: list
+ """
+ enconded_content_general_form = []
+ encoded_media_type = []
+ # Encode the actual content type
+ encoded_media_type = Encoder.encode_media_type(media_type)
+ # Encode all parameters
+ encoded_parameters = [Encoder.encode_parameter(name, parameters[name])
+ for name in parameters]
+
+ value_length = len(encoded_media_type) + len(encoded_parameters)
+ encoded_value_length = Encoder.encode_value_length(value_length)
+ enconded_content_general_form.extend(encoded_value_length)
+ enconded_content_general_form.extend(encoded_media_type)
+ enconded_content_general_form.extend(encoded_parameters)
+
+ return enconded_content_general_form
+
+ @staticmethod
+ def encode_value_length(length):
+ """
+ Encodes the specified length value as a value length indicator
+
+ "Value length" is used to indicate the length of a value to follow, as
+ used in the `Content-Type` header in the MMS body, for example.
+
+ The encoding for a value length indicator is specified in [5],
+ section 8.4.2.2, and follows the form::
+
+ Value-length = [Short-length] --or-- [Length-quote] [Length]
+ ^^^^^^ ^^^^^^ ^^^^^^
+ 1 byte 1 byte x bytes
+ uint_var-integer
+
+ :raise EncodeError: The value_length could not be encoded.
+
+ :return: The encoded value length indicator, as a sequence of bytes
+ :rtype: list
+ """
+ encoded_value_length = []
+ # Try and encode it as a short-length
+ try:
+ encoded_value_length = Encoder.encode_short_length(length)
+ except EncodeError:
+ # Encode it with a Length-quote and uint_var
+ encoded_value_length.append(31) # Length-quote
+ encoded_value_length.extend(Encoder.encode_uint_var(length))
+
+ return encoded_value_length
+
+ @staticmethod
+ def encode_short_length(length):
+ """
+ From [5], section 8.4.2.2::
+
+ Short-length =
+
+ :raise EncodeError: The specified cannot be encoded as a
+ short-length value; it is not in octet range 0-30.
+
+ :return: The encoded short-length, as a sequence of bytes
+ :rtype: list
+ """
+ if length < 0 or length > 30:
+ raise EncodeError('Cannot encode short-length; length should '
+ 'be in the 0-30 range')
+
+ return [length]
+
+ @staticmethod
+ def encode_accept_value(accept_value):
+ """
+ From [5], section 8.4.2.7::
+
+ Accept-value = Constrained-media | Accept-general-form
+ Accept-general-form = Value-length Media-range [Accept-parameters]
+ Media-range = (Well-known-media | Extension-Media) *(Parameter)
+ Accept-parameters = Q-token Q-value *(Accept-extension)
+ Accept-extension = Parameter
+ Q-token =
+
+ :note: This implementation does not currently support encoding of
+ "Accept-parameters".
+
+ :param accept_value: The Accept-value to encode (media/content type)
+ :type accept_value: str
+
+ :raise EncodeError: The encoding failed.
+
+ :return: The encoded Accept-value, as a sequence of bytes
+ :rtype: list
+ """
+ encoded_accept_value = []
+ # Try to use Constrained-media encoding
+ try:
+ encoded_accept_value = Encoder.encode_constrained_media(accept_value)
+ except EncodeError:
+ # ...now try Accept-general-form
+ try:
+ encoded_media_range = Encoder.encode_media_type(accept_value)
+ except EncodeError as msg:
+ raise EncodeError('Cannot encode Accept-value: %s' % msg)
+
+ value_length = Encoder.encode_value_length(len(encoded_media_range))
+ encoded_accept_value = value_length
+ encoded_accept_value.extend(encoded_media_range)
+
+ return encoded_accept_value
diff --git a/messaging/sms/__init__.py b/messaging/sms/__init__.py
new file mode 100644
index 0000000..f00ad10
--- /dev/null
+++ b/messaging/sms/__init__.py
@@ -0,0 +1,7 @@
+# See LICENSE
+
+from messaging.sms.submit import SmsSubmit
+from messaging.sms.deliver import SmsDeliver
+from messaging.sms.gsm0338 import is_gsm_text
+
+__all__ = ["SmsSubmit", "SmsDeliver", "is_gsm_text"]
diff --git a/messaging/sms/base.py b/messaging/sms/base.py
new file mode 100644
index 0000000..b7b1d59
--- /dev/null
+++ b/messaging/sms/base.py
@@ -0,0 +1,14 @@
+# see LICENSE
+
+
+class SmsBase(object):
+
+ def __init__(self):
+ self.udh = None
+ self.number = None
+ self.text = None
+ self.fmt = None
+ self.dcs = None
+ self.pid = None
+ self.csca = None
+ self.type = None
diff --git a/messaging/sms/consts.py b/messaging/sms/consts.py
new file mode 100644
index 0000000..caf7806
--- /dev/null
+++ b/messaging/sms/consts.py
@@ -0,0 +1,17 @@
+# see LICENSE
+SEVENBIT_SIZE = 160
+EIGHTBIT_SIZE = 140
+UCS2_SIZE = 70
+SEVENBIT_MP_SIZE = SEVENBIT_SIZE - 7
+EIGHTBIT_MP_SIZE = EIGHTBIT_SIZE - 6
+UCS2_MP_SIZE = UCS2_SIZE - 3
+
+# address type
+UNKNOWN = 0
+INTERNATIONAL = 1
+NATIONAL = 2
+NETWORK_SPECIFIC = 3
+SUBSCRIBER = 4
+ALPHANUMERIC = 5
+ABBREVIATED = 6
+RESERVED = 7
diff --git a/messaging/sms/deliver.py b/messaging/sms/deliver.py
new file mode 100644
index 0000000..37714e6
--- /dev/null
+++ b/messaging/sms/deliver.py
@@ -0,0 +1,264 @@
+# see LICENSE
+"""Classes for processing received SMS"""
+
+from datetime import datetime, timedelta
+
+from messaging.utils import (swap, swap_number, encode_bytes, debug,
+ unpack_msg, unpack_msg2, to_array)
+from messaging.sms import consts
+from messaging.sms.base import SmsBase
+from messaging.sms.udh import UserDataHeader
+
+
+class SmsDeliver(SmsBase):
+ """I am a delivered SMS in your Inbox"""
+
+ def __init__(self, pdu, strict=True):
+ super(SmsDeliver, self).__init__()
+ self._pdu = None
+ self._strict = strict
+ self.date = None
+ self.mtype = None
+ self.sr = None
+
+ self.pdu = pdu
+
+ @property
+ def data(self):
+ """
+ Returns a dict populated with the SMS attributes
+
+ It mimics the old API to ease the port to the new API
+ """
+ ret = {
+ 'text': self.text,
+ 'pid': self.pid,
+ 'dcs': self.dcs,
+ 'csca': self.csca,
+ 'number': self.number,
+ 'type': self.type,
+ 'date': self.date,
+ 'fmt': self.fmt,
+ 'sr': self.sr,
+ }
+
+ if self.udh is not None:
+ if self.udh.concat is not None:
+ ret.update({
+ 'ref': self.udh.concat.ref,
+ 'cnt': self.udh.concat.cnt,
+ 'seq': self.udh.concat.seq,
+ })
+
+ return ret
+
+ def _set_pdu(self, pdu):
+ if not self._strict and len(pdu) % 2:
+ # if not strict and PDU-length is odd, remove the last character
+ # and make it even. See the discussion of this bug at
+ # http://github.com/pmarti/python-messaging/issues#issue/7
+ pdu = pdu[:-1]
+
+ if len(pdu) % 2:
+ raise ValueError("Can not decode an odd-length pdu")
+
+ # XXX: Should we keep the original PDU or the modified one?
+ self._pdu = pdu
+
+ data = to_array(self._pdu)
+
+ # Service centre address
+ smscl = data.pop(0)
+ if smscl > 0:
+ smscertype = data.pop(0)
+ smscl -= 1
+ self.csca = swap_number(encode_bytes(data[:smscl]))
+ if (smscertype >> 4) & 0x07 == consts.INTERNATIONAL:
+ self.csca = '+%s' % self.csca
+ data = data[smscl:]
+ else:
+ self.csca = None
+
+ # 1 byte(octet) == 2 char
+ # Message type TP-MTI bits 0,1
+ # More messages to send/deliver bit 2
+ # Status report request indicated bit 5
+ # User Data Header Indicator bit 6
+ # Reply path set bit 7
+ try:
+ self.mtype = data.pop(0)
+ except TypeError:
+ raise ValueError("Decoding this type of SMS is not supported yet")
+
+ mtype = self.mtype & 0x03
+
+ if mtype == 0x02:
+ return self._decode_status_report_pdu(data)
+
+ if mtype == 0x01:
+ raise ValueError("Cannot decode a SmsSubmitReport message yet")
+
+ sndlen = data.pop(0)
+ if sndlen % 2:
+ sndlen += 1
+ sndlen = int(sndlen / 2.0)
+
+ sndtype = (data.pop(0) >> 4) & 0x07
+ if sndtype == consts.ALPHANUMERIC:
+ # coded according to 3GPP TS 23.038 [9] GSM 7-bit default alphabet
+ sender = unpack_msg2(data[:sndlen]).decode("gsm0338")
+ else:
+ # Extract phone number of sender
+ sender = swap_number(encode_bytes(data[:sndlen]))
+ if sndtype == consts.INTERNATIONAL:
+ sender = '+%s' % sender
+
+ self.number = sender
+ data = data[sndlen:]
+
+ # 1 byte TP-PID (Protocol IDentifier)
+ self.pid = data.pop(0)
+ # 1 byte TP-DCS (Data Coding Scheme)
+ self.dcs = data.pop(0)
+ if self.dcs & (0x04 | 0x08) == 0:
+ self.fmt = 0x00
+ elif self.dcs & 0x04:
+ self.fmt = 0x04
+ elif self.dcs & 0x08:
+ self.fmt = 0x08
+
+ datestr = ''
+ # Get date stamp (sender's local time)
+ date = list(encode_bytes(data[:6]))
+ for n in range(1, len(date), 2):
+ date[n - 1], date[n] = date[n], date[n - 1]
+
+ data = data[6:]
+
+ # Get sender's offset from GMT (TS 23.040 TP-SCTS)
+ tz = data.pop(0)
+
+ offset = ((tz & 0x07) * 10 + ((tz & 0xf0) >> 4)) * 15
+ if (tz & 0x08):
+ offset = offset * -1
+
+ # 02/08/26 19:37:41
+ datestr = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date)
+ outputfmt = '%y/%m/%d %H:%M:%S'
+
+ sndlocaltime = datetime.strptime(datestr, outputfmt)
+ sndoffset = timedelta(minutes=offset)
+ # date as UTC
+ self.date = sndlocaltime - sndoffset
+
+ self._process_message(data)
+
+ def _process_message(self, data):
+ # Now get message body
+ msgl = data.pop(0)
+ msg = encode_bytes(data[:msgl])
+ # check for header
+ headlen = ud_len = 0
+
+ if self.mtype & 0x40: # UDHI present
+ ud_len = data.pop(0)
+ self.udh = UserDataHeader.from_bytes(data[:ud_len])
+ headlen = (ud_len + 1) * 8
+ if self.fmt == 0x00:
+ while headlen % 7:
+ headlen += 1
+ headlen /= 7
+
+ headlen = int(headlen)
+
+ if self.fmt == 0x00:
+ # XXX: Use unpack_msg2
+ data = data[ud_len:].tolist()
+ #self.text = unpack_msg2(data).decode("gsm0338")
+ self.text = unpack_msg(msg)[headlen:msgl].decode("gsm0338")
+
+ elif self.fmt == 0x04:
+ self.text = data[ud_len:].tostring()
+
+ elif self.fmt == 0x08:
+ data = data[ud_len:].tolist()
+ _bytes = [int("%02X%02X" % (data[i], data[i + 1]), 16)
+ for i in range(0, len(data), 2)]
+ self.text = ''.join(list(map(chr, _bytes)))
+
+ pdu = property(lambda self: self._pdu, _set_pdu)
+
+ def _decode_status_report_pdu(self, data):
+ self.udh = UserDataHeader.from_status_report_ref(data.pop(0))
+
+ sndlen = data.pop(0)
+ if sndlen % 2:
+ sndlen += 1
+ sndlen = int(sndlen / 2.0)
+
+ sndtype = data.pop(0)
+ recipient = swap_number(encode_bytes(data[:sndlen]))
+ if (sndtype >> 4) & 0x07 == consts.INTERNATIONAL:
+ recipient = '+%s' % recipient
+
+ data = data[sndlen:]
+
+ date = swap(list(encode_bytes(data[:7])))
+ try:
+ scts_str = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date[0:12])
+ self.date = datetime.strptime(scts_str, "%y/%m/%d %H:%M:%S")
+ except (ValueError, TypeError):
+ scts_str = ''
+ debug('Could not decode scts: %s' % date)
+
+ data = data[7:]
+
+ date = swap(list(encode_bytes(data[:7])))
+ try:
+ dt_str = "%s%s/%s%s/%s%s %s%s:%s%s:%s%s" % tuple(date[0:12])
+ dt = datetime.strptime(dt_str, "%y/%m/%d %H:%M:%S")
+ except (ValueError, TypeError):
+ dt_str = ''
+ dt = None
+ debug('Could not decode date: %s' % date)
+
+ data = data[7:]
+
+ msg_l = [recipient, scts_str]
+ try:
+ status = data.pop(0)
+ except IndexError:
+ # Yes it is entirely possible that a status report comes
+ # with no status at all! I'm faking for now the values and
+ # set it to SR-UNKNOWN as that's all we can do
+ _status = None
+ status = 0x1
+ sender = 'SR-UNKNOWN'
+ msg_l.append(dt_str)
+ else:
+ _status = status
+ if status == 0x00:
+ msg_l.append(dt_str)
+ else:
+ msg_l.append('')
+ if status == 0x00:
+ sender = "SR-OK"
+ elif status == 0x1:
+ sender = "SR-UNKNOWN"
+ elif status == 0x30:
+ sender = "SR-STORED"
+ else:
+ sender = "SR-UNKNOWN"
+
+ self.number = sender
+ self.text = "|".join(msg_l)
+ self.fmt = 0x08 # UCS2
+ self.type = 0x03 # status report
+
+ self.sr = {
+ 'recipient': recipient,
+ 'scts': self.date,
+ 'dt': dt,
+ 'status': _status
+ }
+
diff --git a/messaging/sms/gsm0338.py b/messaging/sms/gsm0338.py
new file mode 100644
index 0000000..10e4ae7
--- /dev/null
+++ b/messaging/sms/gsm0338.py
@@ -0,0 +1,292 @@
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import codecs
+import sys
+import traceback
+
+# data from
+# http://snoops.roy202.org/testerman/browser/trunk/plugins/codecs/gsm0338.py
+
+# default GSM 03.38 -> unicode
+def_regular_decode_dict = {
+ '\x00': u'\u0040', # COMMERCIAL AT
+ '\x01': u'\u00A3', # POUND SIGN
+ '\x02': u'\u0024', # DOLLAR SIGN
+ '\x03': u'\u00A5', # YEN SIGN
+ '\x04': u'\u00E8', # LATIN SMALL LETTER E WITH GRAVE
+ '\x05': u'\u00E9', # LATIN SMALL LETTER E WITH ACUTE
+ '\x06': u'\u00F9', # LATIN SMALL LETTER U WITH GRAVE
+ '\x07': u'\u00EC', # LATIN SMALL LETTER I WITH GRAVE
+ '\x08': u'\u00F2', # LATIN SMALL LETTER O WITH GRAVE
+ '\x09': u'\u00C7', # LATIN CAPITAL LETTER C WITH CEDILLA
+ # The Unicode page suggests this is a mistake: but
+ # it's still in the latest version of the spec and
+ # our implementation has to be exact.
+
+ '\x0A': u'\u000A', # LINE FEED
+ '\x0B': u'\u00D8', # LATIN CAPITAL LETTER O WITH STROKE
+ '\x0C': u'\u00F8', # LATIN SMALL LETTER O WITH STROKE
+ '\x0D': u'\u000D', # CARRIAGE RETURN
+ '\x0E': u'\u00C5', # LATIN CAPITAL LETTER A WITH RING ABOVE
+ '\x0F': u'\u00E5', # LATIN SMALL LETTER A WITH RING ABOVE
+ '\x10': u'\u0394', # GREEK CAPITAL LETTER DELTA
+ '\x11': u'\u005F', # LOW LINE
+ '\x12': u'\u03A6', # GREEK CAPITAL LETTER PHI
+ '\x13': u'\u0393', # GREEK CAPITAL LETTER GAMMA
+ '\x14': u'\u039B', # GREEK CAPITAL LETTER LAMDA
+ '\x15': u'\u03A9', # GREEK CAPITAL LETTER OMEGA
+ '\x16': u'\u03A0', # GREEK CAPITAL LETTER PI
+ '\x17': u'\u03A8', # GREEK CAPITAL LETTER PSI
+ '\x18': u'\u03A3', # GREEK CAPITAL LETTER SIGMA
+ '\x19': u'\u0398', # GREEK CAPITAL LETTER THETA
+ '\x1A': u'\u039E', # GREEK CAPITAL LETTER XI
+ '\x1C': u'\u00C6', # LATIN CAPITAL LETTER AE
+ '\x1D': u'\u00E6', # LATIN SMALL LETTER AE
+ '\x1E': u'\u00DF', # LATIN SMALL LETTER SHARP S (German)
+ '\x1F': u'\u00C9', # LATIN CAPITAL LETTER E WITH ACUTE
+ '\x20': u'\u0020', # SPACE
+ '\x21': u'\u0021', # EXCLAMATION MARK
+ '\x22': u'\u0022', # QUOTATION MARK
+ '\x23': u'\u0023', # NUMBER SIGN
+ '\x24': u'\u00A4', # CURRENCY SIGN
+ '\x25': u'\u0025', # PERCENT SIGN
+ '\x26': u'\u0026', # AMPERSAND
+ '\x27': u'\u0027', # APOSTROPHE
+ '\x28': u'\u0028', # LEFT PARENTHESIS
+ '\x29': u'\u0029', # RIGHT PARENTHESIS
+ '\x2A': u'\u002A', # ASTERISK
+ '\x2B': u'\u002B', # PLUS SIGN
+ '\x2C': u'\u002C', # COMMA
+ '\x2D': u'\u002D', # HYPHEN-MINUS
+ '\x2E': u'\u002E', # FULL STOP
+ '\x2F': u'\u002F', # SOLIDUS
+ '\x30': u'\u0030', # DIGIT ZERO
+ '\x31': u'\u0031', # DIGIT ONE
+ '\x32': u'\u0032', # DIGIT TWO
+ '\x33': u'\u0033', # DIGIT THREE
+ '\x34': u'\u0034', # DIGIT FOUR
+ '\x35': u'\u0035', # DIGIT FIVE
+ '\x36': u'\u0036', # DIGIT SIX
+ '\x37': u'\u0037', # DIGIT SEVEN
+ '\x38': u'\u0038', # DIGIT EIGHT
+ '\x39': u'\u0039', # DIGIT NINE
+ '\x3A': u'\u003A', # COLON
+ '\x3B': u'\u003B', # SEMICOLON
+ '\x3C': u'\u003C', # LESS-THAN SIGN
+ '\x3D': u'\u003D', # EQUALS SIGN
+ '\x3E': u'\u003E', # GREATER-THAN SIGN
+ '\x3F': u'\u003F', # QUESTION MARK
+ '\x40': u'\u00A1', # INVERTED EXCLAMATION MARK
+ '\x41': u'\u0041', # LATIN CAPITAL LETTER A
+ '\x42': u'\u0042', # LATIN CAPITAL LETTER B
+ '\x43': u'\u0043', # LATIN CAPITAL LETTER C
+ '\x44': u'\u0044', # LATIN CAPITAL LETTER D
+ '\x45': u'\u0045', # LATIN CAPITAL LETTER E
+ '\x46': u'\u0046', # LATIN CAPITAL LETTER F
+ '\x47': u'\u0047', # LATIN CAPITAL LETTER G
+ '\x48': u'\u0048', # LATIN CAPITAL LETTER H
+ '\x49': u'\u0049', # LATIN CAPITAL LETTER I
+ '\x4A': u'\u004A', # LATIN CAPITAL LETTER J
+ '\x4B': u'\u004B', # LATIN CAPITAL LETTER K
+ '\x4C': u'\u004C', # LATIN CAPITAL LETTER L
+ '\x4D': u'\u004D', # LATIN CAPITAL LETTER M
+ '\x4E': u'\u004E', # LATIN CAPITAL LETTER N
+ '\x4F': u'\u004F', # LATIN CAPITAL LETTER O
+ '\x50': u'\u0050', # LATIN CAPITAL LETTER P
+ '\x51': u'\u0051', # LATIN CAPITAL LETTER Q
+ '\x52': u'\u0052', # LATIN CAPITAL LETTER R
+ '\x53': u'\u0053', # LATIN CAPITAL LETTER S
+ '\x54': u'\u0054', # LATIN CAPITAL LETTER T
+ '\x55': u'\u0055', # LATIN CAPITAL LETTER U
+ '\x56': u'\u0056', # LATIN CAPITAL LETTER V
+ '\x57': u'\u0057', # LATIN CAPITAL LETTER W
+ '\x58': u'\u0058', # LATIN CAPITAL LETTER X
+ '\x59': u'\u0059', # LATIN CAPITAL LETTER Y
+ '\x5A': u'\u005A', # LATIN CAPITAL LETTER Z
+ '\x5B': u'\u00C4', # LATIN CAPITAL LETTER A WITH DIAERESIS
+ '\x5C': u'\u00D6', # LATIN CAPITAL LETTER O WITH DIAERESIS
+ '\x5D': u'\u00D1', # LATIN CAPITAL LETTER N WITH TILDE
+ '\x5E': u'\u00DC', # LATIN CAPITAL LETTER U WITH DIAERESIS
+ '\x5F': u'\u00A7', # SECTION SIGN
+ '\x60': u'\u00BF', # INVERTED QUESTION MARK
+ '\x61': u'\u0061', # LATIN SMALL LETTER A
+ '\x62': u'\u0062', # LATIN SMALL LETTER B
+ '\x63': u'\u0063', # LATIN SMALL LETTER C
+ '\x64': u'\u0064', # LATIN SMALL LETTER D
+ '\x65': u'\u0065', # LATIN SMALL LETTER E
+ '\x66': u'\u0066', # LATIN SMALL LETTER F
+ '\x67': u'\u0067', # LATIN SMALL LETTER G
+ '\x68': u'\u0068', # LATIN SMALL LETTER H
+ '\x69': u'\u0069', # LATIN SMALL LETTER I
+ '\x6A': u'\u006A', # LATIN SMALL LETTER J
+ '\x6B': u'\u006B', # LATIN SMALL LETTER K
+ '\x6C': u'\u006C', # LATIN SMALL LETTER L
+ '\x6D': u'\u006D', # LATIN SMALL LETTER M
+ '\x6E': u'\u006E', # LATIN SMALL LETTER N
+ '\x6F': u'\u006F', # LATIN SMALL LETTER O
+ '\x70': u'\u0070', # LATIN SMALL LETTER P
+ '\x71': u'\u0071', # LATIN SMALL LETTER Q
+ '\x72': u'\u0072', # LATIN SMALL LETTER R
+ '\x73': u'\u0073', # LATIN SMALL LETTER S
+ '\x74': u'\u0074', # LATIN SMALL LETTER T
+ '\x75': u'\u0075', # LATIN SMALL LETTER U
+ '\x76': u'\u0076', # LATIN SMALL LETTER V
+ '\x77': u'\u0077', # LATIN SMALL LETTER W
+ '\x78': u'\u0078', # LATIN SMALL LETTER X
+ '\x79': u'\u0079', # LATIN SMALL LETTER Y
+ '\x7A': u'\u007A', # LATIN SMALL LETTER Z
+ '\x7B': u'\u00E4', # LATIN SMALL LETTER A WITH DIAERESIS
+ '\x7C': u'\u00F6', # LATIN SMALL LETTER O WITH DIAERESIS
+ '\x7D': u'\u00F1', # LATIN SMALL LETTER N WITH TILDE
+ '\x7E': u'\u00FC', # LATIN SMALL LETTER U WITH DIAERESIS
+ '\x7F': u'\u00E0', # LATIN SMALL LETTER A WITH GRAVE
+}
+
+# default GSM 03.38 escaped characters -> unicode
+def_escape_decode_dict = {
+ '\x0A': u'\u000C', # FORM FEED
+ '\x14': u'\u005E', # CIRCUMFLEX ACCENT
+ '\x28': u'\u007B', # LEFT CURLY BRACKET
+ '\x29': u'\u007D', # RIGHT CURLY BRACKET
+ '\x2F': u'\u005C', # REVERSE SOLIDUS
+ '\x3C': u'\u005B', # LEFT SQUARE BRACKET
+ '\x3D': u'\u007E', # TILDE
+ '\x3E': u'\u005D', # RIGHT SQUARE BRACKET
+ '\x40': u'\u007C', # VERTICAL LINE
+ '\x65': u'\u20AC', # EURO SIGN
+}
+
+# Replacement characters, default is question mark. Used when it is not too
+# important to ensure exact UTF-8 -> GSM -> UTF-8 equivilence, such as when
+# humans read and write SMS. But for USSD and other M2M applications it's
+# important to ensure the conversion is exact.
+def_replace_encode_dict = {
+ u'\u00E7': '\x09', # LATIN SMALL LETTER C WITH CEDILLA
+
+ u'\u0391': '\x41', # GREEK CAPITAL LETTER ALPHA
+ u'\u0392': '\x42', # GREEK CAPITAL LETTER BETA
+ u'\u0395': '\x45', # GREEK CAPITAL LETTER EPSILON
+ u'\u0397': '\x48', # GREEK CAPITAL LETTER ETA
+ u'\u0399': '\x49', # GREEK CAPITAL LETTER IOTA
+ u'\u039A': '\x4B', # GREEK CAPITAL LETTER KAPPA
+ u'\u039C': '\x4D', # GREEK CAPITAL LETTER MU
+ u'\u039D': '\x4E', # GREEK CAPITAL LETTER NU
+ u'\u039F': '\x4F', # GREEK CAPITAL LETTER OMICRON
+ u'\u03A1': '\x50', # GREEK CAPITAL LETTER RHO
+ u'\u03A4': '\x54', # GREEK CAPITAL LETTER TAU
+ u'\u03A7': '\x58', # GREEK CAPITAL LETTER CHI
+ u'\u03A5': '\x59', # GREEK CAPITAL LETTER UPSILON
+ u'\u0396': '\x5A', # GREEK CAPITAL LETTER ZETA
+}
+
+QUESTION_MARK = chr(0x3f)
+
+# unicode -> default GSM 03.38
+def_regular_encode_dict = \
+ dict((u, g) for g, u in iter(def_regular_decode_dict.items()))
+
+# unicode -> default escaped GSM 03.38 characters
+def_escape_encode_dict = \
+ dict((u, g) for g, u in iter(def_escape_decode_dict.items()))
+
+
+def encode(input_, errors='strict'):
+ """
+ :type input_: unicode
+
+ :return: string
+ """
+ result = []
+ for c in input_:
+ try:
+ result.append(def_regular_encode_dict[c])
+ except KeyError:
+ if c in def_escape_encode_dict:
+ # OK, let's encode it as an escaped characters
+ result.append('\x1b')
+ result.append(def_escape_encode_dict[c])
+ else:
+ if errors == 'strict':
+ raise UnicodeError("Invalid GSM character")
+ elif errors == 'replace':
+ result.append(
+ def_replace_encode_dict.get(c, QUESTION_MARK))
+ elif errors == 'ignore':
+ pass
+ else:
+ raise UnicodeError("Unknown error handling")
+
+ ret = ''.join(result)
+ return ret, len(ret)
+
+
+def decode(input_, errors='strict'):
+ """
+ :type input_: str
+
+ :return: unicode
+ """
+ result = []
+ index = 0
+ while index < len(input_):
+ c = input_[index]
+ index += 1
+ if c == '\x1b':
+ if index < len(input_):
+ c = input_[index]
+ index += 1
+ result.append(def_escape_decode_dict.get(c, u'\xa0'))
+ else:
+ result.append(u'\xa0')
+ else:
+ try:
+ result.append(def_regular_decode_dict[c])
+ except KeyError:
+ # error handling: unassigned byte, must be > 0x7f
+ if errors == 'strict':
+ raise UnicodeError("Unrecognized GSM character")
+ elif errors == 'replace':
+ result.append('?')
+ elif errors == 'ignore':
+ pass
+ else:
+ raise UnicodeError("Unknown error handling")
+
+ ret = u''.join(result)
+ return ret, len(ret)
+
+
+# encodings module API
+def getregentry(encoding):
+ if encoding == 'gsm0338':
+ return codecs.CodecInfo(name='gsm0338',
+ encode=encode,
+ decode=decode)
+
+# Codec registration
+codecs.register(getregentry)
+
+
+def is_gsm_text(text):
+ """Returns True if ``text`` can be encoded as gsm text"""
+ try:
+ text.encode("gsm0338")
+ except UnicodeError:
+ return False
+ except:
+ traceback.print_exc(file=sys.stdout)
+ return False
+
+ return True
diff --git a/messaging/sms/pdu.py b/messaging/sms/pdu.py
new file mode 100644
index 0000000..9d680d5
--- /dev/null
+++ b/messaging/sms/pdu.py
@@ -0,0 +1,10 @@
+# see LICENSE
+
+
+class Pdu(object):
+
+ def __init__(self, pdu, len_smsc, cnt=1, seq=1):
+ self.pdu = pdu.upper()
+ self.length = len(pdu) / 2 - len_smsc
+ self.cnt = cnt
+ self.seq = seq
diff --git a/messaging/sms/submit.py b/messaging/sms/submit.py
new file mode 100644
index 0000000..f0e9941
--- /dev/null
+++ b/messaging/sms/submit.py
@@ -0,0 +1,330 @@
+# See LICENSE
+"""Classes for sending SMS"""
+
+from datetime import datetime, timedelta
+import re
+
+from messaging.sms import consts
+from messaging.utils import (debug, encode_str, clean_number,
+ pack_8bits_to_ucs2, pack_8bits_to_7bits,
+ pack_8bits_to_8bit,
+ timedelta_to_relative_validity,
+ datetime_to_absolute_validity)
+from messaging.sms.base import SmsBase
+from messaging.sms.gsm0338 import is_gsm_text
+from messaging.sms.pdu import Pdu
+
+VALID_NUMBER = re.compile("^\+?\d{3,20}$")
+
+
+class SmsSubmit(SmsBase):
+ """I am a SMS ready to be sent"""
+
+ def __init__(self, number, text):
+ super(SmsSubmit, self).__init__()
+ self._number = None
+ self._csca = None
+ self._klass = None
+ self._validity = None
+ self.request_status = False
+ self.ref = None
+ self.rand_id = None
+ self.id_list = range(0, 255)
+ self.msgvp = 0xaa
+ self.pid = 0x00
+
+ self.number = number
+ self.text = text
+ self.text_gsm = None
+
+ def _set_number(self, number):
+ if number and not VALID_NUMBER.match(number):
+ raise ValueError("Invalid number format: %s" % number)
+
+ self._number = number
+
+ number = property(lambda self: self._number, _set_number)
+
+ def _set_csca(self, csca):
+ if csca and not VALID_NUMBER.match(csca):
+ raise ValueError("Invalid csca format: %s" % csca)
+
+ self._csca = csca
+
+ csca = property(lambda self: self._csca, _set_csca)
+
+ def _set_validity(self, validity):
+ if validity is None or isinstance(validity, (timedelta, datetime)):
+ # valid values are None, timedelta and datetime
+ self._validity = validity
+ else:
+ raise TypeError("Don't know what to do with %s" % validity)
+
+ validity = property(lambda self: self._validity, _set_validity)
+
+ def _set_klass(self, klass):
+ if not isinstance(klass, int):
+ raise TypeError("_set_klass only accepts int objects")
+
+ if klass not in [0, 1, 2, 3]:
+ raise ValueError("class must be between 0 and 3")
+
+ self._klass = klass
+
+ klass = property(lambda self: self._klass, _set_klass)
+
+ def to_pdu(self):
+ """Returns a list of :class:`~messaging.pdu.Pdu` objects"""
+ smsc_pdu = self._get_smsc_pdu()
+ sms_submit_pdu = self._get_sms_submit_pdu()
+ tpmessref_pdu = self._get_tpmessref_pdu()
+ sms_phone_pdu = self._get_phone_pdu()
+ tppid_pdu = self._get_tppid_pdu()
+ sms_msg_pdu = self._get_msg_pdu()
+
+ if len(sms_msg_pdu) == 1:
+ pdu = smsc_pdu
+ len_smsc = len(smsc_pdu) / 2
+ pdu += sms_submit_pdu
+ pdu += tpmessref_pdu
+ pdu += sms_phone_pdu
+ pdu += tppid_pdu
+ pdu += sms_msg_pdu[0]
+ debug("smsc_pdu: %s" % smsc_pdu)
+ debug("sms_submit_pdu: %s" % sms_submit_pdu)
+ debug("tpmessref_pdu: %s" % tpmessref_pdu)
+ debug("sms_phone_pdu: %s" % sms_phone_pdu)
+ debug("tppid_pdu: %s" % tppid_pdu)
+ debug("sms_msg_pdu: %s" % sms_msg_pdu)
+ debug("-" * 20)
+ debug("full_pdu: %s" % pdu)
+ debug("full_text: %s" % self.text)
+ debug("-" * 20)
+ return [Pdu(pdu, len_smsc)]
+
+ # multipart SMS
+ sms_submit_pdu = self._get_sms_submit_pdu(udh=True)
+ pdu_list = []
+ cnt = len(sms_msg_pdu)
+ for i, sms_msg_pdu_item in enumerate(sms_msg_pdu):
+ pdu = smsc_pdu
+ len_smsc = len(smsc_pdu) / 2
+ pdu += sms_submit_pdu
+ pdu += tpmessref_pdu
+ pdu += sms_phone_pdu
+ pdu += tppid_pdu
+ pdu += sms_msg_pdu_item
+ debug("smsc_pdu: %s" % smsc_pdu)
+ debug("sms_submit_pdu: %s" % sms_submit_pdu)
+ debug("tpmessref_pdu: %s" % tpmessref_pdu)
+ debug("sms_phone_pdu: %s" % sms_phone_pdu)
+ debug("tppid_pdu: %s" % tppid_pdu)
+ debug("sms_msg_pdu: %s" % sms_msg_pdu_item)
+ debug("-" * 20)
+ debug("full_pdu: %s" % pdu)
+ debug("full_text: %s" % self.text)
+ debug("-" * 20)
+
+ pdu_list.append(Pdu(pdu, len_smsc, cnt=cnt, seq=i + 1))
+
+ return pdu_list
+
+ def _get_smsc_pdu(self):
+ if not self.csca or not self.csca.strip():
+ return "00"
+
+ number = clean_number(self.csca)
+ ptype = 0x81 # set to unknown number by default
+ if number[0] == '+':
+ number = number[1:]
+ ptype = 0x91
+
+ if len(number) % 2:
+ number += 'F'
+
+ ps = chr(ptype)
+ for n in range(0, len(number), 2):
+ num = number[n + 1] + number[n]
+ ps += chr(int(num, 16))
+
+ pl = len(ps)
+ ps = chr(pl) + ps
+
+ return encode_str(ps)
+
+ def _get_tpmessref_pdu(self):
+ if self.ref is None:
+ self.ref = self._get_rand_id()
+
+ self.ref &= 0xFF
+ return encode_str(chr(self.ref))
+
+ def _get_phone_pdu(self):
+ number = clean_number(self.number)
+ ptype = 0x81
+ if number[0] == '+':
+ number = number[1:]
+ ptype = 0x91
+
+ pl = len(number)
+ if len(number) % 2:
+ number += 'F'
+
+ ps = chr(ptype)
+ for n in range(0, len(number), 2):
+ num = number[n + 1] + number[n]
+ ps += chr(int(num, 16))
+
+ ps = chr(pl) + ps
+ return encode_str(ps)
+
+ def _get_tppid_pdu(self):
+ return encode_str(chr(self.pid))
+
+ def _get_sms_submit_pdu(self, udh=False):
+ sms_submit = 0x1
+ if self.validity is None:
+ # handle no validity
+ pass
+ elif isinstance(self.validity, datetime):
+ # handle absolute validity
+ sms_submit |= 0x18
+ elif isinstance(self.validity, timedelta):
+ # handle relative validity
+ sms_submit |= 0x10
+
+ if self.request_status:
+ sms_submit |= 0x20
+
+ if udh:
+ sms_submit |= 0x40
+
+ return encode_str(chr(sms_submit))
+
+ def _get_msg_pdu(self):
+ # Data coding scheme
+ if self.fmt is None:
+ if is_gsm_text(self.text):
+ self.fmt = 0x00
+ else:
+ self.fmt = 0x08
+
+ self.dcs = self.fmt
+
+ if self.klass is not None:
+ if self.klass == 0:
+ self.dcs |= 0x10
+ elif self.klass == 1:
+ self.dcs |= 0x11
+ elif self.klass == 2:
+ self.dcs |= 0x12
+ elif self.klass == 3:
+ self.dcs |= 0x13
+
+ dcs_pdu = encode_str(chr(self.dcs))
+
+ # Validity period
+ msgvp_pdu = ""
+ if self.validity is None:
+ # handle no validity
+ pass
+
+ elif isinstance(self.validity, timedelta):
+ # handle relative
+ msgvp = timedelta_to_relative_validity(self.validity)
+ msgvp_pdu = encode_str(chr(msgvp))
+
+ elif isinstance(self.validity, datetime):
+ # handle absolute
+ msgvp = datetime_to_absolute_validity(self.validity)
+ msgvp_pdu = ''.join(map(encode_str, map(chr, msgvp)))
+
+ # UDL + UD
+ message_pdu = ""
+
+ if self.fmt == 0x00:
+ self.text_gsm = self.text.encode("gsm0338")
+ if len(self.text_gsm) <= consts.SEVENBIT_SIZE:
+ message_pdu = [pack_8bits_to_7bits(self.text_gsm)]
+ else:
+ message_pdu = self._split_sms_message(self.text_gsm)
+ elif self.fmt == 0x04:
+ if len(self.text) <= consts.EIGHTBIT_SIZE:
+ message_pdu = [pack_8bits_to_8bit(self.text)]
+ else:
+ message_pdu = self._split_sms_message(self.text)
+ elif self.fmt == 0x08:
+ if len(self.text) <= consts.UCS2_SIZE:
+ message_pdu = [pack_8bits_to_ucs2(self.text)]
+ else:
+ message_pdu = self._split_sms_message(self.text)
+ else:
+ raise ValueError("Unknown data coding scheme: %d" % self.fmt)
+
+ ret = []
+ for msg in message_pdu:
+ ret.append(dcs_pdu + msgvp_pdu + msg)
+
+ return ret
+
+ def _split_sms_message(self, text):
+ if self.fmt == 0x00:
+ len_without_udh = consts.SEVENBIT_MP_SIZE
+ limit = consts.SEVENBIT_SIZE
+ packing_func = pack_8bits_to_7bits
+ total_len = len(self.text_gsm)
+
+ elif self.fmt == 0x04:
+ len_without_udh = consts.EIGHTBIT_MP_SIZE
+ limit = consts.EIGHTBIT_SIZE
+ packing_func = pack_8bits_to_8bit
+ total_len = len(self.text)
+
+ elif self.fmt == 0x08:
+ len_without_udh = consts.UCS2_MP_SIZE
+ limit = consts.UCS2_SIZE
+ packing_func = pack_8bits_to_ucs2
+ total_len = len(self.text)
+
+ msgs = []
+ pi, pe = 0, len_without_udh
+
+ while pi < total_len:
+ if text[pi:pe][-1] == '\x1b':
+ pe -= 1
+
+ msgs.append(text[pi:pe])
+ pi = pe
+ pe += len_without_udh
+
+ pdu_msgs = []
+
+ udh_len = 0x05
+ mid = 0x00
+ data_len = 0x03
+
+ sms_ref = self._get_rand_id() if self.rand_id is None else self.rand_id
+ sms_ref &= 0xFF
+
+ for i, msg in enumerate(msgs):
+ i += 1
+ total_parts = len(msgs)
+ if limit == consts.SEVENBIT_SIZE:
+ udh = (chr(udh_len) + chr(mid) + chr(data_len) +
+ chr(sms_ref) + chr(total_parts) + chr(i))
+ padding = " "
+ else:
+ udh = (unichr(int("%04x" % ((udh_len << 8) | mid), 16)) +
+ unichr(int("%04x" % ((data_len << 8) | sms_ref), 16)) +
+ unichr(int("%04x" % ((total_parts << 8) | i), 16)))
+ padding = ""
+
+ pdu_msgs.append(packing_func(padding + msg, udh))
+
+ return pdu_msgs
+
+ def _get_rand_id(self):
+ if not self.id_list:
+ self.id_list = range(0, 255)
+
+ return list(self.id_list).pop(0)
diff --git a/messaging/sms/udh.py b/messaging/sms/udh.py
new file mode 100644
index 0000000..eecfa23
--- /dev/null
+++ b/messaging/sms/udh.py
@@ -0,0 +1,79 @@
+# See LICENSE
+
+
+class PortAddress(object):
+
+ def __init__(self, dest_port, orig_port, eight_bits):
+ self.dest_port = dest_port
+ self.orig_port = orig_port
+ self.eight_bits = eight_bits
+
+ def __repr__(self):
+ args = (self.dest_port, self.orig_port)
+ return "" % 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 "" % 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 "" % 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
diff --git a/messaging/sms/wap.py b/messaging/sms/wap.py
new file mode 100644
index 0000000..46611ab
--- /dev/null
+++ b/messaging/sms/wap.py
@@ -0,0 +1,38 @@
+# See LICENSE
+
+from array import array
+
+from messaging.mms.mms_pdu import MMSDecoder
+
+
+def is_a_wap_push_notification(s):
+ if not isinstance(s, str):
+ raise TypeError("data must be an array.array serialised to string")
+
+ data = array("B", s)
+
+ try:
+ return data[1] == 0x06
+ except IndexError:
+ return False
+
+
+def extract_push_notification(s):
+ data = array("B", s)
+
+ wap_push, offset = data[1:3]
+ assert wap_push == 0x06
+
+ offset += 3
+ data = data[offset:]
+
+ # XXX: Not all WAP pushes are MMS
+ return MMSDecoder().decode_data(data)
+
+
+def is_mms_notification(push):
+ # XXX: Pretty poor, but until we decode generic WAP pushes
+ # it will have to suffice. Ideally we would read the
+ # content-type from the WAP push header and test
+ return (push.headers.get('From') is not None and
+ push.headers.get('Content-Location') is not None)
diff --git a/messaging/test/__init__.py b/messaging/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/messaging/test/mms-data/27d0a048cd79555de05283a22372b0eb.mms b/messaging/test/mms-data/27d0a048cd79555de05283a22372b0eb.mms
new file mode 100644
index 0000000..574825b
Binary files /dev/null and b/messaging/test/mms-data/27d0a048cd79555de05283a22372b0eb.mms differ
diff --git a/messaging/test/mms-data/BTMMS.MMS b/messaging/test/mms-data/BTMMS.MMS
new file mode 100644
index 0000000..478f505
Binary files /dev/null and b/messaging/test/mms-data/BTMMS.MMS differ
diff --git a/messaging/test/mms-data/NOWMMS.MMS b/messaging/test/mms-data/NOWMMS.MMS
new file mode 100644
index 0000000..51056ab
Binary files /dev/null and b/messaging/test/mms-data/NOWMMS.MMS differ
diff --git a/messaging/test/mms-data/SEC-SGHS300M.mms b/messaging/test/mms-data/SEC-SGHS300M.mms
new file mode 100644
index 0000000..99f9feb
Binary files /dev/null and b/messaging/test/mms-data/SEC-SGHS300M.mms differ
diff --git a/messaging/test/mms-data/SIMPLE.MMS b/messaging/test/mms-data/SIMPLE.MMS
new file mode 100644
index 0000000..11a0cc1
Binary files /dev/null and b/messaging/test/mms-data/SIMPLE.MMS differ
diff --git a/messaging/test/mms-data/SonyEricssonT310-R201.mms b/messaging/test/mms-data/SonyEricssonT310-R201.mms
new file mode 100644
index 0000000..6034994
Binary files /dev/null and b/messaging/test/mms-data/SonyEricssonT310-R201.mms differ
diff --git a/messaging/test/mms-data/TOMSLOT.MMS b/messaging/test/mms-data/TOMSLOT.MMS
new file mode 100644
index 0000000..0ef5898
Binary files /dev/null and b/messaging/test/mms-data/TOMSLOT.MMS differ
diff --git a/messaging/test/mms-data/gallery2test.mms b/messaging/test/mms-data/gallery2test.mms
new file mode 100644
index 0000000..8f53007
Binary files /dev/null and b/messaging/test/mms-data/gallery2test.mms differ
diff --git a/messaging/test/mms-data/iPhone.mms b/messaging/test/mms-data/iPhone.mms
new file mode 100644
index 0000000..a2f7019
Binary files /dev/null and b/messaging/test/mms-data/iPhone.mms differ
diff --git a/messaging/test/mms-data/images_are_cut_off_debug.mms b/messaging/test/mms-data/images_are_cut_off_debug.mms
new file mode 100644
index 0000000..dc6f500
Binary files /dev/null and b/messaging/test/mms-data/images_are_cut_off_debug.mms differ
diff --git a/messaging/test/mms-data/m.mms b/messaging/test/mms-data/m.mms
new file mode 100644
index 0000000..2b7fa27
Binary files /dev/null and b/messaging/test/mms-data/m.mms differ
diff --git a/messaging/test/mms-data/openwave.mms b/messaging/test/mms-data/openwave.mms
new file mode 100644
index 0000000..d99d4b6
Binary files /dev/null and b/messaging/test/mms-data/openwave.mms differ
diff --git a/messaging/test/mms-data/projekt_exempel.mms b/messaging/test/mms-data/projekt_exempel.mms
new file mode 100644
index 0000000..4b1f8fd
Binary files /dev/null and b/messaging/test/mms-data/projekt_exempel.mms differ
diff --git a/messaging/test/test_gsm_encoding.py b/messaging/test/test_gsm_encoding.py
new file mode 100644
index 0000000..11c201c
--- /dev/null
+++ b/messaging/test/test_gsm_encoding.py
@@ -0,0 +1,267 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2011 Sphere Systems Ltd
+# Author: Andrew Bird
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+"""Unittests for the gsm encoding/decoding module"""
+
+import unittest
+import messaging.sms.gsm0338 # imports GSM7 codec
+
+# Reversed from: ftp://ftp.unicode.org/Public/MAPPINGS/ETSI/GSM0338.TXT
+MAP = {
+# unichr(0x0000): (0x0000, 0x00), # Null
+ '@': (0x0040, 0x00),
+ '£': (0x00a3, 0x01),
+ '$': (0x0024, 0x02),
+ '¥': (0x00a5, 0x03),
+ 'è': (0x00e8, 0x04),
+ 'é': (0x00e9, 0x05),
+ 'ù': (0x00f9, 0x06),
+ 'ì': (0x00ec, 0x07),
+ 'ò': (0x00f2, 0x08),
+ 'Ç': (0x00c7, 0x09), # LATIN CAPITAL LETTER C WITH CEDILLA
+ chr(0x000a): (0x000a, 0x0a), # Linefeed
+ 'Ø': (0x00d8, 0x0b),
+ 'ø': (0x00f8, 0x0c),
+ chr(0x000d): (0x000d, 0x0d), # Carriage return
+ 'Å': (0x00c5, 0x0e),
+ 'å': (0x00e5, 0x0f),
+ 'Δ': (0x0394, 0x10),
+ '_': (0x005f, 0x11),
+ 'Φ': (0x03a6, 0x12),
+ 'Γ': (0x0393, 0x13),
+ 'Λ': (0x039b, 0x14),
+ 'Ω': (0x03a9, 0x15),
+ 'Π': (0x03a0, 0x16),
+ 'Ψ': (0x03a8, 0x17),
+ 'Σ': (0x03a3, 0x18),
+ 'Θ': (0x0398, 0x19),
+ 'Ξ': (0x039e, 0x1a),
+ chr(0x00a0): (0x00a0, 0x1b), # Escape to extension table (displayed
+ # as NBSP, on decode of invalid escape
+ # sequence)
+ 'Æ': (0x00c6, 0x1c),
+ 'æ': (0x00e6, 0x1d),
+ 'ß': (0x00df, 0x1e),
+ 'É': (0x00c9, 0x1f),
+ ' ': (0x0020, 0x20),
+ '!': (0x0021, 0x21),
+ '"': (0x0022, 0x22),
+ '#': (0x0023, 0x23),
+ '¤': (0x00a4, 0x24),
+ '%': (0x0025, 0x25),
+ '&': (0x0026, 0x26),
+ '\'': (0x0027, 0x27),
+ '{': (0x007b, 0x1b28),
+ '}': (0x007d, 0x1b29),
+ '*': (0x002a, 0x2a),
+ '+': (0x002b, 0x2b),
+ ',': (0x002c, 0x2c),
+ '-': (0x002d, 0x2d),
+ '.': (0x002e, 0x2e),
+ '\\': (0x005c, 0x1b2f),
+ '0': (0x0030, 0x30),
+ '1': (0x0031, 0x31),
+ '2': (0x0032, 0x32),
+ '3': (0x0033, 0x33),
+ '4': (0x0034, 0x34),
+ '5': (0x0035, 0x35),
+ '6': (0x0036, 0x36),
+ '7': (0x0037, 0x37),
+ '8': (0x0038, 0x38),
+ '9': (0x0039, 0x39),
+ ':': (0x003a, 0x3a),
+ ';': (0x003b, 0x3b),
+ '[': (0x005b, 0x1b3c),
+ chr(0x000c): (0x000c, 0x1b0a), # Formfeed
+ ']': (0x005d, 0x1b3e),
+ '?': (0x003f, 0x3f),
+ '|': (0x007c, 0x1b40),
+ 'A': (0x0041, 0x41),
+ 'B': (0x0042, 0x42),
+ 'C': (0x0043, 0x43),
+ 'D': (0x0044, 0x44),
+ 'E': (0x0045, 0x45),
+ 'F': (0x0046, 0x46),
+ 'G': (0x0047, 0x47),
+ 'H': (0x0048, 0x48),
+ 'I': (0x0049, 0x49),
+ 'J': (0x004a, 0x4a),
+ 'K': (0x004b, 0x4b),
+ 'L': (0x004c, 0x4c),
+ 'M': (0x004d, 0x4d),
+ 'N': (0x004e, 0x4e),
+ 'O': (0x004f, 0x4f),
+ 'P': (0x0050, 0x50),
+ 'Q': (0x0051, 0x51),
+ 'R': (0x0052, 0x52),
+ 'S': (0x0053, 0x53),
+ 'T': (0x0054, 0x54),
+ 'U': (0x0055, 0x55),
+ 'V': (0x0056, 0x56),
+ 'W': (0x0057, 0x57),
+ 'X': (0x0058, 0x58),
+ 'Y': (0x0059, 0x59),
+ 'Z': (0x005a, 0x5a),
+ 'Ä': (0x00c4, 0x5b),
+ 'Ö': (0x00d6, 0x5c),
+ 'Ñ': (0x00d1, 0x5d),
+ 'Ü': (0x00dc, 0x5e),
+ '§': (0x00a7, 0x5f),
+ '¿': (0x00bf, 0x60),
+ 'a': (0x0061, 0x61),
+ 'b': (0x0062, 0x62),
+ 'c': (0x0063, 0x63),
+ 'd': (0x0064, 0x64),
+ '€': (0x20ac, 0x1b65),
+ 'f': (0x0066, 0x66),
+ 'g': (0x0067, 0x67),
+ 'h': (0x0068, 0x68),
+ '<': (0x003c, 0x3c),
+ 'j': (0x006a, 0x6a),
+ 'k': (0x006b, 0x6b),
+ 'l': (0x006c, 0x6c),
+ 'm': (0x006d, 0x6d),
+ 'n': (0x006e, 0x6e),
+ '~': (0x007e, 0x1b3d),
+ 'p': (0x0070, 0x70),
+ 'q': (0x0071, 0x71),
+ 'r': (0x0072, 0x72),
+ 's': (0x0073, 0x73),
+ 't': (0x0074, 0x74),
+ '>': (0x003e, 0x3e),
+ 'v': (0x0076, 0x76),
+ 'i': (0x0069, 0x69),
+ 'x': (0x0078, 0x78),
+ '^': (0x005e, 0x1b14),
+ 'z': (0x007a, 0x7a),
+ 'ä': (0x00e4, 0x7b),
+ 'ö': (0x00f6, 0x7c),
+ 'ñ': (0x00f1, 0x7d),
+ 'ü': (0x00fc, 0x7e),
+ 'à': (0x00e0, 0x7f),
+ '¡': (0x00a1, 0x40),
+ '/': (0x002f, 0x2f),
+ 'o': (0x006f, 0x6f),
+ 'u': (0x0075, 0x75),
+ 'w': (0x0077, 0x77),
+ 'y': (0x0079, 0x79),
+ 'e': (0x0065, 0x65),
+ '=': (0x003d, 0x3d),
+ '(': (0x0028, 0x28),
+ ')': (0x0029, 0x29),
+}
+
+GREEK_MAP = { # Note: these might look like Latin uppercase, but they aren't
+ 'Α': (0x0391, 0x41),
+ 'Β': (0x0392, 0x42),
+ 'Ε': (0x0395, 0x45),
+ 'Η': (0x0397, 0x48),
+ 'Ι': (0x0399, 0x49),
+ 'Κ': (0x039a, 0x4b),
+ 'Μ': (0x039c, 0x4d),
+ 'Ν': (0x039d, 0x4e),
+ 'Ο': (0x039f, 0x4f),
+ 'Ρ': (0x03a1, 0x50),
+ 'Τ': (0x03a4, 0x54),
+ 'Χ': (0x03a7, 0x58),
+ 'Υ': (0x03a5, 0x59),
+ 'Ζ': (0x0396, 0x5a),
+}
+
+QUIRK_MAP = {
+ 'ç': (0x00e7, 0x09),
+}
+
+BAD = -1
+
+
+class TestEncodingFunctions(unittest.TestCase):
+
+ def test_encoding_supported_unicode_gsm(self):
+
+ for key in list(MAP.keys()):
+ # Use 'ignore' so that we see the code tested, not an exception
+ s_gsm = key.encode('gsm0338', 'ignore')
+
+ if len(s_gsm) == 1:
+ i_gsm = ord(s_gsm)
+ elif len(s_gsm) == 2:
+ i_gsm = (ord(s_gsm[0]) << 8) + ord(s_gsm[1])
+ else:
+ i_gsm = BAD # so we see the comparison, not an exception
+
+ # We shouldn't generate an invalid escape sequence
+ if key == chr(0x00a0):
+ self.assertEqual(BAD, i_gsm)
+ else:
+ self.assertEqual(MAP[key][1], i_gsm)
+
+ def test_encoding_supported_greek_unicode_gsm(self):
+ # Note: Conversion is one way, hence no corresponding decode test
+
+ for key in list(GREEK_MAP.keys()):
+ # Use 'replace' so that we trigger the mapping
+ s_gsm = key.encode('gsm0338', 'replace')
+
+ if len(s_gsm) == 1:
+ i_gsm = ord(s_gsm)
+ else:
+ i_gsm = BAD # so we see the comparison, not an exception
+
+ self.assertEqual(GREEK_MAP[key][1], i_gsm)
+
+ def test_encoding_supported_quirk_unicode_gsm(self):
+ # Note: Conversion is one way, hence no corresponding decode test
+
+ for key in list(QUIRK_MAP.keys()):
+ # Use 'replace' so that we trigger the mapping
+ s_gsm = key.encode('gsm0338', 'replace')
+
+ if len(s_gsm) == 1:
+ i_gsm = ord(s_gsm)
+ else:
+ i_gsm = BAD # so we see the comparison, not an exception
+
+ self.assertEqual(QUIRK_MAP[key][1], i_gsm)
+
+ def test_decoding_supported_unicode_gsm(self):
+ for key in list(MAP.keys()):
+ i_gsm = MAP[key][1]
+ if i_gsm <= 0xff:
+ s_gsm = chr(i_gsm)
+ elif i_gsm <= 0xffff:
+ s_gsm = chr((i_gsm & 0xff00) >> 8)
+ s_gsm += chr(i_gsm & 0x00ff)
+
+ s_unicode = s_gsm.decode('gsm0338', 'strict')
+ self.assertEqual(MAP[key][0], ord(s_unicode))
+
+ def test_is_gsm_text_true(self):
+ for key in list(MAP.keys()):
+ if key == chr(0x00a0):
+ continue
+ self.assertEqual(messaging.sms.gsm0338.is_gsm_text(key), True)
+
+ def test_is_gsm_text_false(self):
+ self.assertEqual(
+ messaging.sms.gsm0338.is_gsm_text(chr(0x00a0)), False)
+
+ for i in range(1, 0xffff + 1):
+ if chr(i) not in MAP:
+ # Note: it's a little odd, but on error we want to see values
+ if messaging.sms.gsm0338.is_gsm_text(chr(i)) is not False:
+ self.assertEqual(BAD, i)
diff --git a/messaging/test/test_mms.py b/messaging/test/test_mms.py
new file mode 100644
index 0000000..2cf6bf1
--- /dev/null
+++ b/messaging/test/test_mms.py
@@ -0,0 +1,378 @@
+# -*- coding: utf-8 -*-
+from array import array
+import datetime
+import os
+import unittest
+
+from messaging.mms.message import MMSMessage
+
+# test data extracted from heyman's
+# http://github.com/heyman/mms-decoder
+DATA_DIR = os.path.join(os.path.dirname(__file__), 'mms-data')
+
+
+class TestMmsDecoding(unittest.TestCase):
+
+ def test_decoding_from_data(self):
+ path = os.path.join(DATA_DIR, 'iPhone.mms')
+ data = array("B", open(path, 'rb').read())
+ mms = MMSMessage.from_data(data)
+ headers = {
+ 'From': '', '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': '', '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 = '\n\n\n \n\n\n\n\n\n\n\n\n\n\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': '', 'Type': 'application/smil'}),
+ 'Subject': 'BT Ignite MMS',
+ }
+ smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n'
+ 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': '', 'Type': 'application/smil'}),
+ 'Subject': 'Tom Slot Band',
+ }
+ smil_data = '\r\n\t\r\n\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\r\n\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\r\n\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\t\r\n\t\t\t\r\n\t\t\t\r\n\t\t\r\n\t\r\n\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': '', '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': '', 'Type': 'application/smil'}),
+ 'Subject': 'Picture3',
+ }
+ smil_data = ''
+ 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': '', 'Type': 'application/smil'}),
+ 'Subject': 'rubrik',
+ }
+ smil_data = '\n \n \n \n \n \n \n \n \n \n \n \n \n\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': '',
+ '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': '', 'Type': 'application/smil'}),
+ }
+ text_data = 'Hej hopp'
+ smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\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': '', 'Type': 'application/smil'}),
+ 'Subject': 'Jgj',
+ }
+ text_data = 'Jgj'
+ smil_data = '\n \n \n \n \n \n \n \n \n \n \n \n \n \n\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': '',
+ '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': '', 'Type': 'application/smil'}),
+ 'Subject': 'Hej',
+ }
+ smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\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': '', '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 = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n\r\n\r\n\r\n \r\n \r\n\r\n\r\n'
+ 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': '',
+ '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': '', 'Type': 'application/smil'}),
+ 'Subject': 'Angående art-tillhörighet',
+ #'Subject': 'Ang\xc3\xa5ende art-tillh\xc3\xb6righet',
+ }
+ smil_data = '\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\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': '',
+ '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)
diff --git a/messaging/test/test_sms.py b/messaging/test/test_sms.py
new file mode 100644
index 0000000..c3ff7b5
--- /dev/null
+++ b/messaging/test/test_sms.py
@@ -0,0 +1,481 @@
+# -*- coding: utf-8 -*-
+from datetime import datetime, timedelta
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+from messaging.sms import SmsSubmit, SmsDeliver
+from messaging.utils import (timedelta_to_relative_validity as to_relative,
+ datetime_to_absolute_validity as to_absolute,
+ FixedOffset)
+
+
+class TestEncodingFunctions(unittest.TestCase):
+
+ def test_converting_timedelta_to_validity(self):
+ self.assertRaises(ValueError, to_relative, timedelta(minutes=4))
+ self.assertRaises(ValueError, to_relative, timedelta(weeks=64))
+
+ self.assertTrue(isinstance(to_relative(timedelta(hours=6)), int))
+ self.assertTrue(isinstance(to_relative(timedelta(hours=18)), int))
+ self.assertTrue(isinstance(to_relative(timedelta(days=15)), int))
+ self.assertTrue(isinstance(to_relative(timedelta(weeks=31)), int))
+
+ self.assertEqual(to_relative(timedelta(minutes=5)), 0)
+ self.assertEqual(to_relative(timedelta(minutes=6)), 0)
+ self.assertEqual(to_relative(timedelta(minutes=10)), 1)
+
+ self.assertEqual(to_relative(timedelta(hours=12)), 143)
+ self.assertEqual(to_relative(timedelta(hours=13)), 145)
+ self.assertEqual(to_relative(timedelta(hours=24)), 167)
+
+ self.assertEqual(to_relative(timedelta(days=2)), 168)
+ self.assertEqual(to_relative(timedelta(days=30)), 196)
+
+ def test_converting_datetime_to_validity(self):
+ # http://www.dreamfabric.com/sms/scts.html
+ # 12. Feb 1999 05:57:30 GMT+3
+ when = datetime(1999, 2, 12, 5, 57, 30, 0,
+ FixedOffset(3 * 60, "GMT+3"))
+ expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x21]
+ self.assertEqual(to_absolute(when, "GMT+3"), expected)
+
+ when = datetime(1999, 2, 12, 5, 57, 30, 0)
+ expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x0]
+ self.assertEqual(to_absolute(when, "UTC"), expected)
+
+ when = datetime(1999, 2, 12, 5, 57, 30, 0,
+ FixedOffset(-3 * 60, "GMT-3"))
+ expected = [0x99, 0x20, 0x21, 0x50, 0x75, 0x03, 0x29]
+ self.assertEqual(to_absolute(when, "GMT-3"), expected)
+
+
+class TestSmsSubmit(unittest.TestCase):
+
+ def test_encoding_validity(self):
+ # no validity
+ number = '2b3334363136353835313139'.decode('hex')
+ text = "hola"
+ expected = "0001000B914316565811F9000004E8373B0C"
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected)
+
+ # absolute validity
+ number = '2b3334363136353835313139'.decode('hex')
+ text = "hola"
+ expected = "0019000B914316565811F900000170520251930004E8373B0C"
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+ sms.validity = datetime(2010, 7, 25, 20, 15, 39)
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected)
+
+ # relative validity
+ number = '2b3334363136353835313139'.decode('hex')
+ text = "hola"
+ expected = "0011000B914316565811F90000AA04E8373B0C"
+ expected_len = 18
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+ sms.validity = timedelta(days=4)
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected)
+ self.assertEqual(pdu.length, expected_len)
+
+ def test_encoding_csca(self):
+ number = '2b3334363136353835313139'.decode('hex')
+ text = "hola"
+ csca = "+34646456456"
+ expected = "07914346466554F601000B914316565811F9000004E8373B0C"
+ expected_len = 17
+
+ sms = SmsSubmit(number, text)
+ sms.csca = csca
+ sms.ref = 0x0
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected)
+ self.assertEqual(pdu.length, expected_len)
+ self.assertEqual(pdu.cnt, 1)
+ self.assertEqual(pdu.seq, 1)
+
+ def test_encoding_class(self):
+ number = '2b3334363534313233343536'.decode('hex')
+ text = "hey yo"
+ expected_0 = "0001000B914356143254F6001006E8721E947F03"
+ expected_1 = "0001000B914356143254F6001106E8721E947F03"
+ expected_2 = "0001000B914356143254F6001206E8721E947F03"
+ expected_3 = "0001000B914356143254F6001306E8721E947F03"
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+ sms.klass = 0
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected_0)
+
+ sms.klass = 1
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected_1)
+
+ sms.klass = 2
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected_2)
+
+ sms.klass = 3
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected_3)
+
+ def test_encoding_request_status(self):
+ # tested with pduspy.exe and http://www.rednaxela.net/pdu.php
+ number = '2b3334363534313233343536'.decode('hex')
+ text = "hey yo"
+ expected = "0021000B914356143254F6000006E8721E947F03"
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+ sms.request_status = True
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected)
+
+ def test_encoding_message_with_latin1_chars(self):
+ # tested with pduspy.exe
+ number = '2b3334363534313233343536'.decode('hex')
+ text = u"Hölä"
+ expected = "0011000B914356143254F60000AA04483E7B0F"
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+ sms.validity = timedelta(days=4)
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected)
+
+ # tested with pduspy.exe
+ number = '2b3334363534313233343536'.decode('hex')
+ text = u"BÄRÇA äñ@"
+ expected = "0001000B914356143254F6000009C2AD341104EDFB00"
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected)
+
+ def test_encoding_8bit_message(self):
+ number = "01000000000"
+ csca = "+44000000000"
+ text = "Hi there..."
+ expected = "07914400000000F001000B811000000000F000040B48692074686572652E2E2E"
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+ sms.csca = csca
+ sms.fmt = 0x04 # 8 bits
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected)
+
+ def test_encoding_ucs2_message(self):
+ number = '2b3334363136353835313139'.decode('hex')
+ text = u'あ叶葉'
+ csca = '+34646456456'
+ expected = "07914346466554F601000B914316565811F9000806304253F68449"
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+ sms.csca = csca
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected)
+
+ text = u"Русский"
+ number = '363535333435363738'.decode('hex')
+ expected = "001100098156355476F80008AA0E0420044304410441043A04380439"
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+ sms.validity = timedelta(days=4)
+
+ pdu = sms.to_pdu()[0]
+ self.assertEqual(pdu.pdu, expected)
+
+ def test_encoding_multipart_7bit(self):
+ # text encoded with umts-tools
+ text = "Or walk with Kings - nor lose the common touch, if neither foes nor loving friends can hurt you, If all men count with you, but none too much; If you can fill the unforgiving minute With sixty seconds' worth of distance run, Yours is the Earth and everything thats in it, And - which is more - you will be a Man, my son"
+ number = '363535333435363738'.decode('hex')
+ expected = [
+ "005100098156355476F80000AAA00500038803019E72D03DCC5E83EE693A1AB44CBBCF73500BE47ECB41ECF7BC0CA2A3CBA0F1BBDD7EBB41F4777D8C6681D26690BB9CA6A3CB7290F95D9E83DC6F3988FDB6A7DD6790599E2EBBC973D038EC06A1EB723A28FFAEB340493328CC6683DA653768FCAEBBE9A07B9A8E06E5DF7516485CA783DC6F7719447FBF41EDFA18BD0325CDA0FCBB0E1A87DD",
+ "005100098156355476F80000AAA005000388030240E6349B0DA2A3CBA0BADBFC969FD3F6B4FB0C6AA7DD757A19744DD3D1A0791A4FCF83E6E5F1DB4D9E9F40F7B79C8E06BDCD20727A4E0FBBC76590BCEE6681B2EFBA7C0E4ACF41747419540CCBE96850D84D0695ED65799E8E4EBBCF203A3A4C9F83D26E509ACE0205DD64500B7447A7C768507A0E6ABFE565500B947FD741F7349B0D129741",
+ "005100098156355476F80000AA14050003880303C2A066D8CD02B5F3A0F9DB0D",
+ ]
+
+ sms = SmsSubmit(number, text)
+ sms.ref = 0x0
+ sms.rand_id = 136
+ sms.validity = timedelta(days=4)
+
+ ret = sms.to_pdu()
+ cnt = len(ret)
+ for i, pdu in enumerate(ret):
+ self.assertEqual(pdu.pdu, expected[i])
+ self.assertEqual(pdu.seq, i + 1)
+ self.assertEqual(pdu.cnt, cnt)
+
+ def test_encoding_bad_number_raises_error(self):
+ self.assertRaises(ValueError, SmsSubmit, "032BADNUMBER", "text")
+
+ def test_encoding_bad_csca_raises_error(self):
+ sms = SmsSubmit("54342342", "text")
+ self.assertRaises(ValueError, setattr, sms, 'csca', "1badcsca")
+
+
+class TestSubmitPduCounts(unittest.TestCase):
+
+ DEST = "+3530000000"
+ GSM_CHAR = "x"
+ EGSM_CHAR = u"€"
+ UNICODE_CHAR = u"ő"
+
+ def test_gsm_1(self):
+ sms = SmsSubmit(self.DEST, self.GSM_CHAR * 160)
+ self.assertEqual(len(sms.to_pdu()), 1)
+
+ def test_gsm_2(self):
+ sms = SmsSubmit(self.DEST, self.GSM_CHAR * 161)
+ self.assertEqual(len(sms.to_pdu()), 2)
+
+ def test_gsm_3(self):
+ sms = SmsSubmit(self.DEST, self.GSM_CHAR * 153 * 2)
+ self.assertEqual(len(sms.to_pdu()), 2)
+
+ def test_gsm_4(self):
+ sms = SmsSubmit(self.DEST,
+ self.GSM_CHAR * 153 * 2 + self.GSM_CHAR)
+ self.assertEqual(len(sms.to_pdu()), 3)
+
+ def test_gsm_5(self):
+ sms = SmsSubmit(self.DEST, self.GSM_CHAR * 153 * 3)
+ self.assertEqual(len(sms.to_pdu()), 3)
+
+ def test_gsm_6(self):
+ sms = SmsSubmit(self.DEST,
+ self.GSM_CHAR * 153 * 3 + self.GSM_CHAR)
+ self.assertEqual(len(sms.to_pdu()), 4)
+
+ def test_egsm_1(self):
+ sms = SmsSubmit(self.DEST, self.EGSM_CHAR * 80)
+ self.assertEqual(len(sms.to_pdu()), 1)
+
+ def test_egsm_2(self):
+ sms = SmsSubmit(self.DEST,
+ self.EGSM_CHAR * 79 + self.GSM_CHAR)
+ self.assertEqual(len(sms.to_pdu()), 1)
+
+ def test_egsm_3(self):
+ sms = SmsSubmit(self.DEST, self.EGSM_CHAR * 153) # 306 septets
+ self.assertEqual(len(sms.to_pdu()), 3)
+
+ def test_egsm_4(self):
+ sms = SmsSubmit(self.DEST,
+ self.EGSM_CHAR * 229 + self.GSM_CHAR) # 459 septets
+ self.assertEqual(len(sms.to_pdu()), 4)
+
+ def test_unicode_1(self):
+ sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 70)
+ self.assertEqual(len(sms.to_pdu()), 1)
+
+ def test_unicode_2(self):
+ sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 70 + self.GSM_CHAR)
+ self.assertEqual(len(sms.to_pdu()), 2)
+
+ def test_unicode_3(self):
+ sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 2)
+ self.assertEqual(len(sms.to_pdu()), 2)
+
+ def test_unicode_4(self):
+ sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 2 + self.GSM_CHAR)
+ self.assertEqual(len(sms.to_pdu()), 3)
+
+ def test_unicode_5(self):
+ sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 3)
+ self.assertEqual(len(sms.to_pdu()), 3)
+
+ def test_unicode_6(self):
+ sms = SmsSubmit(self.DEST, self.UNICODE_CHAR * 67 * 3 + self.GSM_CHAR)
+ self.assertEqual(len(sms.to_pdu()), 4)
+
+
+class TestSmsDeliver(unittest.TestCase):
+
+ def test_decoding_7bit_pdu(self):
+ pdu = "07911326040000F0040B911346610089F60000208062917314080CC8F71D14969741F977FD07"
+ text = "How are you?"
+ csca = "+31624000000"
+ number = '2b3331363431363030393836'.decode('hex')
+
+ sms = SmsDeliver(pdu)
+ self.assertEqual(sms.text, text)
+ self.assertEqual(sms.csca, csca)
+ self.assertEqual(sms.number, number)
+
+ def test_decoding_ucs2_pdu(self):
+ pdu = "07914306073011F0040B914316709807F2000880604290224080084E2D5174901A8BAF"
+ text = u"中兴通讯"
+ csca = "+34607003110"
+ number = '2b3334363130373839373032'.decode('hex')
+
+ sms = SmsDeliver(pdu)
+ self.assertEqual(sms.text, text)
+ self.assertEqual(sms.csca, csca)
+ self.assertEqual(sms.number, number)
+
+ def test_decoding_7bit_pdu_data(self):
+ pdu = "07911326040000F0040B911346610089F60000208062917314080CC8F71D14969741F977FD07"
+ text = "How are you?"
+ csca = "+31624000000"
+ number = '2b3331363431363030393836'.decode('hex')
+
+ data = SmsDeliver(pdu).data
+ self.assertEqual(data['text'], text)
+ self.assertEqual(data['csca'], csca)
+ self.assertEqual(data['number'], number)
+ self.assertEqual(data['pid'], 0)
+ self.assertEqual(data['fmt'], 0)
+ self.assertEqual(data['date'], datetime(2002, 8, 26, 19, 37, 41))
+
+ def test_decoding_datetime_gmtplusone(self):
+ pdu = "0791447758100650040C914497716247010000909010711423400A2050EC468B81C4733A"
+ text = " 1741 bst"
+ number = '2b343437393137323637343130'.decode('hex')
+ date = datetime(2009, 9, 1, 16, 41, 32)
+
+ sms = SmsDeliver(pdu)
+ self.assertEqual(sms.text, text)
+ self.assertEqual(sms.number, number)
+ self.assertEqual(sms.date, date)
+
+ def test_decoding_datetime_gmtminusthree(self):
+ pdu = "0791553001000001040491578800000190115101112979CF340B342F9FEBE536E83D0791C3E4F71C440E83E6F53068FE66A7C7697A781C7EBB4050F99BFE1EBFD96F1D48068BC16030182E66ABD560B41988FC06D1D3F03768FA66A7C7697A781C7E83CCEF34282C2ECBE96F50B90D8AC55EB0DC4B068BC140B1994E16D3D1622E"
+ date = datetime(2010, 9, 11, 18, 10, 11) # 11/09/10 15:10 GMT-3.00
+
+ sms = SmsDeliver(pdu)
+ self.assertEqual(sms.date, date)
+
+ def test_decoding_number_alphanumeric(self):
+ # Odd length test
+ pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701B"
+ number = "FONIC"
+ text = "Lieber FONIC Kunde, die Tarifoption Internet-Tagesflatrate wurde aktiviert. Internet-Nutzung wird jetzt pro Nutzungstag abgerechnet. Ihr FONIC Team"
+ csca = "+491760000443"
+
+ sms = SmsDeliver(pdu)
+ self.assertEqual(sms.text, text)
+ self.assertEqual(sms.csca, csca)
+ self.assertEqual(sms.number, number)
+
+ # Even length test
+ pdu = "07919333852804000412D0F7FBDD454FB75D693A0000903002801153402BCD301E9F0605D9E971191483C140412A35690D52832063D2F9040599A058EE05A3BD6430580E"
+ number = "www.tim.it"
+ text = 'Maxxi Alice 100 ATTIVATA FINO AL 19/04/2009'
+ csca = '+393358824000'
+
+ sms = SmsDeliver(pdu)
+ self.assertEqual(sms.text, text)
+ self.assertEqual(sms.csca, csca)
+ self.assertEqual(sms.number, number)
+
+ def test_decode_sms_confirmation(self):
+ pdu = "07914306073011F006270B913426565711F7012081111345400120811174054043"
+ csca = "+34607003110"
+ date = datetime(2010, 2, 18, 11, 31, 54)
+ number = "SR-UNKNOWN"
+ # XXX: the number should be +344626575117, is the prefix flipped ?
+ text = "+43626575117|10/02/18 11:31:54|"
+
+ sms = SmsDeliver(pdu)
+ self.assertEqual(sms.text, text)
+ self.assertEqual(sms.csca, csca)
+ self.assertEqual(sms.number, number)
+ self.assertEqual(sms.date, date)
+
+ def test_decode_weird_multipart_german_pdu(self):
+ pdus = [
+ "07919471227210244405852122F039F101506271217180A005000319020198E9B2B82C0759DFE4B0F9ED2EB7967537B9CC02B5D37450122D2FCB41EE303DFD7687D96537881A96A7CD6F383DFD7683F46134BBEC064DD36550DA0D22A7CBF3721BE42CD3F5A0198B56036DCA20B8FC0D6A0A4170767D0EAAE540433A082E7F83A6E5F93CFD76BB40D7B2DB0D9AA6CB2072BA3C2F83926EF31BE44E8FD17450BB8C9683CA",
+ "07919471227210244405852122F039F1015062712181804F050003190202E4E8309B5E7683DAFC319A5E76B340F73D9A5D7683A6E93268FD9ED3CB6EF67B0E5AD172B19B2C2693C9602E90355D6683A6F0B007946E8382F5393BEC26BB00",
+ ]
+ texts = [
+ u"Lieber Vodafone-Kunde, mit Ihrer nationalen Tarifoption zahlen Sie in diesem Netz 3,45 € pro MB plus 59 Ct pro Session. Wenn Sie diese Info nicht mehr e",
+ u"rhalten möchten, wählen Sie kostenlos +4917212220. Viel Spaß im Ausland.",
+ ]
+
+ for i, sms in enumerate(map(SmsDeliver, pdus)):
+ self.assertEqual(sms.text, texts[i])
+ self.assertEqual(sms.udh.concat.cnt, len(pdus))
+ self.assertEqual(sms.udh.concat.seq, i + 1)
+ self.assertEqual(sms.udh.concat.ref, 25)
+
+ def test_decoding_odd_length_pdu_strict_raises_valueerror(self):
+ # same pdu as in test_decoding_number_alpha1 minus last char
+ pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701"
+ self.assertRaises(ValueError, SmsDeliver, pdu)
+
+ def test_decoding_odd_length_pdu_no_strict(self):
+ # same pdu as in test_decoding_number_alpha1 minus last char
+ pdu = "07919471060040340409D0C6A733390400009060920173018093CC74595C96838C4F6772085AD6DDE4320B444E9741D4B03C6D7EC3E9E9B71B9474D3CB727799DEA286CFE5B9991DA6CBC3F432E85E9793CBA0F09A9EB6A7CB72BA0B9474D3CB727799DE72D6E9FABAFB0CBAA7E56490BA4CD7D34170F91BE4ACD3F575F7794E0F9F4161F1B92C2F8FD1EE32DD054AA2E520E3D3991C82A8E5701"
+ text = "Lieber FONIC Kunde, die Tarifoption Internet-Tagesflatrate wurde aktiviert. Internet-Nutzung wird jetzt pro Nutzungstag abgerechnet. Ihr FONIC Tea"
+
+ sms = SmsDeliver(pdu, strict=False)
+ self.assertEqual(sms.text, text)
+
+ def test_decoding_delivery_status_report(self):
+ pdu = "0791538375000075061805810531F1019082416500400190824165004000"
+ sr = {
+ 'status': 0,
+ 'scts': datetime(2010, 9, 28, 14, 56),
+ 'dt': datetime(2010, 9, 28, 14, 56),
+ 'recipient': '50131'
+ }
+
+ sms = SmsDeliver(pdu)
+ self.assertEqual(sms.csca, "+353857000057")
+ data = sms.data
+ self.assertEqual(data['ref'], 24)
+ self.assertEqual(sms.sr, sr)
+
+ def test_decoding_delivery_status_report_without_smsc_address(self):
+ pdu = "00060505810531F1010150610000400101506100004000"
+ sr = {
+ 'status': 0,
+ 'scts': datetime(2010, 10, 5, 16, 0),
+ 'dt': datetime(2010, 10, 5, 16, 0),
+ 'recipient': '50131'
+ }
+
+ sms = SmsDeliver(pdu)
+ self.assertEqual(sms.csca, None)
+ data = sms.data
+ self.assertEqual(data['ref'], 5)
+ self.assertEqual(sms.sr, sr)
+
+# XXX: renable when support added
+# def test_decoding_submit_status_report(self):
+# # sent from SMSC to indicate submission failed or additional info
+# pdu = "07914306073011F001000B914306565711F9000007F0B2FC0DCABF01"
+# csca = "+34607003110"
+# number = "SR-UNKNOWN"
+#
+# sms = SmsDeliver(pdu)
+# self.assertEqual(sms.csca, csca)
+# self.assertEqual(sms.number, number)
diff --git a/messaging/test/test_udh.py b/messaging/test/test_udh.py
new file mode 100644
index 0000000..9496ff6
--- /dev/null
+++ b/messaging/test/test_udh.py
@@ -0,0 +1,24 @@
+import unittest
+
+from messaging.sms.udh import UserDataHeader
+from messaging.utils import to_array
+
+
+class TestUserDataHeader(unittest.TestCase):
+
+ def test_user_data_header(self):
+ data = to_array("08049f8e020105040b8423f0")
+ udh = UserDataHeader.from_bytes(data)
+
+ self.assertEqual(udh.concat.seq, 1)
+ self.assertEqual(udh.concat.cnt, 2)
+ self.assertEqual(udh.concat.ref, 40846)
+ self.assertEqual(udh.ports.dest_port, 2948)
+ self.assertEqual(udh.ports.orig_port, 9200)
+
+ data = to_array("0003190201")
+ udh = UserDataHeader.from_bytes(data)
+
+ self.assertEqual(udh.concat.seq, 1)
+ self.assertEqual(udh.concat.cnt, 2)
+ self.assertEqual(udh.concat.ref, 25)
diff --git a/messaging/test/test_wap.py b/messaging/test/test_wap.py
new file mode 100644
index 0000000..84224a1
--- /dev/null
+++ b/messaging/test/test_wap.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+from array import array
+import unittest
+
+from messaging.sms import SmsDeliver
+from messaging.sms.wap import (is_a_wap_push_notification as is_push,
+ is_mms_notification,
+ extract_push_notification)
+
+
+def list_to_str(l):
+ a = array("B", l)
+ return a.tostring()
+
+
+class TestSmsWapPush(unittest.TestCase):
+
+ data = [1, 6, 34, 97, 112, 112, 108, 105, 99, 97, 116, 105, 111,
+ 110, 47, 118, 110, 100, 46, 119, 97, 112, 46, 109, 109, 115, 45,
+ 109, 101, 115, 115, 97, 103, 101, 0, 175, 132, 140, 130, 152, 78,
+ 79, 75, 53, 67, 105, 75, 99, 111, 84, 77, 89, 83, 71, 52, 77, 66,
+ 83, 119, 65, 65, 115, 75, 118, 49, 52, 70, 85, 72, 65, 65, 65, 65,
+ 65, 65, 65, 65, 0, 141, 144, 137, 25, 128, 43, 52, 52, 55, 55, 56,
+ 53, 51, 52, 50, 55, 52, 57, 47, 84, 89, 80, 69, 61, 80, 76, 77, 78,
+ 0, 138, 128, 142, 2, 116, 0, 136, 5, 129, 3, 1, 25, 64, 131, 104,
+ 116, 116, 112, 58, 47, 47, 112, 114, 111, 109, 109, 115, 47, 115,
+ 101, 114, 118, 108, 101, 116, 115, 47, 78, 79, 75, 53, 67, 105, 75,
+ 99, 111, 84, 77, 89, 83, 71, 52, 77, 66, 83, 119, 65, 65, 115, 75,
+ 118, 49, 52, 70, 85, 72, 65, 65, 65, 65, 65, 65, 65, 65, 0]
+
+ def test_is_a_wap_push_notification(self):
+ self.assertTrue(is_push(list_to_str(self.data)))
+ self.assertTrue(is_push(list_to_str([1, 6, 57, 92, 45])))
+ self.assertFalse(is_push(list_to_str([4, 5, 57, 92, 45])))
+ self.assertRaises(TypeError, is_push, 1)
+
+ def test_decoding_m_notification_ind(self):
+ pdus = [
+ "0791447758100650400E80885810000000810004016082415464408C0C08049F8E020105040B8423F00106226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82984E4F4B3543694B636F544D595347344D4253774141734B7631344655484141414141414141008D908919802B3434373738353334323734392F545950453D504C4D4E008A808E0274008805810301194083687474703A2F",
+ "0791447758100650440E8088581000000081000401608241547440440C08049F8E020205040B8423F02F70726F6D6D732F736572766C6574732F4E4F4B3543694B636F544D595347344D4253774141734B763134465548414141414141414100",
+ ]
+ number = '3838383530313030303030303138'.decode('hex')
+ csca = "+447785016005"
+ data = ""
+
+ sms = SmsDeliver(pdus[0])
+ self.assertEqual(sms.udh.concat.ref, 40846)
+ self.assertEqual(sms.udh.concat.cnt, 2)
+ self.assertEqual(sms.udh.concat.seq, 1)
+ self.assertEqual(sms.number, number)
+ self.assertEqual(sms.csca, csca)
+ data += sms.text
+
+ sms = SmsDeliver(pdus[1])
+ self.assertEqual(sms.udh.concat.ref, 40846)
+ self.assertEqual(sms.udh.concat.cnt, 2)
+ self.assertEqual(sms.udh.concat.seq, 2)
+ self.assertEqual(sms.number, number)
+ data += sms.text
+
+ mms = extract_push_notification(data)
+ self.assertEqual(is_mms_notification(mms), True)
+
+ self.assertEqual(mms.headers['Message-Type'], 'm-notification-ind')
+ self.assertEqual(mms.headers['Transaction-Id'],
+ 'NOK5CiKcoTMYSG4MBSwAAsKv14FUHAAAAAAAA')
+ self.assertEqual(mms.headers['MMS-Version'], '1.0')
+ self.assertEqual(mms.headers['From'],
+ '2b3434373738353334323734392f545950453d504c4d4e'.decode('hex'))
+ self.assertEqual(mms.headers['Message-Class'], 'Personal')
+ self.assertEqual(mms.headers['Message-Size'], 29696)
+ self.assertEqual(mms.headers['Expiry'], 72000)
+ self.assertEqual(mms.headers['Content-Location'],
+ 'http://promms/servlets/NOK5CiKcoTMYSG4MBSwAAsKv14FUHAAAAAAAA')
+
+ pdus = [
+ "0791447758100650400E80885810000000800004017002314303408C0C0804DFD3020105040B8423F00106226170706C69636174696F6E2F766E642E7761702E6D6D732D6D65737361676500AF848C82984E4F4B3541315A6446544D595347344F3356514141734A763934476F4E4141414141414141008D908919802B3434373731373237353034392F545950453D504C4D4E008A808E0274008805810303F47F83687474703A2F",
+ "0791447758100650440E8088581000000080000401700231431340440C0804DFD3020205040B8423F02F70726F6D6D732F736572766C6574732F4E4F4B3541315A6446544D595347344F3356514141734A763934476F4E414141414141414100",
+ ]
+
+ number = "88850100000008"
+ data = ""
+
+ sms = SmsDeliver(pdus[0])
+ self.assertEqual(sms.udh.concat.ref, 57299)
+ self.assertEqual(sms.udh.concat.cnt, 2)
+ self.assertEqual(sms.udh.concat.seq, 1)
+ self.assertEqual(sms.number, number)
+ data += sms.text
+
+ sms = SmsDeliver(pdus[1])
+ self.assertEqual(sms.udh.concat.ref, 57299)
+ self.assertEqual(sms.udh.concat.cnt, 2)
+ self.assertEqual(sms.udh.concat.seq, 2)
+ self.assertEqual(sms.number, number)
+ data += sms.text
+
+ mms = extract_push_notification(data)
+ self.assertEqual(is_mms_notification(mms), True)
+
+ self.assertEqual(mms.headers['Message-Type'], 'm-notification-ind')
+ self.assertEqual(mms.headers['Transaction-Id'],
+ 'NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA')
+ self.assertEqual(mms.headers['MMS-Version'], '1.0')
+ self.assertEqual(mms.headers['From'],
+ '2b3434373731373237353034392f545950453d504c4d4e'.decode('hex'))
+ self.assertEqual(mms.headers['Message-Class'], 'Personal')
+ self.assertEqual(mms.headers['Message-Size'], 29696)
+ self.assertEqual(mms.headers['Expiry'], 259199)
+ self.assertEqual(mms.headers['Content-Location'],
+ 'http://promms/servlets/NOK5A1ZdFTMYSG4O3VQAAsJv94GoNAAAAAAAA')
+
+ def test_decoding_generic_wap_push(self):
+ pdus = [
+ "0791947122725014440C8500947122921105F5112042519582408C0B05040B8423F0000396020101060B03AE81EAC3958D01A2B48403056A0A20566F6461666F6E650045C60C037761702E6D65696E63616C6C79612E64652F000801035A756D206B6F7374656E6C6F73656E20506F7274616C20224D65696E0083000322202D2065696E66616368206175662064656E20666F6C67656E64656E204C696E6B206B6C69636B656E",
+ "0791947122725014440C8500947122921105F5112042519592403C0B05040B8423F00003960202206F6465722064696520536569746520646972656B7420617566727566656E2E2049687200830003205465616D000101",
+ ]
+ number = '303034393137323232393131'.decode('hex')
+ csca = "+491722270541"
+ data = ""
+
+ sms = SmsDeliver(pdus[0])
+ self.assertEqual(sms.udh.concat.ref, 150)
+ self.assertEqual(sms.udh.concat.cnt, 2)
+ self.assertEqual(sms.udh.concat.seq, 1)
+ self.assertEqual(sms.number, number)
+ self.assertEqual(sms.csca, csca)
+ data += sms.text
+
+ sms = SmsDeliver(pdus[1])
+ self.assertEqual(sms.udh.concat.ref, 150)
+ self.assertEqual(sms.udh.concat.cnt, 2)
+ self.assertEqual(sms.udh.concat.seq, 2)
+ self.assertEqual(sms.number, number)
+ data += sms.text
+
+ self.assertEqual(data, '\x01\x06\x0b\x03\xae\x81\xea\xc3\x95\x8d\x01\xa2\xb4\x84\x03\x05j\n Vodafone\x00E\xc6\x0c\x03wap.meincallya.de/\x00\x08\x01\x03Zum kostenlosen Portal "Mein\x00\x83\x00\x03" - einfach auf den folgenden Link klicken oder die Seite direkt aufrufen. Ihr\x00\x83\x00\x03 Team\x00\x01\x01')
+
+ push = extract_push_notification(data)
+ self.assertEqual(is_mms_notification(push), False)
diff --git a/messaging/utils.py b/messaging/utils.py
new file mode 100644
index 0000000..9544a4d
--- /dev/null
+++ b/messaging/utils.py
@@ -0,0 +1,267 @@
+from array import array
+from datetime import timedelta, tzinfo
+from math import floor
+import sys
+
+
+class FixedOffset(tzinfo):
+ """Fixed offset in minutes east from UTC."""
+
+ def __init__(self, offset, name):
+ if isinstance(offset, timedelta):
+ self.offset = offset
+ else:
+ self.offset = timedelta(minutes=offset)
+
+ self.__name = name
+
+ @classmethod
+ def from_timezone(cls, tz_str, name):
+ # no timezone, GMT+3, GMT-3
+ # '', '+0330', '-0300'
+ if not tz_str:
+ return cls(timedelta(0), name)
+
+ sign = 1 if '+' in tz_str else -1
+ offset = tz_str.replace('+', '').replace('-', '')
+ hours, minutes = int(offset[:2]), int(offset[2:])
+ minutes += hours * 60
+
+ if sign == 1:
+ td = timedelta(minutes=minutes)
+ elif sign == -1:
+ td = timedelta(days=-1, minutes=minutes)
+
+ return cls(td, name)
+
+ def utcoffset(self, dt):
+ return self.offset
+
+ def tzname(self, dt):
+ return self.__name
+
+ def dst(self, dt):
+ return timedelta(0)
+
+
+def bytes_to_str(b):
+ if sys.version_info >= (3,):
+ return b.decode('latin1')
+
+ return b
+
+
+def to_array(pdu):
+ return array('B', [int(pdu[i:i + 2], 16) for i in range(0, len(pdu), 2)])
+
+
+def to_bytes(s):
+ if sys.version_info >= (3,):
+ return bytes(s)
+
+ return ''.join(map(chr, s))
+
+
+def debug(s):
+ # set this to True if you want to poke at PDU encoding/decoding
+ if False:
+ print(s)
+
+
+def swap(s):
+ """Swaps ``s`` according to GSM 23.040"""
+ what = s[:]
+ for n in range(1, len(what), 2):
+ what[n - 1], what[n] = what[n], what[n - 1]
+
+ return what
+
+
+def swap_number(n):
+ data = swap(list(n.replace('f', '')))
+ return ''.join(data)
+
+
+def clean_number(n):
+ return n.strip().replace(' ', '')
+
+
+def encode_str(s):
+ """Returns the hexadecimal representation of ``s``"""
+ return ''.join(["%02x" % ord(n) for n in s])
+
+
+def encode_bytes(b):
+ return ''.join(["%02x" % n for n in b])
+
+
+def pack_8bits_to_7bits(message, udh=None):
+ pdu = ""
+ txt = bytes_to_str(message)
+
+ if udh is None:
+ tl = len(txt)
+ txt += '\x00'
+ msgl = int(len(txt) * 7 / 8)
+ op = [-1] * msgl
+ c = shift = 0
+
+ for n in range(msgl):
+ if shift == 6:
+ c += 1
+
+ shift = n % 7
+ lb = ord(txt[c]) >> shift
+ hb = (ord(txt[c + 1]) << (7 - shift) & 255)
+ op[n] = lb + hb
+ c += 1
+
+ pdu = chr(tl) + ''.join(map(chr, op))
+ else:
+ txt = "\x00\x00\x00\x00\x00\x00" + txt
+ tl = len(txt)
+
+ txt += '\x00'
+ msgl = int(len(txt) * 7 / 8)
+ op = [-1] * msgl
+ c = shift = 0
+
+ for n in range(msgl):
+ if shift == 6:
+ c += 1
+
+ shift = n % 7
+ lb = ord(txt[c]) >> shift
+ hb = (ord(txt[c + 1]) << (7 - shift) & 255)
+ op[n] = lb + hb
+ c += 1
+
+ for i, char in enumerate(udh):
+ op[i] = ord(char)
+
+ pdu = chr(tl) + ''.join(map(chr, op))
+
+ return encode_str(pdu)
+
+
+def pack_8bits_to_8bit(message, udh=None):
+ text = message
+ if udh is not None:
+ text = udh + text
+
+ mlen = len(text)
+ message = chr(mlen) + message
+ return encode_str(message)
+
+
+def pack_8bits_to_ucs2(message, udh=None):
+ # XXX: This does not control the size respect to UDH
+ text = message
+ nmesg = ''
+
+ if udh is not None:
+ text = udh + text
+
+ for n in text:
+ nmesg += chr(ord(n) >> 8) + chr(ord(n) & 0xFF)
+
+ mlen = len(text) * 2
+ message = chr(mlen) + nmesg
+ return encode_str(message)
+
+
+def unpack_msg(pdu):
+ """Unpacks ``pdu`` into septets and returns the decoded string"""
+ # Taken/modified from Dave Berkeley's pysms package
+ count = last = 0
+ result = []
+
+ for i in range(0, len(pdu), 2):
+ byte = int(pdu[i:i + 2], 16)
+ mask = 0x7F >> count
+ out = ((byte & mask) << count) + last
+ last = byte >> (7 - count)
+ result.append(out)
+
+ if len(result) >= 0xa0:
+ break
+
+ if count == 6:
+ result.append(last)
+ last = 0
+
+ count = (count + 1) % 7
+
+ return to_bytes(result)
+
+
+def unpack_msg2(pdu):
+ """Unpacks ``pdu`` into septets and returns the decoded string"""
+ # Taken/modified from Dave Berkeley's pysms package
+ count = last = 0
+ result = []
+
+ for byte in pdu:
+ mask = 0x7F >> count
+ out = ((byte & mask) << count) + last
+ last = byte >> (7 - count)
+ result.append(out)
+
+ if len(result) >= 0xa0:
+ break
+
+ if count == 6:
+ result.append(last)
+ last = 0
+
+ count = (count + 1) % 7
+
+ return to_bytes(result)
+
+
+def timedelta_to_relative_validity(t):
+ """
+ Convert ``t`` to its relative validity period
+
+ In case the resolution of ``t`` is too small for a time unit,
+ it will be floor-rounded to the previous sane value
+
+ :type t: datetime.timedelta
+
+ :return int
+ """
+ if t < timedelta(minutes=5):
+ raise ValueError("Min resolution is five minutes")
+
+ if t > timedelta(weeks=63):
+ raise ValueError("Max validity is 63 weeks")
+
+ if t <= timedelta(hours=12):
+ return int(floor(t.seconds / (60 * 5))) - 1
+
+ if t <= timedelta(hours=24):
+ t -= timedelta(hours=12)
+ return int(floor(t.seconds / (60 * 30))) + 143
+
+ if t <= timedelta(days=30):
+ return t.days + 166
+
+ if t <= timedelta(weeks=63):
+ return int(floor(t.days / 7)) + 192
+
+
+def datetime_to_absolute_validity(d, tzname='Unknown'):
+ """Convert ``d`` to its integer representation"""
+ n = d.strftime("%y %m %d %H %M %S %z").split(" ")
+ # compute offset
+ offset = FixedOffset.from_timezone(n[-1], tzname).offset
+ # one unit is 15 minutes
+ s = "%02d" % int(floor(offset.seconds / (60 * 15)))
+
+ if offset.days < 0:
+ # set MSB to 1
+ s = "%02x" % ((int(s[0]) << 4) | int(s[1]) | 0x80)
+
+ n[-1] = s
+
+ return [int(c[::-1], 16) for c in n]
diff --git a/requirements.txt b/requirements.txt
index 8eb8388..0969dc0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,3 +18,4 @@ pinax-theme-bootstrap
django-bootstrap3
requests
webdavclient
+pyst2
diff --git a/systemd_units/djing_dial.service b/systemd_units/djing_dial.service
new file mode 100644
index 0000000..a3adbf6
--- /dev/null
+++ b/systemd_units/djing_dial.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Dialing inbox sms unit
+
+[Service]
+Type=simple
+ExecStart=/usr/bin/python3 dialing.py
+WorkingDirectory=/srv/http/djing
+User=http
+Group=http
+
+[Install]
+WantedBy=multi-user.target