initial commit
[fmms] / src / mms / mms_pdu.py
1 #!/usr/bin/env python
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
6 #
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
9
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.
13
14 @author: Francois Aucamp <faucamp@csir.co.za>
15 @author: Nick Leppänen Larsson <frals@frals.se>
16 @license: GNU LGPL
17 """
18
19 import os, array
20 import wsp_pdu
21 import message
22 from iterator import PreviewIterator
23
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')}
50
51
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
57             @type filename: str
58         """
59         self._mmsData = array.array('B')
60         self._mmsMessage = message.MMSMessage(noHeaders)
61         self._parts = []
62         self._path = filename
63
64     def decodeFile(self, filename):
65         """ Load the data contained in the specified file, and decode it.
66         
67         @param filename: The name of the MMS message file to open
68         @type filename: str
69         
70         @raises OSError: The filename is invalid
71         
72         @return: The decoded MMS data
73         @rtype: MMSMessage
74         """
75         nBytes = os.stat(filename)[6]
76         data = array.array('B')
77         f = open(filename, 'rb')
78         data.fromfile(f, nBytes)
79         f.close()
80         return self.decodeData(data)
81         
82     def decodeData(self, data):
83         """ Decode the specified MMS message data
84         
85         @note: This creates a 'headers' file containing
86                 MMS headers as plain-text
87      
88         @param data: The MMS message data to decode
89         @type filename: array.array('B')
90         
91         @return: The decoded MMS data
92         @rtype: MMSMessage
93         """
94         self._mmsMessage = message.MMSMessage()
95         self._mmsData = data
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')
102                 for k in hdrlist:
103                         #print "MMS HEADER:", k, hdrlist[k]     
104                         fp.write(str(k) + " " + str(hdrlist[k]) + "\n")
105                 fp.close()
106         self._mmsMessage.attachments = self.decodeMessageBodyToPath(bodyIter)
107         #print self._mmsMessage.headers
108         return self._mmsMessage
109     
110     def decodeCustom(self, data):
111         list = self.decodeMessageHeaderToList(data)
112         return list
113    
114     def decodeMessageHeaderToList(self, data):
115         """ Decodes the (full) MMS header data
116         
117         @note: This B{must} be called before C{_decodeBody()}, as it sets
118         certain internal variables relating to data lengths, etc.
119         """
120         dataIter = PreviewIterator(data)
121         
122         # First 3  headers (in order
123         ############################
124         # - X-Mms-Message-Type
125         # - X-Mms-Transaction-ID
126         # - X-Mms-Version
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
130            
131         # Misc headers
132         ##############
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):
142             try:
143                 header, value = self.decodeHeader(dataIter)
144             except StopIteration, e:
145                 print e, e.args
146                 break
147             if header == MMSEncodingAssignments.fieldNames[0x04][0]:
148                 contentTypeFound = True
149             elif header == MMSEncodingAssignments.fieldNames[0x03][0]:
150                 contentLocationFound = True
151                 break
152                 #pass
153             else:
154                 try:
155                         self._mmsMessage.headers[header] = value
156                         #print '%s: %s' % (header, str(value))
157                 except StopIteration, e:
158                         print e, e.args
159                         break
160         
161         cType = value[0]
162         #print '%s: %s' % (header, cType)
163         params = value[1]
164         #for parameter in params:
165         #    print '    %s: %s' % (parameter, str(params[parameter]))
166
167         self._mmsMessage.headers[header] = (value)
168         return self._mmsMessage.headers
169     
170     """ messy stuff """
171     # TODO: clean up
172     def decodeResponseHeader(self, data):
173                 dataIter = PreviewIterator(data)
174                 length = len(data)
175                 headers = {}
176                 eof = False
177                 while 1:
178                         try:
179                                 try:
180                                         byte = wsp_pdu.Decoder.decodeShortIntegerFromByte(dataIter.preview())
181                                 except StopIteration:
182                                         break
183                                 if byte in MMSEncodingAssignments.fieldNames:
184                                     dataIter.next()
185                                     mmsFieldName = MMSEncodingAssignments.fieldNames[byte][0]
186                                 else:
187                                     dataIter.resetPreview()
188                                     raise wsp_pdu.DecodeError, 'Invalid MMS Header: could not decode MMS field name'
189                                 try:
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
194                                 except:
195                                     print 'A fatal error occurred, probably due to an unimplemented decoding operation. Tried to decode header: %s' % mmsFieldName
196                                     raise
197                         except wsp_pdu.DecodeError:
198                                 mmsFieldName, mmsValue = wsp_pdu.Decoder.decodeHeader(dataIter) #MMSDecoder.decodeApplicationHeader(byteIter)
199                         headers[mmsFieldName] = mmsValue
200                 return headers
201     
202     def decodeMessageHeader(self):
203         """ Decodes the (full) MMS header data
204         
205         @note: This B{must} be called before C{_decodeBody()}, as it sets
206         certain internal variables relating to data lengths, etc.
207         """
208         dataIter = PreviewIterator(self._mmsData)
209         
210         # First 3  headers (in order
211         ############################
212         # - X-Mms-Message-Type
213         # - X-Mms-Transaction-ID
214         # - X-Mms-Version
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
218            
219         # Misc headers
220         ##############
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
230             else:
231                 self._mmsMessage.headers[header] = value
232                 #print '%s: %s' % (header, str(value))
233         
234         cType = value[0]
235         #print '%s: %s' % (header, cType)
236         params = value[1]
237         #for parameter in params:
238         #    print '    %s: %s' % (parameter, str(params[parameter]))
239
240         self._mmsMessage.headers[header] = (cType, params)
241         #print self._mmsMessage.headers
242         return dataIter
243     
244             
245     def decodeMessageBody(self, dataIter):
246         """ Decodes the MMS message body
247         
248         @param dataIter: an iterator over the sequence of bytes of the MMS
249                          body
250         @type dataIteror: iter
251         """
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
256         
257         ########## MMS body: entries ##########
258         # For every data "part", we have to read the following sequence:
259         # <length of content-type + other possible headers>,
260         # <length of data>,
261         # <content-type + other possible headers>,
262         # <data>
263         for partNum in range(nEntries):
264             #print '\nPart %d:\n------' % partNum
265             headersLen = self.decodeUintvar(dataIter)
266             dataLen = self.decodeUintvar(dataIter)
267             
268             # Prepare to read content-type + other possible headers
269             ctFieldBytes = []
270             for i in range(headersLen):
271                 ctFieldBytes.append(dataIter.next())
272 #            ctIter = iter(ctFieldBytes)
273             ctIter = PreviewIterator(ctFieldBytes)
274             # Get content type
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]))
280                 
281             # Now read other possible headers until <headersLen> bytes have been read
282             while True:
283                 try:
284                     hdr, value = self.decodeHeader(ctIter)
285                     headers[hdr] = value
286                     #print '%s: %s' % (otherHeader, otherValue)
287                 except StopIteration:
288                     break
289             #print 'Data length:', dataLen, 'bytes'
290             
291             # Data (note: this is not null-terminated)
292             data = array.array('B')
293             for i in range(dataLen):
294                 data.append(dataIter.next())
295             
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
302             #extension = 'dump'
303             if contentType == 'image/jpeg':
304                 extension = 'jpg'
305             #if contentType == 'image/gif':
306             #    extension = 'gif'
307             #elif contentType == 'audio/wav':
308             #    extension = 'wav'
309             #elif contentType == 'audio/midi':
310             #    extension = 'mid'
311             elif contentType == 'text/plain':
312                 extension = 'txt'
313             elif contentType == 'application/smil':
314                 extension = 'smil'
315             
316             f = open('part%d.%s' % (partNum, extension), 'wb')
317             data.tofile(f)
318             f.close()
319     
320     def decodeMessageBodyToPath(self, dataIter):
321         """ Decodes the MMS message body
322         
323         @param dataIter: an iterator over the sequence of bytes of the MMS
324                          body
325         @type dataIteror: iter
326         """
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
331         
332         attachments = []
333         
334         ########## MMS body: entries ##########
335         # For every data "part", we have to read the following sequence:
336         # <length of content-type + other possible headers>,
337         # <length of data>,
338         # <content-type + other possible headers>,
339         # <data>
340         for partNum in range(nEntries):
341             #print '\nPart %d:\n------' % partNum
342             headersLen = self.decodeUintvar(dataIter)
343             dataLen = self.decodeUintvar(dataIter)
344             
345             # Prepare to read content-type + other possible headers
346             ctFieldBytes = []
347             for i in range(headersLen):
348                 ctFieldBytes.append(dataIter.next())
349 #            ctIter = iter(ctFieldBytes)
350             ctIter = PreviewIterator(ctFieldBytes)
351             # Get content type
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]))
357                 
358             # Now read other possible headers until <headersLen> bytes have been read
359             while True:
360                 try:
361                     hdr, value = self.decodeHeader(ctIter)
362                     headers[hdr] = value
363                     #print '%s: %s' % (otherHeader, otherValue)
364                 except StopIteration:
365                     break
366             #print 'Data length:', dataLen, 'bytes'
367             
368             # Data (note: this is not null-terminated)
369             data = array.array('B')
370             for i in range(dataLen):
371                 data.append(dataIter.next())
372             
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
379             #extension = 'dump'
380             if contentType == 'image/jpeg':
381                 extension = 'jpg'
382             if contentType == 'image/gif':
383                 extension = 'gif'
384             elif contentType == 'audio/wav':
385                 extension = 'wav'
386             elif contentType == 'audio/midi':
387                 extension = 'mid'
388             elif contentType == 'text/plain':
389                 extension = 'txt'
390             elif contentType == 'application/smil':
391                 extension = 'smil'
392             else:
393                 extension = 'unknown'
394             
395             
396             ## TODO: FIX THIS ##
397             dirname = self._path
398             #dirname = self._path + "_dir"
399             ## TODO: FIX THIS ##
400             
401             if not os.path.isdir(dirname):
402                 os.makedirs(dirname)
403             filename = None
404             #print "MMSBODY HEADERS:", headers
405             
406             ### loop through the headers, if we find a "name" header
407             ### using it seems like a good idea, right?
408             try:
409                         for k in headers:
410                                 print k, headers[k]
411                                 h = headers[k][1]
412                                 if h.__class__ == dict:
413                                     filename = h['Name']
414             except:
415                         # this shouldnt really happen, but just in case...
416                         pass
417
418             
419             if filename == None:    
420                 filename = str(partNum) + "." + str(extension)
421             f = open(dirname + '/%s' % (filename), 'wb')
422             data.tofile(f)
423             f.close()
424             attachments.append(filename)
425                 return attachments
426     
427     @staticmethod
428     def decodeHeader(byteIter):
429         """ Decodes a header entry from an MMS message, starting at the byte
430         pointed to by C{byteIter.next()}
431         
432         From [4], section 7.1:
433         C{Header = MMS-header | Application-header}
434         
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.
440         
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).
445         
446         @return: The decoded header entry from the MMS, in the format:
447                  (<str:header name>, <str/int/float:header value>)
448         @rtype: tuple
449         """
450         header = ''
451         value = ''
452         try:
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)
457     
458     @staticmethod
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
465         
466         This method takes into account the assigned number values for MMS
467         field names, as specified in [4], section 7.3, table 8.            
468         
469         @raise wsp_pdu.DecodeError: The MMS field name could not be parsed.
470                                 C{byteIter} will not be modified in this case.
471         
472         @return: The decoded MMS header, in the format:
473                  (<str:MMS-field-name>, <str:MMS-value>)
474         @rtype: tuple
475         """
476         # Get the MMS-field-name
477         mmsFieldName = ''
478         byte = wsp_pdu.Decoder.decodeShortIntegerFromByte(byteIter.preview())
479         #byte = wsp_pdu.Decoder.decodeShortInteger(byteIter)
480         if byte in MMSEncodingAssignments.fieldNames:
481             byteIter.next()
482             mmsFieldName = MMSEncodingAssignments.fieldNames[byte][0]
483 #            byteIter.next()
484         else:
485             byteIter.resetPreview()
486             raise wsp_pdu.DecodeError, 'Invalid MMS Header: could not decode MMS field name'
487         # Now get the MMS-value
488         mmsValue = ''
489         try:
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
494         except:
495             print 'A fatal error occurred, probably due to an unimplemented decoding operation. Tried to decode header: %s' % mmsFieldName
496             raise
497         return (mmsFieldName, mmsValue)
498
499     @staticmethod
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.
504         
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.
509         
510         @return: The decoded text string
511         @rtype: str
512         """
513         decodedString = ''
514         try:
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...
518             try:
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)
526         return decodedString
527     
528     #TODO: maybe change this to boolean values
529     @staticmethod
530     def decodeBooleanValue(byteIter):
531         """ From [4], section 7.2.6::
532          Delivery-report-value = Yes | No
533          Yes = <Octet 128>
534          No = <Octet 129>
535         
536         A lot of other yes/no fields use this encoding (read-reply, 
537         report-allowed, etc)
538         
539         @raise wsp_pdu.DecodeError: The boolean value could not be parsed.
540                                 C{byteIter} will not be modified in this case.
541         
542         @return The value for the field: 'Yes' or 'No'
543         @rtype: str
544         """
545         value = ''
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)
552         else:
553             byte = byteIter.next()
554             if byte == 128:
555                 value = 'Yes'
556             elif byte == 129:
557                 value = 'No'
558         return value
559
560     @staticmethod
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>
566         
567         @return: The "From" address value
568         @rtype: str
569         """
570         fromValue = ''
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>'
576         else:
577             fromValue = MMSDecoder.decodeEncodedStringValue(byteIter)
578         return fromValue
579     
580     @staticmethod
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>
588         Auto = <Octet 131>
589         The token-text is an extension method to the message class.
590         
591         @return: The decoded message class
592         @rtype: str
593         """
594         classIdentifiers = {128 : 'Personal',
595                             129 : 'Advertisement',
596                             130 : 'Informational',
597                             131 : 'Auto'}
598         msgClass = ''
599 #        byteIter, localIter = itertools.tee(byteIter)
600 #        byte = localIter.next()
601         byte = byteIter.preview()
602         if byte in classIdentifiers:
603             byteIter.next()
604             msgClass = classIdentifiers[byte]
605         else:
606             byteIter.resetPreview()
607             msgClass = wsp_pdu.Decoder.decodeTokenText(byteIter)
608         return msgClass
609
610     @staticmethod
611     def decodeMessageTypeValue(byteIter):
612         """ Defined in [4], section 7.2.14.
613
614         @return: The decoded message type, or '<unknown>'
615         @rtype: str
616         """
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:
626             byteIter.next()
627             return messageTypes[byte]
628         else:
629             byteIter.resetPreview()
630             return '<unknown>'
631     
632     @staticmethod
633     def decodePriorityValue(byteIter):
634         """ Defined in [4], section 7.2.17
635         
636         @raise wsp_pdu.DecodeError: The priority value could not be decoded;
637                                 C{byteIter} is not modified in this case.
638         
639         @return: The decoded priority value
640         @rtype: str
641         """
642         priorities = {128 : 'Low',
643                       129 : 'Normal',
644                       130 : 'High'}
645 #        byteIter, localIter = itertools.tee(byteIter)
646         byte = byteIter.preview()
647         if byte in priorities:
648             byte = byteIter.next()
649             return priorities[byte]
650         else:
651             byteIter.resetPreview()
652             raise wsp_pdu.DecodeError, 'Error parsing Priority value for byte:',byte
653         
654     @staticmethod
655     def decodeSenderVisibilityValue(byteIter):
656         """ Defined in [4], section 7.2.22::
657          Sender-visibility-value = Hide | Show
658          Hide = <Octet 128>
659          Show = <Octet 129>
660         
661         @raise wsp_pdu.DecodeError: The sender visibility value could not be
662                                 parsed.
663                                 C{byteIter} will not be modified in this case.
664         
665         @return: The sender visibility: 'Hide' or 'Show'
666         @rtype: str
667         """
668         value = ''
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)
675         else:
676             byte = byteIter.next()
677             if byte == 128:
678                 value = 'Hide'
679             elif byte == 129:
680                 value = 'Show'
681         return value
682     
683     @staticmethod
684     def decodeResponseStatusValue(byteIter):
685         """ Defined in [4], section 7.2.20
686         
687         Used to decode the "Response Status" MMS header.
688         
689         @raise wsp_pdu.DecodeError: The sender visibility value could not be
690                                 parsed.
691                                 C{byteIter} will not be modified in this case.
692         
693         @return: The decoded Response-status-value
694         @rtype: str
695         """
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:
707             byteIter.next()
708             return responseStatusValues[byte]
709         else:
710             byteIter.next()
711             # Return an unspecified error if the response is not recognized
712             return responseStatusValues[0x81]
713         
714     @staticmethod
715     def decodeStatusValue(byteIter):
716         """ Defined in [4], section 7.2.23
717         
718         Used to decode the "Status" MMS header.
719         
720         @raise wsp_pdu.DecodeError: The sender visibility value could not be
721                                 parsed.
722                                 C{byteIter} will not be modified in this case.
723         
724         @return: The decoded Status-value
725         @rtype: str
726         """
727         
728         statusValues = {0x80 : 'Expired',
729                         0x81 : 'Retrieved',
730                         0x82 : 'Rejected',
731                         0x83 : 'Deferred',
732                         0x84 : 'Unrecognised'}
733         
734         byte = byteIter.preview()
735         if byte in statusValues:
736             byteIter.next()
737             return statusValues[byte]
738         else:
739             byteIter.next()
740             # Return an unrecognised state if it couldn't be decoded
741             return statusValues[0x84]
742     
743     
744     @staticmethod
745     def decodeExpiryValue(byteIter):
746         """ Defined in [4], section 7.2.10
747         
748         Used to decode the "Expiry" MMS header.
749         
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>
754
755         @raise wsp_pdu.DecodeError: The Expiry-value could not be decoded
756         
757         @return: The decoded Expiry-value, either as a date, or as a delta-seconds value
758         @rtype: str or int
759         """
760         valueLength = MMSDecoder.decodeValueLength(byteIter)
761         token = byteIter.next()
762         
763         if token == 0x80: # Absolute-token
764             data = MMSDecoder.decodeDateValue(byteIter)
765         elif token == 0x81: # Relative-token
766             data = MMSDecoder.decodeDeltaSecondsValue(byteIter)
767         else:
768             raise wsp_pdu.DecodeError, 'Unrecognized token value: %s' % hex(token)
769         return data
770         
771     
772 class MMSEncoder(wsp_pdu.Encoder):
773     def __init__(self, noHeaders=None):
774         self.noHeaders = noHeaders
775         self._mmsMessage = message.MMSMessage(noHeaders)
776     
777     def encode(self, mmsMessage):
778         """ Encodes the specified MMS message
779         
780         @param mmsMessage: The MMS message to encode
781         @type mmsMessage: MMSMessage
782         
783         @return: The binary-encoded MMS data, as a sequence of bytes
784         @rtype: array.array('B')
785         """
786         self._mmsMessage = mmsMessage
787         msgData = self.encodeMessageHeader()
788         if self.noHeaders == None:
789                 msgData.extend(self.encodeMessageBody())
790         return msgData
791     
792     def encodeMessageHeader(self):
793         """ Binary-encodes the MMS header data.
794         
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].
798                 
799         @return: the MMS PDU header, as an array of bytes
800         @rtype: array.array('B')
801         """        
802         # See [4], chapter 8 for info on how to use these
803         fromTypes = {'Address-present-token' : 0x80,
804                      'Insert-address-token'  : 0x81}
805             
806         contentTypes = {'application/vnd.wap.multipart.related' : 0xb3}
807         
808         # Create an array of 8-bit values
809         messageHeader = array.array('B')
810     
811         headersToEncode = self._mmsMessage.headers
812         
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'
818                 else:
819                     cleanHeader = hdr.replace('X-Mms-', '', 1)
820                 headersToEncode[cleanHeader] = headersToEncode[hdr]
821                 del headersToEncode[hdr]
822                 
823         # First 3  headers (in order), according to [4]:
824         ################################################
825         # - X-Mms-Message-Type
826         # - X-Mms-Transaction-ID
827         # - X-Mms-Version
828         
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'
834         
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
845                     break
846             if not foundDestAddress:
847                 headersToEncode['Message-Type'] = 'm-retrieve-conf'
848         ### End of Message-Type verification
849         
850         ### Start of Transaction-Id verification
851         if 'Transaction-Id' not in headersToEncode:
852             import random
853             headersToEncode['Transaction-Id'] = str(random.randint(1000, 9999))
854         ### End of Transaction-Id verification
855         
856         ### Start of MMS-Version verification
857         if 'MMS-Version' not in headersToEncode:
858             headersToEncode['MMS-Version'] = '1.0'
859         
860         messageType = headersToEncode['Message-Type']
861         
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]
866     
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':
871                 continue
872             messageHeader.extend(MMSEncoder.encodeHeader(hdr, headersToEncode[hdr]))
873         
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))
882         return messageHeader
883     
884     def encodeMessageBody(self):
885         """ Binary-encodes the MMS body data.
886         
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::
890         
891             [ header ][ entries ]
892             ^^^^^^^^^^^^^^^^^^^^^
893                   MMS Body
894         
895         The MMS Body header consists of one entry[5]::
896          name          type          purpose
897          -------      -------        -----------
898          nEntries     Uintvar        number of entries in the multipart entity
899         
900         The MMS body's multipart entries structure::
901          name             type                   purpose
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> 
908                            - length of 
909                           <ContentType>) octets  the part's headers
910          Data             <DataLen> octets       the part's data
911         
912         @note: The MMS body's header should not be confused with the actual
913                MMS header, as returned by C{_encodeHeader()}.
914         
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.
918                
919         @return: The binary-encoded MMS PDU body, as an array of bytes
920         @rtype: array.array('B')
921         """
922
923         messageBody = array.array('B')
924         
925         #TODO: enable encoding of MMSs without SMIL file
926         ########## MMS body: header ##########
927         # Parts: SMIL file + <number of data elements in each slide>
928         nEntries = 1
929         for page in self._mmsMessage._pages:
930             nEntries += page.numberOfParts()
931         for dataPart in self._mmsMessage._dataParts:
932             nEntries += 1
933             
934         messageBody.extend(self.encodeUintvar(nEntries))
935         
936         ########## MMS body: entries ##########
937         # For every data "part", we have to add the following sequence:
938         # <length of content-type + other possible headers>,
939         # <length of data>,
940         # <content-type + other possible headers>,
941         # <data>.
942
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>'
949         parts = [smilPart]
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])
954     
955         for part in parts:
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':
960                     continue
961                 encodedPartHeaders.extend(wsp_pdu.Encoder.encodeHeader(hdr, part.headers[hdr]))
962         
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)))
968             # ContentType entry
969             messageBody.extend(partContentType)
970             # Headers
971             messageBody.extend(encodedPartHeaders)
972             # Data (note: we do not null-terminate this)
973             for char in part.data:
974                 messageBody.append(ord(char))
975         return messageBody
976
977     
978     @staticmethod
979     def encodeHeader(headerFieldName, headerValue):
980         """ Encodes a header entry for an MMS message
981         
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}
988                     
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.
994         
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).
999         
1000         @return: The decoded header entry from the MMS, in the format:
1001                  (<str:header name>, <str/int/float:header value>)
1002         @rtype: tuple
1003         """
1004         encodedHeader = []
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]
1011                 try:
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
1015                 except:
1016                     print 'A fatal error occurred, probably due to an unimplemented encoding operation'
1017                     raise
1018                 break
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)
1024             # Now add the value
1025             encodedHeader.extend(wsp_pdu.Encoder.encodeTextString(headerValue))
1026         return encodedHeader
1027     
1028     @staticmethod
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].
1032         
1033         From [4], section 7.1:
1034         C{MMS-field-name = Short-integer}
1035         
1036         @raise EncodeError: The specified header field name is not a
1037                             well-known MMS header.
1038         
1039         @param fieldName: The header field name to encode
1040         @type fieldName: str
1041         
1042         @return: The encoded header field name, as a sequence of bytes
1043         @rtype: list
1044         """
1045         encodedMMSFieldName = []
1046         for assignedNumber in MMSEncodingAssignments.fieldNames:
1047             if MMSEncodingAssignments.fieldNames[assignedNumber][0] == fieldName:
1048                 encodedMMSFieldName.extend(wsp_pdu.Encoder.encodeShortInteger(assignedNumber))
1049                 break
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
1053     
1054     @staticmethod
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>
1060         
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
1066         
1067         @return: The encoded "From" address value, as a sequence of bytes
1068         @rtype: list
1069         """
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
1075         else:
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
1083
1084     @staticmethod
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.
1089         
1090         @param stringValue: The text string to encode
1091         @type stringValue: str
1092         
1093         @note: This function is currently a simple wrappper to
1094                C{encodeTextString()}
1095         
1096         @return: The encoded string value, as a sequence of bytes
1097         @rtype: list
1098         """
1099         return wsp_pdu.Encoder.encodeTextString(stringValue)
1100     
1101     @staticmethod
1102     def encodeMessageTypeValue(messageType):
1103         """ Defined in [4], section 7.2.14.
1104         
1105         @note: Unknown message types are discarded; thus they will be encoded
1106                as 0x80 ("m-send-req") by this function
1107         
1108         @param messageType: The MMS message type to encode
1109         @type messageType: str
1110
1111         @return: The encoded message type, as a sequence of bytes
1112         @rtype: list
1113         """
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]]
1123         else:
1124             return [0x80]
1125
1126     @staticmethod
1127     def encodeStatusValue(statusValue):
1128         """ Defined in [4], section 7.2.#
1129         
1130         Used to encode the "Status" MMS header.
1131         
1132         @return: The encoded Status-value, or 0x84 ('Unrecognised') if none
1133         @rtype: str
1134         """
1135         
1136         statusValues = {'Expired' : 0x80,
1137                         'Retrieved' : 0x81,
1138                         'Rejected' : 0x82,
1139                         'Deferred' : 0x83,
1140                         'Unrecognised' : 0x84}
1141         
1142         if statusValue in statusValues:
1143                 return [statusValues[statusValue]]
1144         else:
1145                 return [0x84]