2 # -*- coding: utf-8 -*-
3 # This library is free software, distributed under the terms of
4 # the GNU Lesser General Public License Version 2.
5 # See the COPYING.LESSER file included in this archive
7 # The docstrings in this module contain epytext markup; API documentation
8 # may be created by processing this file with epydoc: http://epydoc.sf.net
10 """ MMS Data Unit structure encoding and decoding classes
11 Original by Francois Aucamp
12 Modified by Nick Leppänen Larsson for use in Maemo5/Fremantle on the Nokia N900.
14 @author: Francois Aucamp <faucamp@csir.co.za>
15 @author: Nick Leppänen Larsson <frals@frals.se>
22 from iterator import PreviewIterator
24 class MMSEncodingAssignments:
25 fieldNames = {0x01 : ('Bcc', 'EncodedStringValue'),
26 0x02 : ('Cc', 'EncodedStringValue'),
27 0x03 : ('Content-Location', 'UriValue'),
28 0x04 : ('Content-Type','ContentTypeValue'),
29 0x05 : ('Date', 'DateValue'),
30 0x06 : ('Delivery-Report', 'BooleanValue'),
31 0x07 : ('Delivery-Time', None),
32 0x08 : ('Expiry', 'ExpiryValue'),
33 0x09 : ('From', 'FromValue'),
34 0x0a : ('Message-Class', 'MessageClassValue'),
35 0x0b : ('Message-ID', 'TextString'),
36 0x0c : ('Message-Type', 'MessageTypeValue'),
37 0x0d : ('MMS-Version', 'VersionValue'),
38 0x0e : ('Message-Size', 'LongInteger'),
39 0x0f : ('Priority', 'PriorityValue'),
40 0x10 : ('Read-Reply', 'BooleanValue'),
41 0x11 : ('Report-Allowed', 'BooleanValue'),
42 0x12 : ('Response-Status', 'ResponseStatusValue'),
43 0x13 : ('Response-Text', 'EncodedStringValue'),
44 0x14 : ('Sender-Visibility', 'SenderVisibilityValue'),
45 0x15 : ('Status', 'StatusValue'),
46 0x16 : ('Subject', 'EncodedStringValue'),
47 0x17 : ('To', 'EncodedStringValue'),
48 0x18 : ('Transaction-Id', 'TextString'),
49 0x40 : ('Content-ID', 'TextString')}
52 class MMSDecoder(wsp_pdu.Decoder):
53 """ A decoder for MMS messages """
54 def __init__(self, filename=None, noHeaders=None):
55 """ @param filename: If specified, decode the content of the MMS
56 message file with this name
59 self._mmsData = array.array('B')
60 self._mmsMessage = message.MMSMessage(noHeaders)
64 def decodeFile(self, filename):
65 """ Load the data contained in the specified file, and decode it.
67 @param filename: The name of the MMS message file to open
70 @raises OSError: The filename is invalid
72 @return: The decoded MMS data
75 nBytes = os.stat(filename)[6]
76 data = array.array('B')
77 f = open(filename, 'rb')
78 data.fromfile(f, nBytes)
80 return self.decodeData(data)
82 def decodeData(self, data):
83 """ Decode the specified MMS message data
85 @note: This creates a 'headers' file containing
86 MMS headers as plain-text
88 @param data: The MMS message data to decode
89 @type filename: array.array('B')
91 @return: The decoded MMS data
94 self._mmsMessage = message.MMSMessage()
96 bodyIter = self.decodeMessageHeader()
97 #print "body begins at: ", bodyIter._it, bodyIter._previewPos
98 # TODO: separate this in to own method?
99 if self._path != None:
100 hdrlist = self.decodeMessageHeaderToList(data)
101 fp = open(self._path + "/headers", 'w')
103 #print "MMS HEADER:", k, hdrlist[k]
104 fp.write(str(k) + " " + str(hdrlist[k]) + "\n")
106 self._mmsMessage.attachments = self.decodeMessageBodyToPath(bodyIter)
107 #print self._mmsMessage.headers
108 return self._mmsMessage
110 def decodeCustom(self, data):
111 list = self.decodeMessageHeaderToList(data)
114 def decodeMessageHeaderToList(self, data):
115 """ Decodes the (full) MMS header data
117 @note: This B{must} be called before C{_decodeBody()}, as it sets
118 certain internal variables relating to data lengths, etc.
120 dataIter = PreviewIterator(data)
122 # First 3 headers (in order
123 ############################
124 # - X-Mms-Message-Type
125 # - X-Mms-Transaction-ID
127 # TODO: reimplement strictness - currently we allow these 3 headers
128 # to be mixed with any of the other headers (this allows the
129 # decoding of "broken" MMSs, but is technically incorrect
133 # The next few headers will not be in a specific order, except for
134 # "Content-Type", which should be the last header
135 # According to [4], MMS header field names will be short integers
136 # If the message type is a M-Notification* or M-Acknowledge
137 #type we don't expect any ContentType and should break on
138 # StopIteration exception
139 contentTypeFound = False
140 contentLocationFound = False
141 while (contentTypeFound == False):
143 header, value = self.decodeHeader(dataIter)
144 except StopIteration, e:
147 if header == MMSEncodingAssignments.fieldNames[0x04][0]:
148 contentTypeFound = True
149 elif header == MMSEncodingAssignments.fieldNames[0x03][0]:
150 contentLocationFound = True
155 self._mmsMessage.headers[header] = value
156 #print '%s: %s' % (header, str(value))
157 except StopIteration, e:
162 #print '%s: %s' % (header, cType)
164 #for parameter in params:
165 # print ' %s: %s' % (parameter, str(params[parameter]))
167 self._mmsMessage.headers[header] = (value)
168 return self._mmsMessage.headers
172 def decodeResponseHeader(self, data):
173 dataIter = PreviewIterator(data)
180 byte = wsp_pdu.Decoder.decodeShortIntegerFromByte(dataIter.preview())
181 except StopIteration:
183 if byte in MMSEncodingAssignments.fieldNames:
185 mmsFieldName = MMSEncodingAssignments.fieldNames[byte][0]
187 dataIter.resetPreview()
188 raise wsp_pdu.DecodeError, 'Invalid MMS Header: could not decode MMS field name'
190 #print MMSEncodingAssignments.fieldNames[byte][1]
191 exec 'mmsValue = MMSDecoder.decode%s(dataIter)' % MMSEncodingAssignments.fieldNames[byte][1]
192 except wsp_pdu.DecodeError, msg:
193 raise wsp_pdu.DecodeError, 'Invalid MMS Header: Could not decode MMS-value: %s' % msg
195 print 'A fatal error occurred, probably due to an unimplemented decoding operation. Tried to decode header: %s' % mmsFieldName
197 except wsp_pdu.DecodeError:
198 mmsFieldName, mmsValue = wsp_pdu.Decoder.decodeHeader(dataIter) #MMSDecoder.decodeApplicationHeader(byteIter)
199 headers[mmsFieldName] = mmsValue
202 def decodeMessageHeader(self):
203 """ Decodes the (full) MMS header data
205 @note: This B{must} be called before C{_decodeBody()}, as it sets
206 certain internal variables relating to data lengths, etc.
208 dataIter = PreviewIterator(self._mmsData)
210 # First 3 headers (in order
211 ############################
212 # - X-Mms-Message-Type
213 # - X-Mms-Transaction-ID
215 # TODO: reimplement strictness - currently we allow these 3 headers
216 # to be mixed with any of the other headers (this allows the
217 # decoding of "broken" MMSs, but is technically incorrect
221 # The next few headers will not be in a specific order, except for
222 # "Content-Type", which should be the last header
223 # According to [4], MMS header field names will be short integers
224 contentTypeFound = False
225 while contentTypeFound == False:
226 header, value = self.decodeHeader(dataIter)
227 #print (header, value)
228 if header == MMSEncodingAssignments.fieldNames[0x04][0]:
229 contentTypeFound = True
231 self._mmsMessage.headers[header] = value
232 #print '%s: %s' % (header, str(value))
235 #print '%s: %s' % (header, cType)
237 #for parameter in params:
238 # print ' %s: %s' % (parameter, str(params[parameter]))
240 self._mmsMessage.headers[header] = (cType, params)
241 #print self._mmsMessage.headers
245 def decodeMessageBody(self, dataIter):
246 """ Decodes the MMS message body
248 @param dataIter: an iterator over the sequence of bytes of the MMS
250 @type dataIteror: iter
252 ######### MMS body: headers ###########
253 # Get the number of data parts in the MMS body
254 nEntries = self.decodeUintvar(dataIter)
255 #print 'Number of data entries (parts) in MMS body:', nEntries
257 ########## MMS body: entries ##########
258 # For every data "part", we have to read the following sequence:
259 # <length of content-type + other possible headers>,
261 # <content-type + other possible headers>,
263 for partNum in range(nEntries):
264 #print '\nPart %d:\n------' % partNum
265 headersLen = self.decodeUintvar(dataIter)
266 dataLen = self.decodeUintvar(dataIter)
268 # Prepare to read content-type + other possible headers
270 for i in range(headersLen):
271 ctFieldBytes.append(dataIter.next())
272 # ctIter = iter(ctFieldBytes)
273 ctIter = PreviewIterator(ctFieldBytes)
275 contentType, ctParameters = self.decodeContentTypeValue(ctIter)
276 headers = {'Content-Type' : (contentType, ctParameters)}
277 #print 'Content-Type:', contentType
278 #for param in ctParameters:
279 # print ' %s: %s' % (param, str(ctParameters[param]))
281 # Now read other possible headers until <headersLen> bytes have been read
284 hdr, value = self.decodeHeader(ctIter)
286 #print '%s: %s' % (otherHeader, otherValue)
287 except StopIteration:
289 #print 'Data length:', dataLen, 'bytes'
291 # Data (note: this is not null-terminated)
292 data = array.array('B')
293 for i in range(dataLen):
294 data.append(dataIter.next())
296 part = message.DataPart()
297 part.setData(data, contentType)
298 part.contentTypeParameters = ctParameters
299 part.headers = headers
300 self._mmsMessage.addDataPart(part)
301 # TODO: Make this pretty
303 if contentType == 'image/jpeg':
305 #if contentType == 'image/gif':
307 #elif contentType == 'audio/wav':
309 #elif contentType == 'audio/midi':
311 elif contentType == 'text/plain':
313 elif contentType == 'application/smil':
316 f = open('part%d.%s' % (partNum, extension), 'wb')
320 def decodeMessageBodyToPath(self, dataIter):
321 """ Decodes the MMS message body
323 @param dataIter: an iterator over the sequence of bytes of the MMS
325 @type dataIteror: iter
327 ######### MMS body: headers ###########
328 # Get the number of data parts in the MMS body
329 nEntries = self.decodeUintvar(dataIter)
330 #print 'Number of data entries (parts) in MMS body:', nEntries
334 ########## MMS body: entries ##########
335 # For every data "part", we have to read the following sequence:
336 # <length of content-type + other possible headers>,
338 # <content-type + other possible headers>,
340 for partNum in range(nEntries):
341 #print '\nPart %d:\n------' % partNum
342 headersLen = self.decodeUintvar(dataIter)
343 dataLen = self.decodeUintvar(dataIter)
345 # Prepare to read content-type + other possible headers
347 for i in range(headersLen):
348 ctFieldBytes.append(dataIter.next())
349 # ctIter = iter(ctFieldBytes)
350 ctIter = PreviewIterator(ctFieldBytes)
352 contentType, ctParameters = self.decodeContentTypeValue(ctIter)
353 headers = {'Content-Type' : (contentType, ctParameters)}
354 #print 'Content-Type:', contentType
355 #for param in ctParameters:
356 # print ' %s: %s' % (param, str(ctParameters[param]))
358 # Now read other possible headers until <headersLen> bytes have been read
361 hdr, value = self.decodeHeader(ctIter)
363 #print '%s: %s' % (otherHeader, otherValue)
364 except StopIteration:
366 #print 'Data length:', dataLen, 'bytes'
368 # Data (note: this is not null-terminated)
369 data = array.array('B')
370 for i in range(dataLen):
371 data.append(dataIter.next())
373 part = message.DataPart()
374 part.setData(data, contentType)
375 part.contentTypeParameters = ctParameters
376 part.headers = headers
377 self._mmsMessage.addDataPart(part)
378 # TODO: Make this pretty
380 if contentType == 'image/jpeg':
382 if contentType == 'image/gif':
384 elif contentType == 'audio/wav':
386 elif contentType == 'audio/midi':
388 elif contentType == 'text/plain':
390 elif contentType == 'application/smil':
393 extension = 'unknown'
398 #dirname = self._path + "_dir"
401 if not os.path.isdir(dirname):
404 #print "MMSBODY HEADERS:", headers
406 ### loop through the headers, if we find a "name" header
407 ### using it seems like a good idea, right?
412 if h.__class__ == dict:
415 # this shouldnt really happen, but just in case...
420 filename = str(partNum) + "." + str(extension)
421 f = open(dirname + '/%s' % (filename), 'wb')
424 attachments.append(filename)
428 def decodeHeader(byteIter):
429 """ Decodes a header entry from an MMS message, starting at the byte
430 pointed to by C{byteIter.next()}
432 From [4], section 7.1:
433 C{Header = MMS-header | Application-header}
435 @raise DecodeError: This uses C{decodeMMSHeader()} and
436 C{decodeApplicationHeader()}, and will raise this
437 exception under the same circumstances as
438 C{decodeApplicationHeader()}. C{byteIter} will
439 not be modified in this case.
441 @note: The return type of the "header value" depends on the header
442 itself; it is thus up to the function calling this to determine
443 what that type is (or at least compensate for possibly
444 different return value types).
446 @return: The decoded header entry from the MMS, in the format:
447 (<str:header name>, <str/int/float:header value>)
453 header, value = MMSDecoder.decodeMMSHeader(byteIter)
454 except wsp_pdu.DecodeError:
455 header, value = wsp_pdu.Decoder.decodeHeader(byteIter) #MMSDecoder.decodeApplicationHeader(byteIter)
456 return (header, value)
459 def decodeMMSHeader(byteIter):
460 """ From [4], section 7.1:
461 MMS-header = MMS-field-name MMS-value
462 MMS-field-name = Short-integer
463 MMS-value = Bcc-value | Cc-value | Content-location-value |
464 Content-type-value | etc
466 This method takes into account the assigned number values for MMS
467 field names, as specified in [4], section 7.3, table 8.
469 @raise wsp_pdu.DecodeError: The MMS field name could not be parsed.
470 C{byteIter} will not be modified in this case.
472 @return: The decoded MMS header, in the format:
473 (<str:MMS-field-name>, <str:MMS-value>)
476 # Get the MMS-field-name
478 byte = wsp_pdu.Decoder.decodeShortIntegerFromByte(byteIter.preview())
479 #byte = wsp_pdu.Decoder.decodeShortInteger(byteIter)
480 if byte in MMSEncodingAssignments.fieldNames:
482 mmsFieldName = MMSEncodingAssignments.fieldNames[byte][0]
485 byteIter.resetPreview()
486 raise wsp_pdu.DecodeError, 'Invalid MMS Header: could not decode MMS field name'
487 # Now get the MMS-value
490 #print MMSEncodingAssignments.fieldNames[byte][1]
491 exec 'mmsValue = MMSDecoder.decode%s(byteIter)' % MMSEncodingAssignments.fieldNames[byte][1]
492 except wsp_pdu.DecodeError, msg:
493 raise wsp_pdu.DecodeError, 'Invalid MMS Header: Could not decode MMS-value: %s' % msg
495 print 'A fatal error occurred, probably due to an unimplemented decoding operation. Tried to decode header: %s' % mmsFieldName
497 return (mmsFieldName, mmsValue)
500 def decodeEncodedStringValue(byteIter):
501 """ From [4], section 7.2.9:
502 C{Encoded-string-value = Text-string | Value-length Char-set Text-string}
503 The Char-set values are registered by IANA as MIBEnum value.
505 @note: This function is not fully implemented, in that it does not
506 have proper support for the Char-set values; it basically just
507 reads over that sequence of bytes, and ignores it (see code for
508 details) - any help with this will be greatly appreciated.
510 @return: The decoded text string
515 # First try "Value-length Char-set Text-string"
516 valueLength = wsp_pdu.Decoder.decodeValueLength(byteIter)
517 #TODO: *probably* have to include proper support for charsets...
519 charSetValue = wsp_pdu.Decoder.decodeWellKnownCharset(byteIter)
520 except wsp_pdu.DecodeError, msg:
521 raise Exception, 'EncodedStringValue decoding error: Could not decode Char-set value; %s' % msg
522 decodedString = wsp_pdu.Decoder.decodeTextString(byteIter)
523 except wsp_pdu.DecodeError:
524 # Fall back on just "Text-string"
525 decodedString = wsp_pdu.Decoder.decodeTextString(byteIter)
528 #TODO: maybe change this to boolean values
530 def decodeBooleanValue(byteIter):
531 """ From [4], section 7.2.6::
532 Delivery-report-value = Yes | No
536 A lot of other yes/no fields use this encoding (read-reply,
539 @raise wsp_pdu.DecodeError: The boolean value could not be parsed.
540 C{byteIter} will not be modified in this case.
542 @return The value for the field: 'Yes' or 'No'
546 # byteIter, localIter = itertools.tee(byteIter)
547 # byte = localIter.next()
548 byte = byteIter.preview()
549 if byte not in (128, 129):
550 byteIter.resetPreview()
551 raise wsp_pdu.DecodeError, 'Error parsing boolean value for byte: %s' % hex(byte)
553 byte = byteIter.next()
561 def decodeFromValue(byteIter):
562 """ From [4], section 7.2.11:
563 From-value = Value-length (Address-present-token Encoded-string-value | Insert-address-token )
564 Address-present-token = <Octet 128>
565 Insert-address-token = <Octet 129>
567 @return: The "From" address value
571 valueLength = wsp_pdu.Decoder.decodeValueLength(byteIter)
572 # See what token we have
573 byte = byteIter.next()
574 if byte == 129: # Insert-address-token
575 fromValue = '<not inserted>'
577 fromValue = MMSDecoder.decodeEncodedStringValue(byteIter)
581 def decodeMessageClassValue(byteIter):
582 """ From [4], section 7.2.12:
583 Message-class-value = Class-identifier | Token-text
584 Class-identifier = Personal | Advertisement | Informational | Auto
585 Personal = <Octet 128>
586 Advertisement = <Octet 129>
587 Informational = <Octet 130>
589 The token-text is an extension method to the message class.
591 @return: The decoded message class
594 classIdentifiers = {128 : 'Personal',
595 129 : 'Advertisement',
596 130 : 'Informational',
599 # byteIter, localIter = itertools.tee(byteIter)
600 # byte = localIter.next()
601 byte = byteIter.preview()
602 if byte in classIdentifiers:
604 msgClass = classIdentifiers[byte]
606 byteIter.resetPreview()
607 msgClass = wsp_pdu.Decoder.decodeTokenText(byteIter)
611 def decodeMessageTypeValue(byteIter):
612 """ Defined in [4], section 7.2.14.
614 @return: The decoded message type, or '<unknown>'
617 messageTypes = {0x80 : 'm-send-req',
618 0x81 : 'm-send-conf',
619 0x82 : 'm-notification-ind',
620 0x83 : 'm-notifyresp-ind',
621 0x84 : 'm-retrieve-conf',
622 0x85 : 'm-acknowledge-ind',
623 0x86 : 'm-delivery-ind'}
624 byte = byteIter.preview()
625 if byte in messageTypes:
627 return messageTypes[byte]
629 byteIter.resetPreview()
633 def decodePriorityValue(byteIter):
634 """ Defined in [4], section 7.2.17
636 @raise wsp_pdu.DecodeError: The priority value could not be decoded;
637 C{byteIter} is not modified in this case.
639 @return: The decoded priority value
642 priorities = {128 : 'Low',
645 # byteIter, localIter = itertools.tee(byteIter)
646 byte = byteIter.preview()
647 if byte in priorities:
648 byte = byteIter.next()
649 return priorities[byte]
651 byteIter.resetPreview()
652 raise wsp_pdu.DecodeError, 'Error parsing Priority value for byte:',byte
655 def decodeSenderVisibilityValue(byteIter):
656 """ Defined in [4], section 7.2.22::
657 Sender-visibility-value = Hide | Show
661 @raise wsp_pdu.DecodeError: The sender visibility value could not be
663 C{byteIter} will not be modified in this case.
665 @return: The sender visibility: 'Hide' or 'Show'
669 # byteIter, localIter = itertools.tee(byteIter)
670 # byte = localIter.next()
671 byte = byteIter.preview()
672 if byte not in (128, 129):
673 byteIter.resetPreview()
674 raise wsp_pdu.DecodeError, 'Error parsing sender visibility value for byte: %s' % hex(byte)
676 byte = byteIter.next()
684 def decodeResponseStatusValue(byteIter):
685 """ Defined in [4], section 7.2.20
687 Used to decode the "Response Status" MMS header.
689 @raise wsp_pdu.DecodeError: The sender visibility value could not be
691 C{byteIter} will not be modified in this case.
693 @return: The decoded Response-status-value
696 responseStatusValues = {0x80 : 'Ok',
697 0x81 : 'Error-unspecified',
698 0x82 : 'Error-service-denied',
699 0x83 : 'Error-message-format-corrupt',
700 0x84 : 'Error-sending-address-unresolved',
701 0x85 : 'Error-message-not-found',
702 0x86 : 'Error-network-problem',
703 0x87 : 'Error-content-not-accepted',
704 0x88 : 'Error-unsupported-message'}
705 byte = byteIter.preview()
706 if byte in responseStatusValues:
708 return responseStatusValues[byte]
711 # Return an unspecified error if the response is not recognized
712 return responseStatusValues[0x81]
715 def decodeStatusValue(byteIter):
716 """ Defined in [4], section 7.2.23
718 Used to decode the "Status" MMS header.
720 @raise wsp_pdu.DecodeError: The sender visibility value could not be
722 C{byteIter} will not be modified in this case.
724 @return: The decoded Status-value
728 statusValues = {0x80 : 'Expired',
732 0x84 : 'Unrecognised'}
734 byte = byteIter.preview()
735 if byte in statusValues:
737 return statusValues[byte]
740 # Return an unrecognised state if it couldn't be decoded
741 return statusValues[0x84]
745 def decodeExpiryValue(byteIter):
746 """ Defined in [4], section 7.2.10
748 Used to decode the "Expiry" MMS header.
750 From [4], section 7.2.10:
751 Expiry-value = Value-length (Absolute-token Date-value | Relative-token Delta-seconds-value)
752 Absolute-token = <Octet 128>
753 Relative-token = <Octet 129>
755 @raise wsp_pdu.DecodeError: The Expiry-value could not be decoded
757 @return: The decoded Expiry-value, either as a date, or as a delta-seconds value
760 valueLength = MMSDecoder.decodeValueLength(byteIter)
761 token = byteIter.next()
763 if token == 0x80: # Absolute-token
764 data = MMSDecoder.decodeDateValue(byteIter)
765 elif token == 0x81: # Relative-token
766 data = MMSDecoder.decodeDeltaSecondsValue(byteIter)
768 raise wsp_pdu.DecodeError, 'Unrecognized token value: %s' % hex(token)
772 class MMSEncoder(wsp_pdu.Encoder):
773 def __init__(self, noHeaders=None):
774 self.noHeaders = noHeaders
775 self._mmsMessage = message.MMSMessage(noHeaders)
777 def encode(self, mmsMessage):
778 """ Encodes the specified MMS message
780 @param mmsMessage: The MMS message to encode
781 @type mmsMessage: MMSMessage
783 @return: The binary-encoded MMS data, as a sequence of bytes
784 @rtype: array.array('B')
786 self._mmsMessage = mmsMessage
787 msgData = self.encodeMessageHeader()
788 if self.noHeaders == None:
789 msgData.extend(self.encodeMessageBody())
792 def encodeMessageHeader(self):
793 """ Binary-encodes the MMS header data.
795 @note: The encoding used for the MMS header is specified in [4].
796 All "constant" encoded values found/used in this method
797 are also defined in [4]. For a good example, see [2].
799 @return: the MMS PDU header, as an array of bytes
800 @rtype: array.array('B')
802 # See [4], chapter 8 for info on how to use these
803 fromTypes = {'Address-present-token' : 0x80,
804 'Insert-address-token' : 0x81}
806 contentTypes = {'application/vnd.wap.multipart.related' : 0xb3}
808 # Create an array of 8-bit values
809 messageHeader = array.array('B')
811 headersToEncode = self._mmsMessage.headers
813 # If the user added any of these to the message manually (X- prefix), rather use those
814 for hdr in ('X-Mms-Message-Type', 'X-Mms-Transaction-Id', 'X-Mms-Version'):
815 if hdr in headersToEncode:
816 if hdr == 'X-Mms-Version':
817 cleanHeader = 'MMS-Version'
819 cleanHeader = hdr.replace('X-Mms-', '', 1)
820 headersToEncode[cleanHeader] = headersToEncode[hdr]
821 del headersToEncode[hdr]
823 # First 3 headers (in order), according to [4]:
824 ################################################
825 # - X-Mms-Message-Type
826 # - X-Mms-Transaction-ID
829 ### Start of Message-Type verification
830 if 'Message-Type' not in headersToEncode:
831 # Default to 'm-retrieve-conf'; we don't need a To/CC field for this
832 # (see WAP-209, section 6.3, table 5)
833 headersToEncode['Message-Type'] = 'm-retrieve-conf'
835 # See if the chosen message type is valid, given the message's other headers
836 # NOTE: we only distinguish between 'm-send-req' (requires a destination number)
837 # and 'm-retrieve-conf' (requires no destination number)
838 # - if "Message-Type" is something else, we assume the message creator
839 # knows what he/she is doing...
840 if headersToEncode['Message-Type'] == 'm-send-req':
841 foundDestAddress = False
842 for addressType in ('To', 'Cc', 'Bc'):
843 if addressType in headersToEncode:
844 foundDestAddress = True
846 if not foundDestAddress:
847 headersToEncode['Message-Type'] = 'm-retrieve-conf'
848 ### End of Message-Type verification
850 ### Start of Transaction-Id verification
851 if 'Transaction-Id' not in headersToEncode:
853 headersToEncode['Transaction-Id'] = str(random.randint(1000, 9999))
854 ### End of Transaction-Id verification
856 ### Start of MMS-Version verification
857 if 'MMS-Version' not in headersToEncode:
858 headersToEncode['MMS-Version'] = '1.0'
860 messageType = headersToEncode['Message-Type']
862 # Encode the first three headers, in correct order
863 for hdr in ('Message-Type', 'Transaction-Id', 'MMS-Version'):
864 messageHeader.extend(MMSEncoder.encodeHeader(hdr, headersToEncode[hdr]))
865 del headersToEncode[hdr]
867 # Encode all remaining MMS message headers, except "Content-Type"
868 # -- this needs to be added last, according [2] and [4]
869 for hdr in headersToEncode:
870 if hdr == 'Content-Type':
872 messageHeader.extend(MMSEncoder.encodeHeader(hdr, headersToEncode[hdr]))
874 # Ok, now only "Content-type" should be left
875 # No content-type if it's a notifyresp-ind
876 if messageType != 'm-notifyresp-ind' and messageType != 'm-acknowledge-ind':
877 ctType = headersToEncode['Content-Type'][0]
878 ctParameters = headersToEncode['Content-Type'][1]
879 messageHeader.extend(MMSEncoder.encodeMMSFieldName('Content-Type'))
880 #print (ctType, ctParameters)
881 messageHeader.extend(MMSEncoder.encodeContentTypeValue(ctType, ctParameters))
884 def encodeMessageBody(self):
885 """ Binary-encodes the MMS body data.
887 @note: The MMS body is of type C{application/vnd.wap.multipart}
888 (C{mixed} or C{related}).
889 As such, its structure is divided into a header, and the data entries/parts::
891 [ header ][ entries ]
892 ^^^^^^^^^^^^^^^^^^^^^
895 The MMS Body header consists of one entry[5]::
897 ------- ------- -----------
898 nEntries Uintvar number of entries in the multipart entity
900 The MMS body's multipart entries structure::
902 ------- ----- -----------
903 HeadersLen Uintvar length of the ContentType and
904 Headers fields combined
905 DataLen Uintvar length of the Data field
906 ContentType Multiple octets the content type of the data
907 Headers (<HeadersLen>
909 <ContentType>) octets the part's headers
910 Data <DataLen> octets the part's data
912 @note: The MMS body's header should not be confused with the actual
913 MMS header, as returned by C{_encodeHeader()}.
915 @note: The encoding used for the MMS body is specified in [5], section 8.5.
916 It is only referenced in [4], however [2] provides a good example of
917 how this ties in with the MMS header encoding.
919 @return: The binary-encoded MMS PDU body, as an array of bytes
920 @rtype: array.array('B')
923 messageBody = array.array('B')
925 #TODO: enable encoding of MMSs without SMIL file
926 ########## MMS body: header ##########
927 # Parts: SMIL file + <number of data elements in each slide>
929 for page in self._mmsMessage._pages:
930 nEntries += page.numberOfParts()
931 for dataPart in self._mmsMessage._dataParts:
934 messageBody.extend(self.encodeUintvar(nEntries))
936 ########## MMS body: entries ##########
937 # For every data "part", we have to add the following sequence:
938 # <length of content-type + other possible headers>,
940 # <content-type + other possible headers>,
943 # Gather the data parts, adding the MMS message's SMIL file
944 smilPart = message.DataPart()
945 smil = self._mmsMessage.smil()
946 smilPart.setData(smil, 'application/smil')
947 # TODO: make this dynamic....
948 #smilPart.headers['Content-ID'] = '<0000>'
950 for slide in self._mmsMessage._pages:
951 for partTuple in (slide.image, slide.audio, slide.text):
952 if partTuple != None:
953 parts.append(partTuple[0])
956 partContentType = self.encodeContentTypeValue(part.headers['Content-Type'][0], part.headers['Content-Type'][1])
957 encodedPartHeaders = []
958 for hdr in part.headers:
959 if hdr == 'Content-Type':
961 encodedPartHeaders.extend(wsp_pdu.Encoder.encodeHeader(hdr, part.headers[hdr]))
963 # HeadersLen entry (length of the ContentType and Headers fields combined)
964 headersLen = len(partContentType) + len(encodedPartHeaders)
965 messageBody.extend(self.encodeUintvar(headersLen))
966 # DataLen entry (length of the Data field)
967 messageBody.extend(self.encodeUintvar(len(part)))
969 messageBody.extend(partContentType)
971 messageBody.extend(encodedPartHeaders)
972 # Data (note: we do not null-terminate this)
973 for char in part.data:
974 messageBody.append(ord(char))
979 def encodeHeader(headerFieldName, headerValue):
980 """ Encodes a header entry for an MMS message
982 From [4], section 7.1:
983 C{Header = MMS-header | Application-header}
984 C{MMS-header = MMS-field-name MMS-value}
985 C{MMS-field-name = Short-integer}
986 C{MMS-value = Bcc-value | Cc-value | Content-location-value |
987 Content-type-value | etc}
989 @raise DecodeError: This uses C{decodeMMSHeader()} and
990 C{decodeApplicationHeader()}, and will raise this
991 exception under the same circumstances as
992 C{decodeApplicationHeader()}. C{byteIter} will
993 not be modified in this case.
995 @note: The return type of the "header value" depends on the header
996 itself; it is thus up to the function calling this to determine
997 what that type is (or at least compensate for possibly
998 different return value types).
1000 @return: The decoded header entry from the MMS, in the format:
1001 (<str:header name>, <str/int/float:header value>)
1005 # First try encoding the header as a "MMS-header"...
1006 for assignedNumber in MMSEncodingAssignments.fieldNames:
1007 if MMSEncodingAssignments.fieldNames[assignedNumber][0] == headerFieldName:
1008 encodedHeader.extend(wsp_pdu.Encoder.encodeShortInteger(assignedNumber))
1009 # Now encode the value
1010 expectedType = MMSEncodingAssignments.fieldNames[assignedNumber][1]
1012 exec 'encodedHeader.extend(MMSEncoder.encode%s(headerValue))' % expectedType
1013 except wsp_pdu.EncodeError, msg:
1014 raise wsp_pdu.EncodeError, 'Error encoding parameter value: %s' % msg
1016 print 'A fatal error occurred, probably due to an unimplemented encoding operation'
1019 # See if the "MMS-header" encoding worked
1020 if len(encodedHeader) == 0:
1021 # ...it didn't. Use "Application-header" encoding
1022 encodedHeaderName = wsp_pdu.Encoder.encodeTokenText(headerFieldName)
1023 encodedHeader.extend(encodedHeaderName)
1025 encodedHeader.extend(wsp_pdu.Encoder.encodeTextString(headerValue))
1026 return encodedHeader
1029 def encodeMMSFieldName(fieldName):
1030 """ Encodes an MMS header field name, using the "assigned values" for
1031 well-known MMS headers as specified in [4].
1033 From [4], section 7.1:
1034 C{MMS-field-name = Short-integer}
1036 @raise EncodeError: The specified header field name is not a
1037 well-known MMS header.
1039 @param fieldName: The header field name to encode
1040 @type fieldName: str
1042 @return: The encoded header field name, as a sequence of bytes
1045 encodedMMSFieldName = []
1046 for assignedNumber in MMSEncodingAssignments.fieldNames:
1047 if MMSEncodingAssignments.fieldNames[assignedNumber][0] == fieldName:
1048 encodedMMSFieldName.extend(wsp_pdu.Encoder.encodeShortInteger(assignedNumber))
1050 if len(encodedMMSFieldName) == 0:
1051 raise wsp_pdu.EncodeError, 'The specified header field name is not a well-known MMS header field name'
1052 return encodedMMSFieldName
1055 def encodeFromValue(fromValue=''):
1056 """ From [4], section 7.2.11:
1057 From-value = Value-length (Address-present-token Encoded-string-value | Insert-address-token )
1058 Address-present-token = <Octet 128>
1059 Insert-address-token = <Octet 129>
1061 @param fromValue: The "originator" of the MMS message. This may be an
1062 empty string, in which case a token will be encoded
1063 informing the MMSC to insert the address of the
1064 device that sent this message (default).
1065 @type fromValue: str
1067 @return: The encoded "From" address value, as a sequence of bytes
1070 encodedFromValue = []
1071 if len(fromValue) == 0:
1072 valueLength = wsp_pdu.Encoder.encodeValueLength(1)
1073 encodedFromValue.extend(valueLength)
1074 encodedFromValue.append(129) # Insert-address-token
1076 encodedAddress = MMSEncoder.encodeEncodedStringValue(fromValue)
1077 length = len(encodedAddress) + 1 # the "+1" is for the Address-present-token
1078 valueLength = wsp_pdu.Encoder.encodeValueLength(length)
1079 encodedFromValue.extend(valueLength)
1080 encodedFromValue.append(128) # Address-present-token
1081 encodedFromValue.extend(encodedAddress)
1082 return encodedFromValue
1085 def encodeEncodedStringValue(stringValue):
1086 """ From [4], section 7.2.9:
1087 C{Encoded-string-value = Text-string | Value-length Char-set Text-string}
1088 The Char-set values are registered by IANA as MIBEnum value.
1090 @param stringValue: The text string to encode
1091 @type stringValue: str
1093 @note: This function is currently a simple wrappper to
1094 C{encodeTextString()}
1096 @return: The encoded string value, as a sequence of bytes
1099 return wsp_pdu.Encoder.encodeTextString(stringValue)
1102 def encodeMessageTypeValue(messageType):
1103 """ Defined in [4], section 7.2.14.
1105 @note: Unknown message types are discarded; thus they will be encoded
1106 as 0x80 ("m-send-req") by this function
1108 @param messageType: The MMS message type to encode
1109 @type messageType: str
1111 @return: The encoded message type, as a sequence of bytes
1114 messageTypes = {'m-send-req' : 0x80,
1115 'm-send-conf' : 0x81,
1116 'm-notification-ind' : 0x81,
1117 'm-notifyresp-ind' : 0x83,
1118 'm-retrieve-conf' : 0x84,
1119 'm-acknowledge-ind' : 0x85,
1120 'm-delivery-ind' : 0x86}
1121 if messageType in messageTypes:
1122 return [messageTypes[messageType]]
1127 def encodeStatusValue(statusValue):
1128 """ Defined in [4], section 7.2.#
1130 Used to encode the "Status" MMS header.
1132 @return: The encoded Status-value, or 0x84 ('Unrecognised') if none
1136 statusValues = {'Expired' : 0x80,
1140 'Unrecognised' : 0x84}
1142 if statusValue in statusValues:
1143 return [statusValues[statusValue]]