initial commit master
authorNick Leppänen Larsson <frals@frals.se>
Mon, 25 Jan 2010 20:30:50 +0000 (21:30 +0100)
committerNick Leppänen Larsson <frals@frals.se>
Mon, 25 Jan 2010 20:30:50 +0000 (21:30 +0100)
24 files changed:
src/COPYING [new file with mode: 0644]
src/CREDITS [new file with mode: 0644]
src/LAST_INCOMING [new file with mode: 0644]
src/LAST_OUTGOING [new file with mode: 0644]
src/__init__.py [new file with mode: 0644]
src/contacts.py [new file with mode: 0644]
src/controller.py [new file with mode: 0644]
src/dbhandler.py [new file with mode: 0644]
src/fmms.png [new file with mode: 0644]
src/fmms_config.py [new file with mode: 0644]
src/fmms_gui.py [new file with mode: 0644]
src/fmms_sender_ui.py [new file with mode: 0644]
src/fmms_viewer.py [new file with mode: 0644]
src/fmmsd.py [new file with mode: 0644]
src/mms/COPYING [new file with mode: 0644]
src/mms/COPYING.LESSER [new file with mode: 0644]
src/mms/WSP.py [new file with mode: 0644]
src/mms/WTP.py [new file with mode: 0644]
src/mms/__init__.py [new file with mode: 0644]
src/mms/iterator.py [new file with mode: 0644]
src/mms/message.py [new file with mode: 0644]
src/mms/mms_pdu.py [new file with mode: 0644]
src/mms/wsp_pdu.py [new file with mode: 0644]
src/wappushhandler.py [new file with mode: 0644]

diff --git a/src/COPYING b/src/COPYING
new file mode 100644 (file)
index 0000000..d511905
--- /dev/null
@@ -0,0 +1,339 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/src/CREDITS b/src/CREDITS
new file mode 100644 (file)
index 0000000..670554f
--- /dev/null
@@ -0,0 +1,2 @@
+claesbas (Claes Norin) - for the great logo!
+Francois Aucamp <faucamp@csir.co.za> - for the original PyMMS library
\ No newline at end of file
diff --git a/src/LAST_INCOMING b/src/LAST_INCOMING
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/LAST_OUTGOING b/src/LAST_OUTGOING
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/__init__.py b/src/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/contacts.py b/src/contacts.py
new file mode 100644 (file)
index 0000000..c8a0b98
--- /dev/null
@@ -0,0 +1,135 @@
+#!/usr/bin/env python2.5
+# -*- coding: utf-8 -*-
+""" Handles contacts integration for fMMS
+
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU GPL
+"""
+import evolution
+import gtk
+       
+class ContactHandler:
+       
+       
+       """ wouldnt mind some nice patches against this """
+       def __init__(self):
+               self.ab = evolution.ebook.open_addressbook("default")
+               self.contacts = self.ab.get_all_contacts()
+               self.phonedict = {}
+               for c in self.contacts:
+                       #print c.get_name(), c.get_property('mobile-phone')
+                       #print c.get_property('other-phone')
+                       # this was a pretty clean solution as well, but oh so wrong!
+                       mb = c.get_property('mobile-phone')
+                       cp = c.get_property('other-phone')
+                       nrlist = (mb, cp)
+                       fname = c.get_name()
+                       # TODO: this is _really_ slow... look at integration with c please
+                       #nrlist = self.get_numbers_from_name(fname)
+                       self.phonedict[c.get_name()] = nrlist
+       
+       """ returns all the numbers from a name, as a list """
+       def get_numbers_from_name(self, fname):
+               search = self.ab.search(fname)
+               res = search[0]
+               # would've been nice if this got all numbers, but alas, it dont.
+               """props = ['assistant-phone', 'business-phone', 'business-phone-2', 'callback-phone', 'car-phone', 'company-phone', 'home-phone', 'home-phone-2', 'mobile-phone', 'other-phone', 'primary-phone']
+               nrlist = []
+               for p in props:
+                       nr = res.get_property(p)
+                       if nr != None:
+                               nrlist.append(nr)"""
+               # creative use of processing power? *cough*
+               nrlist = {}
+               vcardlist = res.get_vcard_string().replace('\r', '').split('\n')
+               for line in vcardlist:
+                       if line.startswith("TEL"):
+                               #print line
+                               nr = line.split(":")[1]
+                               ltype = line.split(":")[0].split("=")
+                               phonetype = "Unknown"
+                               try:
+                                       for type in ltype:
+                                               rtype = type.replace(";TYPE", "")
+                                               if rtype != "TEL" and rtype != "VOICE":
+                                                       phonetype = rtype       
+                               except:
+                                       pass
+                               if nr != None:
+                                       nrlist[nr] = phonetype
+               return nrlist
+               
+       
+       """ returns all contact names sorted by name """
+       def get_contacts_as_list(self):
+               retlist = []
+               for contact in self.contacts:
+                       cn = contact.get_name()
+                       if cn != None:
+                               retlist.append(cn)
+               retlist.sort(key=str.lower)
+               return retlist
+       
+       """ takes a number on international format (ie +46730111111) """
+       def get_name_from_number(self, number):
+               ### do some voodoo here
+               # ugly way of removing country code since this
+               # can be 2 or 3 chars we remove 4 just in case
+               # 3 and the + char = 4
+               numberstrip = number[4:-1]
+               for person in self.phonedict:   
+                       for cbnr in self.phonedict[person]:
+                               if cbnr != None:
+                                       cbnr = cbnr.replace(" ", "")
+                                       cbnr = cbnr.lstrip('0')
+                                       if cbnr == number or numberstrip.endswith(cbnr) or number.endswith(cbnr):
+                                               return person
+                                       
+               return None
+       
+       def get_photo_from_name(self, pname, imgsize):
+               res = self.ab.search(pname)
+               ### do some nice stuff here
+               #l = [x.get_name() for x in res]
+               #print "search for:", pname, "gave res: ", l
+               if res != None:
+                       img = res[0].get_photo(imgsize)
+                       if img == None:
+                               vcardlist = res[0].get_vcard_string().replace('\r', '').split('\n') # vcard for first result
+                               for line in vcardlist:
+                                       if line.startswith("PHOTO;VALUE=uri:file://"):
+                                               imgstr = line.replace("PHOTO;VALUE=uri:file://", "")
+                                               img = gtk.gdk.pixbuf_new_from_file(imgstr)
+                                               height = img.get_height()
+                                               if height != imgsize:
+                                                       newheight = imgsize
+                                                       newwidth = int(newheight * img.get_width() / height)
+                                                       #print "h:", height, "w:", img.get_width()
+                                                       #print "newh:", newheight, "neww:", newwidth
+                                                       img = img.scale_simple(newwidth, newheight, gtk.gdk.INTERP_BILINEAR)
+                       return img
+               else:
+                       return None
+               
+               
+if __name__ == '__main__':
+       cb = ContactHandler()
+       #c = ab.get_contact("id")
+       #c.get_name()
+       #print cb.ab.get_all_contacts()[0].__doc__
+       #asd = cb.get_contacts_as_list()
+       #print asd
+       
+       """r = cb.ab.search('Tom Le') # Returns List of results
+       print r
+       l = [x.get_name() for x in r] # list of results
+       u = r[0].get_name() # name of the first result
+       vcardlist = r[0].get_vcard_string().replace('\r', '').split('\n') # vcard for first result
+       #print vcardlist"""
+       #print cb.get_numbers_from_name('')
+       """
+       for line in vcardlist:
+               if not line.startswith("PHOTO"):
+                       print line
+       """
+       #print r[0].get_photo(64)
diff --git a/src/controller.py b/src/controller.py
new file mode 100644 (file)
index 0000000..90c4192
--- /dev/null
@@ -0,0 +1,264 @@
+#!/usr/bin/env python2.5
+# -*- coding: utf-8 -*-
+""" Useful functions that shouldn't be in the UI code
+
+
+And, yes, I know this is not really a controller.
+
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU GPL
+"""
+import os
+import array
+
+import dbus
+from dbus.mainloop.glib import DBusGMainLoop
+
+import fmms_config as fMMSconf
+import dbhandler as DBHandler
+from mms.message import MMSMessage
+from mms import mms_pdu
+
+#TODO: constants.py?
+MSG_DIRECTION_IN = 0
+MSG_DIRECTION_OUT = 1
+MSG_UNREAD = 0
+MSG_READ = 1
+
+class fMMS_controller():
+       
+       def __init__(self):
+               self.config = fMMSconf.fMMS_config()
+               self._mmsdir = self.config.get_mmsdir()
+               self._pushdir = self.config.get_pushdir()
+               self._outdir = self.config.get_outdir()
+               self.store = DBHandler.DatabaseHandler()
+       
+       
+       def decode_mms_from_push(self, binarydata):
+               decoder = mms_pdu.MMSDecoder()
+               wsplist = decoder.decodeCustom(binarydata)
+
+               sndr, url, trans_id = None, None, None
+               bus = dbus.SystemBus()
+               proxy = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
+               interface = dbus.Interface(proxy,dbus_interface='org.freedesktop.Notifications')
+
+               try:
+                       url = wsplist["Content-Location"]
+                       print "content-location:", url
+                       trans_id = wsplist["Transaction-Id"]
+                       trans_id = str(trans_id)
+                       print "transid:", trans_id
+               except Exception, e:
+                       print "no content-location/transid in push; aborting...", type(e), e
+                       interface.SystemNoteInfoprint ("fMMS: Failed to parse SMS PUSH.")
+                       raise
+               try:
+                       sndr = wsplist["From"]
+                       print "Sender:", sndr
+               except Exception, e:
+                       print "No sender value defined", type(e), e
+                       sndr = "Unknown sender"
+
+               self.save_binary_push(binarydata, trans_id)
+               return (wsplist, sndr, url, trans_id)
+       
+       
+       def save_binary_push(self, binarydata, transaction):
+               data = array.array('B')
+               for b in binarydata:
+                       data.append(b)
+               # TODO: move to config?
+               if not os.path.isdir(self._pushdir):
+                       os.makedirs(self._pushdir)
+               try:
+                       fp = open(self._pushdir + transaction, 'wb')
+                       fp.write(data)
+                       print "saved binary push", fp
+                       fp.close()
+               except Exception, e:
+                       print "failed to save binary push:", type(e), e
+                       raise
+       
+       def save_push_message(self, data):
+               """ Gets the decoded data as a list (preferably from decode_mms_from_push)
+               """
+               pushid = self.store.insert_push_message(data)
+               return pushid
+       
+       
+       def get_push_list(self, types=None):
+               return self.store.get_push_list()
+               
+       
+       def is_fetched_push_by_transid(self, transactionid):
+               return self.store.is_mms_downloaded(transactionid)
+       
+       
+       def read_push_as_list(self, transactionid):
+               return self.store.get_push_message(transactionid)
+       
+       
+       def save_binary_mms(self, data, transaction):
+               dirname = self._mmsdir + transaction
+               if not os.path.isdir(dirname):
+                       os.makedirs(dirname)
+               
+               fp = open(dirname + "/message", 'wb')
+               fp.write(data)
+               print "saved binary mms", fp
+               fp.close()
+               return dirname
+               
+       def save_binary_outgoing_mms(self, data, transaction):
+               transaction = str(transaction)
+               dirname = self._outdir + transaction
+               if not os.path.isdir(dirname):
+                       os.makedirs(dirname)
+
+               fp = open(dirname + "/message", 'wb')
+               fp.write(data)
+               print "saved binary mms", fp
+               fp.close()
+               return dirname
+       
+       def decode_binary_mms(self, path):
+               """ decodes and saves the binary mms"""
+               # Decode the specified file
+               # This also creates all the parts as files in path
+               print "decode_binary_mms running"
+               try:
+                       message = MMSMessage.fromFile(path + "/message")
+               except Exception, e:
+                       print type(e), e
+                       raise
+               print "returning message!"
+               return message
+       
+       
+       def store_mms_message(self, pushid, message):
+               mmsid = self.store.insert_mms_message(pushid, message)
+               return mmsid
+       
+       def store_outgoing_mms(self, message):
+               mmsid = self.store.insert_mms_message(0, message, DBHandler.MSG_DIRECTION_OUT)
+               return mmsid
+               
+       def store_outgoing_push(self, wsplist):
+               pushid = self.store.insert_push_send(wsplist)
+               return pushid
+               
+       def link_push_mms(self, pushid, mmsid):
+               self.store.link_push_mms(pushid, mmsid)
+       
+       def get_direction_mms(self, transid):
+               return self.store.get_direction_mms(transid)
+       
+       def get_mms_from_push(self, transactionid):
+               plist = self.store.get_push_message(transactionid)
+               trans_id = plist['Transaction-Id']
+               pushid = plist['PUSHID']
+               url = plist['Content-Location']
+               
+               from wappushhandler import PushHandler
+               p = PushHandler()
+               path = p._get_mms_message(url, trans_id)
+               print "decoding mms..."
+               message = self.cont.decode_binary_mms(path)
+               print "storing mms..."
+               mmsid = self.cont.store_mms_message(pushid, message)
+               
+               
+       def get_mms_attachments(self, transactionid, allFiles=False):
+               return self.store.get_mms_attachments(transactionid, allFiles)
+       
+       def get_mms_headers(self, transactionid):
+               return self.store.get_mms_headers(transactionid)
+       
+       def delete_mms_message(self, fname):
+               fullpath = self._mmsdir + fname
+               print fullpath
+               if os.path.isdir(fullpath):
+                       print "starting deletion of", fullpath
+                       filelist = self.get_mms_attachments(fname, allFiles=True)
+                       if filelist == None:
+                               filelist = []
+                       filelist.append("message")
+                       for fn in filelist:
+                               try:
+                                       fullfn = fullpath + "/" + fn
+                                       os.remove(fullfn)
+                               except:
+                                       print "failed to remove", fullfn
+                       try:
+                               print "trying to remove", fullpath
+                               os.rmdir(fullpath)
+                       except OSError, e:
+                               print "failed to remove dir:", type(e), e
+                               raise
+               self.store.delete_mms_message(fname)
+               
+       def delete_push_message(self, fname):
+               fullpath = self._pushdir + fname
+               print fullpath
+               if os.path.isfile(fullpath):
+                       print "removing", fullpath
+                       try:
+                               os.remove(fullpath)
+                       except Exception, e:
+                               raise
+               self.store.delete_push_message(fname)
+               
+       def wipe_message(self, transactionid):
+               self.delete_mms_message(transactionid)
+               self.delete_push_message(transactionid)
+       
+       """ DEPRECATED AS OF 0.2.10
+       gets a mms from a previously received push """
+       """ this function requires the fname to be the fullpath """
+       # TODO: dont require fullpath
+       """def get_mms_from_push(self, fname):
+               
+               plist = self.read_push_as_list(fname)
+               try:
+                       sndr = plist['From']
+               except:
+                       sndr = "Unknown"
+               url = plist['Content-Location']
+               print url
+               trans_id = plist['Transaction-Id']
+               print trans_id
+               
+               from wappushhandler import PushHandler
+               push = PushHandler()
+               path = push._get_mms_message(url, trans_id)
+               Push.decodeMMS(path)
+               
+               return 0"""
+       
+       """ Old function relying on files... Deprecated as of 0.2.10
+       def is_fetched_push(self, filename):
+               this function takes the FILENAME, not the full path
+               path = self._mmsdir + filename
+               if os.path.isdir(path):
+                       if os.path.isfile(self._mmsdir + filename + "/message"):
+                               return True
+               else:
+               return False"""
+       
+       
+       """def read_push_as_list(self, fname):
+                       # reads a saved push message into a dict
+                       fp = open(fname, 'r')
+                       pdict = {}
+                       for line in fp:
+                               line = line.replace("\n", "")
+                               lsplit = line.partition(" ")
+                               pdict[lsplit[0]] = lsplit[2]
+                       fp.close()
+               return pdict"""
+       
+if __name__ == '__main__':
+       c = fMMS_controller()
+       pass
\ No newline at end of file
diff --git a/src/dbhandler.py b/src/dbhandler.py
new file mode 100644 (file)
index 0000000..36de632
--- /dev/null
@@ -0,0 +1,403 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+""" database handler for fMMS
+
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU GPL
+"""
+import sqlite3
+import os
+
+from gnome import gnomevfs
+
+import fmms_config as fMMSconf
+
+
+# TODO: constants.py?
+MSG_DIRECTION_IN = 0
+MSG_DIRECTION_OUT = 1
+MSG_UNREAD = 0
+MSG_READ = 1
+
+
+class DatabaseHandler:
+       
+       def __init__(self):
+               self.config = fMMSconf.fMMS_config()
+               self.pushdir = self.config.get_pushdir()
+               self.mmsdir = self.config.get_mmsdir()
+               self.outdir = self.config.get_outdir()
+               self.db = self.config.get_db_path()
+               self.conn = sqlite3.connect(self.db)
+               self.conn.row_factory = sqlite3.Row
+               try:
+                       c = self.conn.cursor()
+                       c.execute("SELECT * FROM revision")
+                       for row in c:
+                               if row['version'] != 1:
+                                       self.create_database_layout()
+               except:
+                       self.create_database_layout()
+
+               
+       def create_database_layout(self):
+               c = self.conn
+               c.execute("""CREATE TABLE "revision" ("version" INT);""")
+               c.execute("""INSERT INTO "revision" ("version") VALUES ('1');""")
+               # database layout
+               c.execute("""CREATE TABLE "push"(
+                         "idpush" INTEGER PRIMARY KEY NOT NULL,
+                         "transactionid" TEXT NOT NULL,
+                         "content_location" TEXT NULL,
+                         "msg_time" TIMESTAMP,
+                         "msg_type" TEXT NOT NULL,
+                         "file" TEXT
+                       );""")
+               c.execute("""CREATE TABLE "contacts"(
+                         "idcontacts" INTEGER PRIMARY KEY NOT NULL,
+                         "number" INTEGER NOT NULL,
+                         "abook_uid" INTEGER DEFAULT NULL
+                       );""")
+               c.execute("""CREATE TABLE "mms"(
+                         "id" INTEGER PRIMARY KEY NOT NULL,
+                         "pushid" INTEGER DEFAULT NULL,
+                         "transactionid" INTEGER DEFAULT NULL,
+                         "msg_time" TIMESTAMP DEFAULT NULL,
+                         "read" INTEGER DEFAULT NULL,
+                         "direction" INTEGER DEFAULT NULL,
+                         "size" INT DEFAULT NULL,
+                         "contact" INTEGER DEFAULT NULL,
+                         "file" TEXT DEFAULT NULL,
+                         CONSTRAINT "pushid"
+                           FOREIGN KEY("pushid")
+                           REFERENCES "push"("idpush"),
+                         CONSTRAINT "contact"
+                           FOREIGN KEY("contact")
+                           REFERENCES "contacts"("idcontacts")
+                       );""")
+               c.execute("""CREATE INDEX "mms.pushid" ON "mms"("pushid");""")
+               c.execute("""CREATE INDEX "mms.contact" ON "mms"("contact");""")
+               c.execute("""CREATE TABLE "mms_headers"(
+                         "idmms_headers" INTEGER PRIMARY KEY NOT NULL,
+                         "mms_id" INTEGER DEFAULT NULL,
+                         "header" TEXT DEFAULT NULL,
+                         "value" TEXT DEFAULT NULL,
+                         CONSTRAINT "mms_id"
+                           FOREIGN KEY("mms_id")
+                           REFERENCES "mms"("id")
+                       );""")
+               c.execute("""CREATE INDEX "mms_headers.mms_id" ON "mms_headers"("mms_id");""")
+               c.execute("""CREATE TABLE "attachments"(
+                         "idattachments" INTEGER PRIMARY KEY NOT NULL,
+                         "mmsidattach" INTEGER DEFAULT NULL,
+                         "file" TEXT DEFAULT NULL,
+                         "hidden" INTEGER DEFAULT NULL,
+                         CONSTRAINT "mmsidattach"
+                           FOREIGN KEY("mmsidattach")
+                           REFERENCES "mms"("id")
+                       );""")
+               c.execute("""CREATE INDEX "attachments.mmsidattach" ON "attachments"("mmsidattach");""")
+               c.execute("""CREATE TABLE "push_headers"(
+                         "idpush_headers" INTEGER PRIMARY KEY NOT NULL,
+                         "push_id" INTEGER DEFAULT NULL,
+                         "header" TEXT DEFAULT NULL,
+                         "value" TEXT DEFAULT NULL,
+                         CONSTRAINT "push_id"
+                           FOREIGN KEY("push_id")
+                           REFERENCES "push"("idpush")
+                       );""")
+               c.execute("""CREATE INDEX "push_headers.push_id" ON "push_headers"("push_id");""")
+               self.conn.commit()
+
+
+       def get_push_list(self, types=None):
+               """ gets all push messages from the db and returns as a list
+               containing a dict for each separate push """
+               c = self.conn.cursor()
+               retlist = []
+               # TODO: better where clause
+               c.execute("select * from push where msg_type != 'm-notifyresp-ind' order by msg_time DESC")
+               pushlist = c.fetchall()
+               for line in pushlist:
+                       result = {}
+                       result['PUSHID'] = line['idpush']
+                       result['Transaction-Id'] = line['transactionid']
+                       result['Content-Location'] = line['content_location']
+                       result['Time'] = line['msg_time']
+                       result['Message-Type'] = line['msg_type']
+                       c.execute("select * from push_headers WHERE push_id = ?", (line['idpush'],))
+                       for line2 in c:
+                               result[line2['header']] = line2['value']
+                               
+                       retlist.append(result)
+
+               return retlist
+
+       def insert_push_message(self, pushlist):
+               """ Inserts a push message (from a list)
+               Returns the id of the inserted row
+               
+               """
+               c = self.conn.cursor()
+               conn = self.conn
+               try:
+                       transid = pushlist['Transaction-Id']
+                       del pushlist['Transaction-Id']
+                       contentloc = pushlist['Content-Location']
+                       del pushlist['Content-Location']
+                       msgtype = pushlist['Message-Type']
+                       del pushlist['Message-Type']
+               except:
+                       print "No transid/contentloc/message-type, bailing out!"
+                       raise
+               fpath = self.pushdir + transid
+               vals = (transid, contentloc, msgtype, fpath)
+               c.execute("insert into push (transactionid, content_location, msg_time, msg_type, file) VALUES (?, ?, datetime('now'), ?, ?)", vals)
+               pushid = c.lastrowid
+               conn.commit()
+               print "inserted row as:", pushid
+               
+               for line in pushlist:
+                       vals = (pushid, line, str(pushlist[line]))
+                       c.execute("insert into push_headers (push_id, header, value) VALUES (?, ?, ?)", vals)
+                       conn.commit()
+                       
+               return pushid
+       
+       def insert_push_send(self, pushlist):
+               """ Inserts a push message (from a list)
+               Returns the id of the inserted row
+
+               """
+               c = self.conn.cursor()
+               conn = self.conn
+               try:
+                       transid = pushlist['Transaction-Id']
+                       del pushlist['Transaction-Id']
+                       msgtype = pushlist['Message-Type']
+                       del pushlist['Message-Type']
+               except:
+                       print "No transid/message-type, bailing out!"
+                       raise
+               fpath = self.outdir + transid
+               vals = (transid, 0, msgtype, fpath)
+               c.execute("insert into push (transactionid, content_location, msg_time, msg_type, file) VALUES (?, ?, datetime('now'), ?, ?)", vals)
+               pushid = c.lastrowid
+               conn.commit()
+               print "inserted row as:", pushid
+
+               for line in pushlist:
+                       vals = (pushid, line, str(pushlist[line]))
+                       c.execute("insert into push_headers (push_id, header, value) VALUES (?, ?, ?)", vals)
+                       conn.commit()
+                                       
+               return pushid
+
+       def link_push_mms(self, pushid, mmsid):
+               c = self.conn.cursor()
+               c.execute("update mms set pushid = ? where id = ?", (pushid, mmsid))
+               self.conn.commit()
+               
+
+       def get_push_message(self, transid):
+               """ retrieves a push message from the db and returns it as a dict """
+               c = self.conn.cursor()
+               retlist = {}
+               vals = (transid,)
+               c.execute("select * from push WHERE transactionid = ? LIMIT 1;", vals)
+               
+               for line in c:
+                       pushid = line['idpush']
+                       retlist['Transaction-Id'] = line['transactionid']
+                       retlist['Content-Location'] = line['content_location']
+                       retlist['Message-Type'] = line['msg_type']
+                       retlist['Time'] = line['msg_time']
+                       retlist['File'] = line['file']
+                       retlist['PUSHID'] = pushid
+               
+               try:
+                       c.execute("select * from push_headers WHERE push_id = ?;", (pushid, ))
+               except Exception, e:
+                       raise
+               
+               for line in c:
+                       hdr = line['header']
+                       val = line['value']
+                       retlist[hdr] = val
+               
+               return retlist
+       
+
+       def is_mms_downloaded(self, transid):
+               c = self.conn.cursor()
+               vals = (transid,)
+               isread = None
+               c.execute("select * from mms where `transactionid` = ?;", vals)
+               for line in c:
+                       isread = line['id']
+               if isread != None:
+                       return True
+               else:
+                       return False
+                       
+       
+       def is_message_read(self, transactionid):
+               c = self.conn.cursor()
+               vals = (transactionid,)
+               isread = None
+               c.execute("select read from mms where `transactionid` = ?;", vals)
+               for line in c:
+                       isread = line['read']
+               if isread == 1:
+                       return True
+               else:
+                       return False
+       
+       def insert_mms_message(self, pushid, message, direction=MSG_DIRECTION_IN):
+               """Takes a MMSMessage object as input, and optionally a MSG_DIRECTION_*
+               Returns the newly inserted rows id.
+               
+               """             
+               #print direction
+               mmslist = message.headers
+               attachments = message.attachments
+               #mmslist = message
+               c = self.conn.cursor()
+               conn = self.conn
+               try:
+                       transid = mmslist['Transaction-Id']
+                       del mmslist['Transaction-Id']
+                       if direction == MSG_DIRECTION_OUT:
+                               basedir = self.outdir + transid
+                       else:
+                               basedir = self.mmsdir + transid
+                               
+                       fpath = basedir + "/message"
+                       size = os.path.getsize(fpath)
+               except:
+                       print "No transid/message-type, bailing out!"
+                       raise
+               try:
+                       time = mmslist['Date']
+                       del mmslist['Date']
+               except:
+                       time = "datetime('now')"
+               isread = MSG_UNREAD
+               contact = 0
+               vals = (pushid, transid, time, isread, direction, size, contact, fpath)
+               c.execute("insert into mms (pushid, transactionid, msg_time, read, direction, size, contact, file) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", vals)
+               mmsid = c.lastrowid
+               conn.commit()
+               print "inserted row as:", mmsid
+               
+               # insert all headers
+               for line in mmslist:
+                       vals = (mmsid, line, str(mmslist[line]))
+                       c.execute("insert into mms_headers (mms_id, header, value) VALUES (?, ?, ?)", vals)
+                       conn.commit()
+               attachpaths = basedir + "/"
+               #print attachpaths
+               # insert the attachments
+               for line in attachments:
+                       print line
+                       filetype = gnomevfs.get_mime_type(attachpaths + line)
+                       (fname, ext) = os.path.splitext(line)
+                       hidden = 0
+                       # These files should be "hidden" from the user
+                       if ext.startswith(".smil") or filetype == "application/smil":
+                               hidden = 1
+                       vals = (mmsid, line, hidden)
+                       c.execute("insert into attachments (mmsidattach, file, hidden) VALUES (?, ?, ?)", vals)
+                       conn.commit()
+                       #print "inserted", vals
+                       
+               return mmsid
+
+       def get_mms_attachments(self, transactionid, allFiles=False):
+               c = self.conn.cursor()
+               mmsid = self.get_mmsid_from_transactionid(transactionid)
+               if mmsid != None:
+                       if allFiles == True:
+                               c.execute("select * from attachments where mmsidattach == ?", (mmsid,))
+                       else:
+                               c.execute("select * from attachments where mmsidattach == ? and hidden == 0", (mmsid,))
+                       filelist = []
+                       for line in c:
+                               filelist.append(line['file'])
+
+                       return filelist
+
+
+       def get_mms_headers(self, transactionid):
+               c = self.conn.cursor()
+               mmsid = self.get_mmsid_from_transactionid(transactionid)
+               retlist = {}
+               
+               c.execute("select * from mms WHERE id = ? LIMIT 1;", (mmsid,))
+                               
+               for line in c:
+                       retlist['Transaction-Id'] = line['transactionid']
+                       retlist['Time'] = line['msg_time']
+
+               if mmsid != None:
+                       c.execute("select * from mms_headers WHERE mms_id = ?;", (mmsid, ))
+                       for line in c:
+                               hdr = line['header']
+                               val = line['value']
+                               retlist[hdr] = val
+               return retlist
+
+       def get_mmsid_from_transactionid(self, transactionid):
+               c = self.conn.cursor()
+               c.execute("select * from mms where transactionid == ?", (transactionid, ))
+               res = c.fetchone()
+               try:
+                       mmsid = res['id']
+                       return mmsid
+               except:
+                       return None
+       
+       def get_direction_mms(self, transid):
+               c = self.conn.cursor()
+               c.execute("select direction from mms where transactionid = ?", (transid, ))
+               res = c.fetchone()
+               try:
+                       direction = res['direction']
+                       return direction
+               except:
+                       return None
+       
+       
+       def get_pushid_from_transactionid(self, transactionid):
+               c = self.conn.cursor()
+               c.execute("select * from push where transactionid == ?", (transactionid, ))
+               res = c.fetchone()
+               try:
+                       mmsid = res['idpush']
+                       return mmsid
+               except:
+                       return None
+       
+       def delete_mms_message(self, transactionid):
+               c = self.conn.cursor()
+               mmsid = self.get_mmsid_from_transactionid(transactionid)
+               if mmsid != None:
+                       c.execute("delete from mms where id == ?", (mmsid,))
+                       c.execute("delete from attachments where mmsidattach == ?", (mmsid,))
+                       c.execute("delete from mms_headers where mms_id == ?", (mmsid,))
+                       self.conn.commit()
+                       
+       def delete_push_message(self, transactionid):
+               c = self.conn.cursor()
+               pushid = self.get_pushid_from_transactionid(transactionid)
+               if pushid != None:
+                       c.execute("delete from push where idpush == ?", (pushid,))
+                       c.execute("delete from push_headers where push_id == ?", (pushid,))
+                       self.conn.commit()      
+
+
+if __name__ == '__main__':
+       db = DatabaseHandler()
+       c = db.conn.cursor()
+       #print db.get_push_list()
+       #print db.get_mms_headers("1tid46730354431_2zzhez")
\ No newline at end of file
diff --git a/src/fmms.png b/src/fmms.png
new file mode 100644 (file)
index 0000000..2c1ebf9
Binary files /dev/null and b/src/fmms.png differ
diff --git a/src/fmms_config.py b/src/fmms_config.py
new file mode 100644 (file)
index 0000000..2a966d7
--- /dev/null
@@ -0,0 +1,196 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+""" Sets up the config for fMMS
+
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU GPL
+"""
+import os
+
+try:
+       import gnome.gconf as gconf
+except:
+       import gconf
+
+
+class fMMS_config:
+
+
+       def __init__(self):
+               self._fmmsdir = "/apps/fmms/"
+               self.client = gconf.client_get_default()
+               self.client.add_dir(self._fmmsdir, gconf.CLIENT_PRELOAD_NONE)
+               if self.get_apn() == None:
+                       self.set_apn("bogus")
+               if self.get_pushdir() == None:
+                       self.set_pushdir("/home/user/.fmms/push/")
+               if self.get_mmsdir() == None:
+                       self.set_mmsdir("/home/user/.fmms/mms/")
+               if self.get_outdir() == None:
+                       self.set_outdir("/home/user/.fmms/sent/")
+               if self.get_imgdir() == None:
+                       self.set_imgdir("/home/user/.fmms/temp/")
+               if self.get_mmsc() == None:
+                       self.set_mmsc("http://")
+               if self.get_phonenumber() == None:
+                       self.set_phonenumber("000")
+               if self.get_img_resize_width() == None:
+                       self.set_img_resize_width(0)
+               if self.get_version() == None:
+                       self.set_version("Unknown")
+               if self.get_db_path() == None:
+                       self.set_db_path("/home/user/.fmms/mms.db")
+               # Create dirs, for good measures
+               if not os.path.isdir(self.get_pushdir()):
+                       os.makedirs(self.get_pushdir())
+               
+               if not os.path.isdir(self.get_mmsdir()):
+                       os.makedirs(self.get_mmsdir())
+
+               if not os.path.isdir(self.get_outdir()):
+                       os.makedirs(self.get_outdir())
+                       
+               if not os.path.isdir(self.get_imgdir()):
+                       os.makedirs(self.get_imgdir())
+               
+       def read_config(self):
+               pass
+       
+       def set_db_path(self, path):
+               self.client.set_string(self._fmmsdir + "db", path)
+       
+       def get_db_path(self):
+               return self.client.get_string(self._fmmsdir + "db")
+       
+       def get_version(self):
+               return self.client.get_string(self._fmmsdir + "version")
+       
+       def set_version(self, val):
+               self.client.set_string(self._fmmsdir + "version", val)
+               
+       def set_firstlaunch(self, val):
+               self.client.set_int(self._fmmsdir + "firstlaunch", val)
+               
+       def get_firstlaunch(self):
+               return self.client.get_int(self._fmmsdir + "firstlaunch")
+       
+        def set_img_resize_width(self, width):
+               try:
+                       width = int(width)
+               except ValueError:
+                       width = 0
+                self.client.set_int(self._fmmsdir + "img_resize_width", width)
+
+        def get_img_resize_width(self):
+                return self.client.get_int(self._fmmsdir + "img_resize_width")         
+       
+       def set_phonenumber(self, number):
+               self.client.set_string(self._fmmsdir + "phonenumber", number)
+       
+       def get_phonenumber(self):
+               return self.client.get_string(self._fmmsdir + "phonenumber")
+       
+       def set_pushdir(self, path):
+               self.client.set_string(self._fmmsdir + "pushdir", path)
+               
+       def get_pushdir(self):
+               return self.client.get_string(self._fmmsdir + "pushdir")                
+               
+       def set_mmsdir(self, path):
+               self.client.set_string(self._fmmsdir + "mmsdir", path)
+
+       def get_mmsdir(self):
+               return self.client.get_string(self._fmmsdir + "mmsdir")
+       
+       def set_outdir(self, path):
+               self.client.set_string(self._fmmsdir + "outdir", path)
+       
+       def get_outdir(self):
+               return self.client.get_string(self._fmmsdir + "outdir")
+               
+       def set_imgdir(self, path):
+               self.client.set_string(self._fmmsdir + "imgdir", path)
+
+       def get_imgdir(self):
+               return self.client.get_string(self._fmmsdir + "imgdir")
+               
+       """ note this takes the *id* from gconf and not the *display name* """
+       def set_apn(self, apn):
+               #apn = apn.replace(" ", "@32@")
+               #self.client.set_string(self._fmmsdir + "apn_nicename", apn)
+               self.client.set_string(self._fmmsdir + "apn", apn)
+               
+       def get_apn_nicename(self):
+               #return self.client.get_string(self._fmmsdir + "apn_nicename")
+               apn = self.client.get_string(self._fmmsdir + "apn")
+               return self.client.get_string('/system/osso/connectivity/IAP/' + apn + '/name')
+       
+       def get_apn(self):
+               return self.client.get_string(self._fmmsdir + "apn")
+               
+       def set_mmsc(self, mmsc):
+               self.client.set_string(self._fmmsdir + "mmsc", mmsc)
+       
+       def get_mmsc(self):
+               return self.client.get_string(self._fmmsdir + "mmsc")
+               
+       def get_proxy_from_apn(self):
+               apn = self.get_apn()
+               proxy = self.client.get_string('/system/osso/connectivity/IAP/' + apn + '/proxy_http')
+               proxyport = self.client.get_int('/system/osso/connectivity/IAP/' + apn + '/proxy_http_port')
+               return proxy, proxyport
+       
+       def get_gprs_apns(self):
+               # get all IAP's
+               dirs = self.client.all_dirs('/system/osso/connectivity/IAP')
+               apnlist = []
+               for subdir in dirs:
+                       # get all sub entries.. this might be costy?
+                       all_entries = self.client.all_entries(subdir)
+                       # this is a big loop as well, possible to make it easier?
+                       # make this faster
+                       for entry in all_entries:
+                               (path, sep, shortname) = entry.key.rpartition('/')
+                               # this SHOULD always be a int
+                               if shortname == 'type' and entry.value.type == gconf.VALUE_STRING and entry.value.get_string() == "GPRS":       
+                                       # split it so we can get the id
+                                       #(spath, sep, apnid) = path.rpartition('/')
+                                       apname = self.client.get_string(path + '/name')
+                                       apnlist.append(apname)
+               
+               return apnlist
+       
+       """ get the gconf alias for the name, be it the real name or
+       an arbitrary string """
+       def get_apnid_from_name(self, apnname):
+               # get all IAP's
+               dirs = self.client.all_dirs('/system/osso/connectivity/IAP')
+               
+               for subdir in dirs:
+                       # get all sub entries.. this might be costy?
+                       all_entries = self.client.all_entries(subdir)
+                       # this is a big loop as well, possible to make it easier?
+                       for entry in all_entries:
+                               (path, sep, shortname) = entry.key.rpartition('/')
+                               
+                               # this SHOULD always be a string
+                               if shortname == 'name':                         
+                                       if entry.value.type == gconf.VALUE_STRING:
+                                               _value = entry.value.get_string()
+                                       if _value == apnname:
+                                               # split it so we can get the id
+                                               (spath, sep, apnid) = path.rpartition('/')              
+                                               return apnid            
+               
+               return None
+               
+               
+if __name__ == '__main__':
+       config = fMMS_config()
+       #config.get_apnid_from_name("Tele2 MMS")
+       #config.set_apn("bogus")
+       #config.set_pushdir("/home/user/.fmms/push/")
+       #config.set_mmsdir("/home/user/.fmms/mms/")
+       #config.set_mmsc("http://bogus")
+       #print config.get_apn()
+       #print config.get_apn_nicename()
\ No newline at end of file
diff --git a/src/fmms_gui.py b/src/fmms_gui.py
new file mode 100644 (file)
index 0000000..690c788
--- /dev/null
@@ -0,0 +1,473 @@
+#!/usr/bin/env python2.5
+# -*- coding: utf-8 -*-
+""" Main-view UI for fMMS
+
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU GPL
+"""
+import os
+import time
+
+import gtk
+import hildon
+import osso
+import gobject
+import dbus
+from gnome import gnomevfs
+
+from wappushhandler import PushHandler
+import fmms_config as fMMSconf
+import fmms_sender_ui as fMMSSenderUI
+import fmms_viewer as fMMSViewer
+import controller as fMMSController
+import contacts as ContactH
+
+class fMMS_GUI(hildon.Program):
+
+       def __init__(self):
+               self.cont = fMMSController.fMMS_controller()
+               self.config = fMMSconf.fMMS_config()
+               self._mmsdir = self.config.get_mmsdir()
+               self._pushdir = self.config.get_pushdir()
+               self.ch = ContactH.ContactHandler()
+               self.osso_c = osso.Context("fMMS", "0.1.0", False)
+       
+               if not os.path.isdir(self._mmsdir):
+                       print "creating dir", self._mmsdir
+                       os.makedirs(self._mmsdir)
+               if not os.path.isdir(self._pushdir):
+                       print "creating dir", self._pushdir
+                       os.makedirs(self._pushdir)
+       
+               hildon.Program.__init__(self)
+               program = hildon.Program.get_instance()
+               
+               self.osso_rpc = osso.Rpc(self.osso_c)
+               self.osso_rpc.set_rpc_callback("se.frals.fmms","/se/frals/fmms","se.frals.fmms", self.cb_open_fmms, self.osso_c)
+               
+               self.window = hildon.StackableWindow()
+               self.window.set_title("fMMS")
+               program.add_window(self.window)
+               
+               self.window.connect("delete_event", self.quit)
+               
+               pan = hildon.PannableArea()
+               pan.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
+               
+               
+               ### TODO: dont hardcode the values here.. oh well
+               iconcell = gtk.CellRendererPixbuf()
+               photocell = gtk.CellRendererPixbuf()
+               textcell = gtk.CellRendererText()
+               iconcell.set_fixed_size(48, 64)
+               cell2 = gtk.CellRendererText()
+               cell2.set_property('xalign', 1.0)
+               photocell.set_property('xalign', 1.0)
+               photocell.set_fixed_size(64, 64)
+               textcell.set_property('mode', gtk.CELL_RENDERER_MODE_INERT)
+               textcell.set_fixed_size(650, 64)
+               textcell.set_property('xalign', 0.0)
+               
+               self.liststore = gtk.ListStore(gtk.gdk.Pixbuf, str, gtk.gdk.Pixbuf, str)
+               self.treeview = hildon.GtkTreeView(gtk.HILDON_UI_MODE_EDIT)
+               self.treeview.set_model(self.liststore)
+
+               
+               icon_col = gtk.TreeViewColumn('Icon')
+               sender_col = gtk.TreeViewColumn('Sender')
+               placeholder_col = gtk.TreeViewColumn('Photo')
+               
+               
+               self.add_buttons_liststore()
+               
+               self.treeview.append_column(icon_col)
+               self.treeview.append_column(sender_col)
+               self.treeview.append_column(placeholder_col)
+               
+               icon_col.pack_start(iconcell, False)
+               icon_col.set_attributes(iconcell, pixbuf=0)
+               sender_col.pack_start(textcell, True)
+               sender_col.set_attributes(textcell, markup=1)
+               placeholder_col.pack_end(photocell, False)
+               placeholder_col.set_attributes(photocell, pixbuf=2)
+               
+               selection = self.treeview.get_selection()
+               #selection.set_mode(gtk.SELECTION_SINGLE)
+               self.treeview.connect('hildon-row-tapped', self.show_mms)
+               
+               
+               self.liststore_menu = self.liststore_mms_menu()
+               self.treeview.tap_and_hold_setup(self.liststore_menu)
+               #treeview.connect('tap-and-hold', self.liststore_mms_clicked)
+               
+       
+               pan.add_with_viewport(self.treeview)
+               self.window.add(pan)
+       
+               self.menu = self.create_menu()
+               self.window.set_app_menu(self.menu)
+               self.window.show_all()
+               self.add_window(self.window)
+               
+               if self.config.get_firstlaunch() == 1:
+                       print "firstlaunch"
+                       note = osso.SystemNote(self.osso_c)
+                       firstlaunchmessage = "NOTE: Currently you have to connect manually to the MMS APN when sending and receiving.\nAlso, only implemented attachment is image."
+                       note.system_note_dialog(firstlaunchmessage , 'notice')
+                       self.create_config_dialog()
+                       self.config.set_firstlaunch(0)
+               
+       def cb_open_fmms(self, interface, method, args, user_data):
+               if method != 'open_mms' and method != 'open_gui':
+                       return
+               if method == 'open_mms':
+                       try:
+                               checkfile = os.path.isfile(self._pushdir + args[0])
+                               if checkfile == True:
+                                       filename = args[0]
+                       except:
+                               return
+                       viewer = fMMSViewer.fMMS_Viewer(filename)
+               elif method == 'open_gui':
+                       print "open_gui called"
+                       self.liststore.clear()
+                       self.add_buttons_liststore()
+                       return
+               
+       def create_menu(self):
+               menu = hildon.AppMenu()
+               
+               send = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+               send.set_label("New MMS")
+               send.connect('clicked', self.menu_button_clicked)
+               
+               config = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+               config.set_label("Configuration")
+               config.connect('clicked', self.menu_button_clicked)
+               
+               about = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+               about.set_label("About")
+               about.connect('clicked', self.menu_button_clicked)
+               
+               menu.append(send)
+               menu.append(config)
+               menu.append(about)
+               
+               menu.show_all()
+               
+               return menu
+               
+       def menu_button_clicked(self, button):
+               buttontext = button.get_label()
+               if buttontext == "Configuration":
+                       ret = self.create_config_dialog()
+               elif buttontext == "New MMS":
+                       ret = fMMSSenderUI.fMMS_GUI(self.window).run()
+               elif buttontext == "About":
+                       ret = self.create_about_dialog()
+               
+       def create_about_dialog(self):
+               dialog = gtk.AboutDialog()                                                 
+               dialog.set_name("fMMS")
+               fmms_logo = gtk.gdk.pixbuf_new_from_file("/opt/fmms/fmms.png")
+               dialog.set_logo(fmms_logo)                                   
+               dialog.set_comments('MMS send and receive support for Fremantle')                      
+               dialog.set_version(self.config.get_version())                                                
+               dialog.set_copyright("By Nick Leppänen Larsson (aka frals)")                    
+               dialog.set_website("http://mms.frals.se/")                                  
+               dialog.connect("response", lambda d, r: d.destroy())                      
+               dialog.show() 
+       def create_config_dialog(self):
+               dialog = gtk.Dialog()
+               dialog.set_title("Configuration")
+               
+               allVBox = gtk.VBox()
+               
+               self.active_apn_index = 0
+               
+               apnHBox = gtk.HBox()
+               apn_label = gtk.Label("APN:")
+               self.selector = self.create_apn_selector()
+               self.apn = hildon.PickerButton(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
+               self.apn.set_selector(self.selector)
+               self.apn.set_active(self.active_apn_index)
+
+               apnHBox.pack_start(apn_label, False, True, 0)
+               apnHBox.pack_start(self.apn, True, True, 0)
+               
+               mmscHBox = gtk.HBox()
+               mmsc_label = gtk.Label("MMSC:")
+               self.mmsc = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+               mmsc_text = self.config.get_mmsc()
+               if mmsc_text != None:   
+                       self.mmsc.set_text(mmsc_text)
+               else:
+                       self.mmsc.set_text("http://")
+               mmscHBox.pack_start(mmsc_label, False, True, 0)
+               mmscHBox.pack_start(self.mmsc, True, True, 0)
+               
+               numberHBox = gtk.HBox()
+               number_label = gtk.Label("Your phonenumber:")
+               self.number = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+               number_text = self.config.get_phonenumber()
+               if number_text != None:
+                       self.number.set_text(number_text)
+               else:
+                       self.number.set_text("")
+               numberHBox.pack_start(number_label, False, True, 0)
+               numberHBox.pack_start(self.number, True, True, 0)
+               
+               imgwidthHBox = gtk.HBox()
+               imgwidth_label = gtk.Label("Resize image width:")
+               self.imgwidth = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+               imgwidth_text = self.config.get_img_resize_width()
+               if imgwidth_text != None:
+                       self.imgwidth.set_text(str(imgwidth_text))
+               else:
+                       self.imgwidth.set_text("")
+               imgwidthHBox.pack_start(imgwidth_label, False, True, 0)
+               imgwidthHBox.pack_start(self.imgwidth, True, True, 0)
+               
+               notelabel = gtk.Label("APN refers to the name of the connection in\n \"Internet Connections\" to use.")
+               
+               allVBox.pack_start(notelabel, False, True, 0)
+               allVBox.pack_start(apnHBox, False, False, 0)
+               allVBox.pack_start(mmscHBox, False, False, 0)
+               allVBox.pack_end(numberHBox, False, False, 0)
+               allVBox.pack_end(imgwidthHBox, False, False, 0)
+               
+               allVBox.show_all()
+               dialog.vbox.add(allVBox)
+               dialog.add_button("Save", gtk.RESPONSE_APPLY)
+               while 1:
+                       ret = dialog.run()
+                       ret2 = self.config_menu_button_clicked(ret)
+                       if ret2 == 0 or ret2 == None: 
+                               break
+                       
+               dialog.destroy()
+               return ret
+
+
+       """ selector for apn """
+       def create_apn_selector(self):
+               selector = hildon.TouchSelector(text = True)
+               apnlist = self.config.get_gprs_apns()
+               currval = self.config.get_apn_nicename()
+               # Populate selector
+               i = 0
+               for apn in apnlist:
+                       if apn != None:
+                               if apn == currval:
+                                       self.active_apn_index = i
+                               i += 1  
+                               # Add item to the column 
+                               selector.append_text(apn)
+                       
+               selector.center_on_selected()
+               selector.set_active(0, i)
+               # Set selection mode to allow multiple selection
+               selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
+               return selector
+               
+               
+       def config_menu_button_clicked(self, action):
+               if action == gtk.RESPONSE_APPLY:
+                       print self.apn.get_selector().get_current_text()
+                       ret_setapn = self.config.get_apnid_from_name(self.apn.get_selector().get_current_text())
+                       if ret_setapn != None:
+                               self.config.set_apn(ret_setapn)
+                               print "Set apn to: %s" % ret_setapn
+                               ret = self.config.set_mmsc(self.mmsc.get_text())
+                               print "Set mmsc to %s" % self.mmsc.get_text()
+                               self.config.set_phonenumber(self.number.get_text())
+                               print "Set phonenumber to %s" % self.number.get_text()
+                               self.config.set_img_resize_width(self.imgwidth.get_text())
+                               print "Set image width to %s" % self.imgwidth.get_text()
+                               banner = hildon.hildon_banner_show_information(self.window, "", "Settings saved")
+                               return 0
+                       else:
+                               print "Set mmsc to %s" % self.mmsc.get_text()
+                               self.config.set_phonenumber(self.number.get_text())
+                               print "Set phonenumber to %s" % self.number.get_text()
+                               self.config.set_img_resize_width(self.imgwidth.get_text())
+                               print "Set image width to %s" % self.imgwidth.get_text()
+                               banner = hildon.hildon_banner_show_information(self.window, "", "Could not save APN settings. Did you enter a correct APN?")
+                               banner.set_timeout(5000)
+                               return -1
+               
+
+       """ add each item to our liststore """
+       def add_buttons_liststore(self):
+                       icon_theme = gtk.icon_theme_get_default()
+                       
+                       pushlist = self.cont.get_push_list()
+                       for varlist in pushlist:
+                               mtime = varlist['Time']
+                               fname = varlist['Transaction-Id']
+                               direction = self.cont.get_direction_mms(fname)
+                               try:
+                                       sender = varlist['From']
+                                       sender = sender.replace("/TYPE=PLMN", "")
+                               except:
+                                       sender = "0000000"
+                               
+                               if direction == fMMSController.MSG_DIRECTION_OUT:
+                                       sender = "You (Outgoing)"
+                               
+                               sendername = self.ch.get_name_from_number(sender)
+                               photo = icon_theme.load_icon("general_default_avatar", 48, 0)
+                               if sendername != None:
+                                       sender = sendername + ' <span size="smaller">(' + sender + ')</span>'
+                                       phototest = self.ch.get_photo_from_name(sendername, 64)
+                                       if phototest != None:   
+                                               photo = phototest
+                                               #print "loaded photo:", photo.get_width(), photo.get_height()
+       
+                               #title = sender + " - " + mtime
+                               
+                               if self.cont.is_fetched_push_by_transid(fname):
+                                       icon = icon_theme.load_icon("general_sms", 48, 0)
+                               else:
+                                       icon = icon_theme.load_icon("chat_unread_sms", 48, 0)
+                               self.liststore.append([icon, sender + '  <span foreground="#666666" size="smaller"><sup>' + mtime + '</sup></span>\n<span foreground="#666666" size="x-small">' + fname + '</span>', photo, fname])
+       
+       """ lets call it quits! """
+       def quit(self, *args):
+               gtk.main_quit()
+       
+       
+       """ forces ui update, kinda... god this is AWESOME """
+       def force_ui_update(self):
+               while gtk.events_pending():
+                       gtk.main_iteration(False)
+               
+               
+       """ delete push message """
+       def delete_push(self, fname):
+               self.cont.delete_push_message(fname)
+               
+       
+       """ delete mms message (eg for redownload) """
+       def delete_mms(self, fname):
+               self.cont.delete_mms_message(fname)
+       
+       """ delete push & mms """
+       def delete_push_mms(self, fname):
+               try:
+                       self.cont.wipe_message(fname)
+                       banner = hildon.hildon_banner_show_information(self.window, "", "fMMS: Message deleted")
+               except Exception, e:
+                       print "Exception caught:"
+                       print type(e), e
+                       raise
+                       banner = hildon.hildon_banner_show_information(self.window, "", "fMMS: Failed to delete message.")
+
+
+       """ action on delete contextmenu click """
+       def liststore_delete_clicked(self, widget):
+               dialog = gtk.Dialog()
+               dialog.set_title("Confirm")
+               dialog.add_button(gtk.STOCK_YES, 1)
+               dialog.add_button(gtk.STOCK_NO, 0)
+               label = gtk.Label("Are you sure you want to delete the message?")
+               dialog.vbox.add(label)
+               dialog.show_all()
+               ret = dialog.run()
+               if ret == 1:
+                       (model, miter) = self.treeview.get_selection().get_selected()
+                       # the 4th value is the filename (start counting at 0)
+                       filename = model.get_value(miter, 3)
+                       print "deleting", filename
+                       self.delete_push_mms(filename)
+                       self.liststore.remove(miter)
+               dialog.destroy()
+               return
+       
+       """ action on redl contextmenu click """
+       def liststore_redl_clicked(self, widget):
+               hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
+               dialog = gtk.Dialog()
+               dialog.set_title("WARNING")
+               dialog.add_button(gtk.STOCK_YES, 1)
+               dialog.add_button(gtk.STOCK_NO, 0)
+               label = gtk.Label("If the message is no longer on your MMSC,\n the message will be lost. Continue?")
+               dialog.vbox.add(label)
+               dialog.show_all()
+               ret = dialog.run()
+               dialog.destroy()
+               self.force_ui_update()
+               
+               if ret == 1:
+                       (model, miter) = self.treeview.get_selection().get_selected()
+                       # the 4th value is the filename (start counting at 0)
+                       filename = model.get_value(miter, 3)
+                       print "redownloading", filename
+                       try:
+                               self.delete_mms(filename)
+                               banner = hildon.hildon_banner_show_information(self.window, "", "fMMS: Trying to download MMS...")
+                               self.force_ui_update()
+                               
+                               # TODO: FIXME
+                               
+                               self.cont.get_mms_from_push(filename)
+                               self.show_mms(self.treeview, model.get_path(miter))
+                       except Exception, e:
+                               print type(e), e
+                               #raise
+                               banner = hildon.hildon_banner_show_information(self.window, "", "fMMS: Operation failed")
+                       hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
+               return
+
+       """ long press on image creates this """
+       def liststore_mms_menu(self):
+               menu = gtk.Menu()
+               menu.set_title("hildon-context-sensitive-menu")
+
+               redlItem = gtk.MenuItem("Redownload")
+               menu.append(redlItem)
+               redlItem.connect("activate", self.liststore_redl_clicked)
+               redlItem.show()
+               
+               separator = gtk.MenuItem()
+               menu.append(separator)
+               separator.show()
+               
+               openItem = gtk.MenuItem("Delete")
+               menu.append(openItem)
+               openItem.connect("activate", self.liststore_delete_clicked)
+               openItem.show()
+               
+               menu.show_all()
+               return menu
+
+       """ show the selected mms """           
+       def show_mms(self, treeview, path):
+               # Show loading indicator
+               hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
+               banner = hildon.hildon_banner_show_information(self.window, "", "fMMS: Opening message")
+               self.force_ui_update()
+               
+               print path
+               model = treeview.get_model()
+               miter = model.get_iter(path)
+               # the 4th value is the transactionid (start counting at 0)
+               transactionid = model.get_value(miter, 3)
+               
+               try:
+                       viewer = fMMSViewer.fMMS_Viewer(transactionid)
+               except Exception, e:
+                       print type(e), e
+                       #raise
+               
+               
+               hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
+
+       def run(self):
+               self.window.show_all()
+               gtk.main()
+               
+if __name__ == "__main__":
+       app = fMMS_GUI()
+       app.run()
\ No newline at end of file
diff --git a/src/fmms_sender_ui.py b/src/fmms_sender_ui.py
new file mode 100644 (file)
index 0000000..83ca048
--- /dev/null
@@ -0,0 +1,332 @@
+#!/usr/bin/env python2.5
+# -*- coding: utf-8 -*-
+""" Sender UI for fMMS
+
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU GPL
+"""
+import os
+import time
+import socket
+import re
+import Image
+import mimetypes
+
+import gtk
+import hildon
+import gobject
+import osso
+import dbus
+
+from wappushhandler import MMSSender
+import fmms_config as fMMSconf
+import contacts as ContactH
+
+
+
+class fMMS_GUI(hildon.Program):
+       def __init__(self, spawner=None):
+               hildon.Program.__init__(self)
+               program = hildon.Program.get_instance()
+               
+               self.config = fMMSconf.fMMS_config()
+               self.ch = ContactH.ContactHandler()
+               
+               self.window = hildon.StackableWindow()
+               self.window.set_title("fMMS - New MMS")
+               program.add_window(self.window)
+               
+               self.window.connect("delete_event", self.quit)
+               
+               if spawner != None:
+                       self.spawner = spawner
+               else:
+                       self.spawner = self.window
+               allBox = gtk.VBox()
+               
+               """ Begin top section """
+               topHBox1 = gtk.HBox()
+               
+               bTo = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_HORIZONTAL, "     To     ")
+               bTo.connect('clicked', self.open_contacts_dialog)
+               self.eNumber = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+               
+               #topHBox.add(bTo)
+               #topHBox.add(eNumber)
+               topHBox1.pack_start(bTo, False, True, 0)
+               topHBox1.pack_start(self.eNumber, True, True, 0)
+               
+               
+               """ Begin midsection """
+               pan = hildon.PannableArea()
+               pan.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)         
+               
+               #midHBox = gtk.HBox()
+               self.tvMessage = hildon.TextView()
+               self.tvMessage.set_wrap_mode(gtk.WRAP_WORD)
+               
+               #midHBox.pack_start(self.tvMessage, True, True, 0)
+               pan.add_with_viewport(self.tvMessage)
+               
+               """ Begin botsection """
+               
+               botHBox = gtk.HBox()
+               #self.bAttachment = gtk.FileChooserButton('')
+               #self.bAttachment.connect('file-set', self.update_size)
+               self.bAttachment = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_HORIZONTAL, "Attachment")
+               self.bAttachment.connect('clicked', self.open_file_dialog)
+               
+               self.lSize = gtk.Label('')
+               
+               self.bSend = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_HORIZONTAL, "    Send    ")
+               self.bSend.connect('clicked', self.send_mms)
+               
+               botHBox.pack_start(self.bAttachment)
+               botHBox.pack_start(self.lSize)
+               botHBox.pack_end(self.bSend, False, False, 5)
+               
+
+               """ Show it all! """
+               allBox.pack_start(topHBox1, False, False)
+               #allBox.pack_start(topHBox2, False, False)
+               #allBox.pack_start(midHBox, True, True)
+               allBox.pack_start(pan, True, True)
+               allBox.pack_start(botHBox, False, False)
+               
+               #self.pan = pan
+               #self.pan.add_with_viewport(allBox)
+               #self.window.add(self.pan)
+               self.window.add(allBox)
+               self.window.show_all()
+               self.add_window(self.window)
+       
+       # TODO: pass reference instead of making it available in the object?
+       def open_contacts_dialog(self, button):
+               selector = self.create_contacts_selector()
+               self.contacts_dialog = gtk.Dialog("Select a contact")
+
+               # TODO: remove hardcoded height
+               self.contacts_dialog.set_default_size(-1, 320)
+                                                           
+               self.contacts_dialog.vbox.pack_start(selector)
+               self.contacts_dialog.add_button("Done", 1)
+               self.contacts_dialog.show_all()
+               while 1:
+                       ret = self.contacts_dialog.run()
+                       if ret == 1:
+                               ret2 = self.contact_selector_changed(selector)
+                               if ret2 == 0:
+                                       break
+                       else:
+                               break
+               self.contacts_dialog.destroy()
+
+       """ forces ui update, kinda... god this is AWESOME """
+       def force_ui_update(self):
+               while gtk.events_pending():
+                       gtk.main_iteration(False)
+       
+       def contact_number_chosen(self, button, nrdialog):
+               print button.get_label()
+               nr = button.get_label().replace(" ", "")
+               nr = re.sub("[^0-9]\+", "", nr)
+               self.eNumber.set_text(nr)
+               nrdialog.response(0)
+               self.contacts_dialog.response(0)
+               
+       def contact_selector_changed(self, selector):
+               username = selector.get_current_text()
+               nrlist = self.ch.get_numbers_from_name(username)
+               print nrlist
+               nrdialog = gtk.Dialog("Pick a number")
+               for number in nrlist:
+                       print number
+                       numberbox = gtk.HBox()
+                       typelabel = gtk.Label(nrlist[number].capitalize())
+                       typelabel.set_width_chars(24)
+                       button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_HORIZONTAL)
+                       button.set_label(number)
+                       button.connect('clicked', self.contact_number_chosen, nrdialog)
+                       numberbox.pack_start(typelabel, False, False, 0)
+                       numberbox.pack_start(button, True, True, 0)
+                       nrdialog.vbox.pack_start(numberbox)
+               nrdialog.show_all()
+               # this is blocking until we get a return
+               ret = nrdialog.run()
+               print "changed ret:", ret
+               nrdialog.destroy()
+               return ret
+       
+       def create_contacts_selector(self):
+               #Create a HildonTouchSelector with a single text column
+               selector = hildon.TouchSelectorEntry(text = True)
+               #selector.connect('changed', self.contact_selector_changed)
+
+               cl = self.ch.get_contacts_as_list()
+
+               # Populate selector
+               for contact in cl:
+                       if contact != None:
+                               # Add item to the column 
+                               #print "adding", contact
+                               selector.append_text(contact)
+
+               # Set selection mode to allow multiple selection
+               selector.set_column_selection_mode(hildon.TOUCH_SELECTOR_SELECTION_MODE_SINGLE)
+               return selector
+
+               
+       def open_file_dialog(self, button):
+               #fsm = hildon.FileSystemModel()
+               #fcd = hildon.FileChooserDialog(self.window, gtk.FILE_CHOOSER_ACTION_OPEN, fsm)
+               # this shouldnt issue a warning according to the pymaemo mailing list, but does
+               # anyway, nfc why :(
+               fcd = gobject.new(hildon.FileChooserDialog, action=gtk.FILE_CHOOSER_ACTION_OPEN)
+               fcd.set_default_response(gtk.RESPONSE_OK)
+               ret = fcd.run()
+               if ret == gtk.RESPONSE_OK:
+                       ### filesize check
+                       ### TODO: dont hardcode
+                       filesize = os.path.getsize(fcd.get_filename()) / 1024
+                       if filesize > 10240:
+                               banner = hildon.hildon_banner_show_information(self.window, "", "10MB attachment limit in effect, please try another file")
+                               self.bAttachment.set_label("Attachment")
+                       else:
+                               self.bAttachment.set_label(fcd.get_filename())
+                               self.update_size(fcd.get_filename())
+                       fcd.destroy()
+               else:
+                       fcd.destroy()
+       
+       """ resize an image """
+       """ thanks tomaszf for this function """
+       """ slightly modified by frals """
+       def resize_img(self, filename):
+               try:
+                       if not os.path.isdir(self.config.get_imgdir()):
+                               print "creating dir", self.config.get_imgdir()
+                               os.makedirs(self.config.get_imgdir())
+                       
+                       hildon.hildon_banner_show_information(self.window, "", "fMMS: Resizing image, this might take a while...")
+                       self.force_ui_update()
+                       
+                       img = Image.open(filename)
+                       print "height", img.size[1]
+                       print "width", img.size[0]
+                       newWidth = int(self.config.get_img_resize_width())
+                       if img.size[0] > newWidth:
+                               print "resizing"
+                               newWidth = int(self.config.get_img_resize_width())
+                               newHeight = int(newWidth * img.size[1] / img.size[0])
+                               print "Resizing image:", str(newWidth), "*", str(newHeight)
+
+                               # Image.BILINEAR, Image.BICUBIC, Image.ANTIALIASING
+                               rimg = img.resize((newWidth, newHeight), Image.BILINEAR)
+                               filename = filename.rpartition("/")
+                               filename = filename[-1]
+                               rattachment = self.config.get_imgdir() + filename
+                               rimg.save(rattachment)
+                               self.attachmentIsResized = True
+                       else:
+                               print "not resizing"
+                               rattachment = filename
+                               
+                       return rattachment
+               
+               except Exception, e:
+                       print "resizing failed:", e, e.args
+                       raise
+       
+       """ sends the message (no shit?) """
+       def send_mms(self, widget):
+               hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
+               # Disable send-button
+               self.bSend.set_sensitive(False)
+               self.force_ui_update()
+               
+               self.osso_c = osso.Context("fMMS", "0.1.0", False)
+               
+               attachment = self.bAttachment.get_label()
+               if attachment == "Attachment" or attachment == None:
+                       attachment = None
+                       self.attachmentIsResized = False
+               else:
+                       print attachment
+                       filetype = mimetypes.guess_type(attachment)[0]
+                       print self.config.get_img_resize_width()
+                       self.attachmentIsResized = False
+                       print filetype.startswith("image")
+                       if self.config.get_img_resize_width() != 0 and filetype.startswith("image"):
+                               try:
+                                       attachment = self.resize_img(attachment)
+                               except Exception, e:
+                                       print e, e.args
+                                       note = osso.SystemNote(self.osso_c)
+                                       errmsg = str(e.args)
+                                       note.system_note_dialog("Resizing failed:\nError: " + errmsg , 'notice')
+                                       raise
+               
+               to = self.eNumber.get_text()
+               sender = self.config.get_phonenumber()
+               tb = self.tvMessage.get_buffer()
+               message = tb.get_text(tb.get_start_iter(), tb.get_end_iter())
+               print sender, attachment, to, message
+
+               """ Construct and send the message, off you go! """
+               # TODO: remove hardcoded subject
+               try:
+                       sender = MMSSender(to, "MMS", message, attachment, sender)
+                       (status, reason, output) = sender.sendMMS()
+                       ### TODO: Clean up and make this look decent
+                       message = str(status) + "_" + str(reason)
+               
+                       reply = str(output)
+                       #print message
+                       #note = osso.SystemNote(self.osso_c)
+                       #ret = note.system_note_dialog("MMSC REPLIED:" + message + "\nBODY:" + reply, 'notice')
+                       banner = hildon.hildon_banner_show_information(self.window, "", "MMSC REPLIED:" + message + "\nBODY: " + reply)
+                        
+               except TypeError, exc:
+                       print type(exc), exc
+                       note = osso.SystemNote(self.osso_c)
+                       errmsg = "Invalid attachment"
+                       note.system_note_dialog("Sending failed:\nError: " + errmsg + " \nPlease make sure the file is valid" , 'notice')
+                       #raise
+               except socket.error, exc:
+                       print type(exc), exc
+                       code = str(exc.args[0])
+                       text = str(exc.args[1])
+                       note = osso.SystemNote(self.osso_c)
+                       errmsg = code + " " + text
+                       note.system_note_dialog("Sending failed:\nError: " + errmsg + " \nPlease make sure APN settings are correct" , 'notice')
+                       #raise
+               except Exception, exc:
+                       print type(exc)
+                       print exc
+                       raise
+               finally:
+                       hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
+                       self.bSend.set_sensitive(True)
+                       
+               if self.attachmentIsResized == True:
+                       print "Removing temporary image..."
+                       os.remove(attachment)
+               #self.window.destroy()
+               
+       def update_size(self, fname):
+               try:
+                       size = os.path.getsize(fname) / 1024
+                       self.lSize.set_markup("Size:\n<small>" + str(size) + "kB</small>")      
+               except TypeError:
+                       self.lSize.set_markup("")
+
+       def quit(self, *args):
+               gtk.main_quit()
+
+       def run(self):
+               self.window.show_all()
+               gtk.main()
+               
+if __name__ == "__main__":
+       app = fMMS_GUI()
+       app.run()
\ No newline at end of file
diff --git a/src/fmms_viewer.py b/src/fmms_viewer.py
new file mode 100644 (file)
index 0000000..74f13ca
--- /dev/null
@@ -0,0 +1,229 @@
+#!/usr/bin/env python2.5
+# -*- coding: utf-8 -*-
+""" Message-viewer UI for fMMS
+
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU GPL
+"""
+import os
+
+import gtk
+import hildon
+import gobject
+import osso
+from gnome import gnomevfs
+
+from wappushhandler import PushHandler
+import fmms_config as fMMSconf
+import controller as fMMSController
+
+
+class fMMS_Viewer(hildon.Program):
+
+       def __init__(self, fname, standalone=False):
+               self.cont = fMMSController.fMMS_controller()
+               self.standalone = standalone
+               self.config = fMMSconf.fMMS_config()
+               self._mmsdir = self.config.get_mmsdir()
+               self._pushdir = self.config.get_pushdir()
+               self._outdir = self.config.get_outdir()
+               self.osso_c = osso.Context("fMMS", "0.1.0", False)
+               
+               self.window = hildon.StackableWindow()
+               self.window.set_title("Showing MMS: " + fname)
+               self.window.connect("delete_event", self.quit)
+               
+               vbox = gtk.VBox()
+               pan = hildon.PannableArea()
+               pan.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
+
+               self._parse_mms(fname, vbox)
+
+               pan.add_with_viewport(vbox)
+               self.window.add(pan)
+
+               mms_menu = self.create_mms_menu(fname)
+               self.window.set_app_menu(mms_menu)
+               self.window.show_all()
+       
+       """ lets call it quits! """
+       def quit(self, *args):
+               self.window.destroy()
+               if self.standalone == True:
+                       gtk.main_quit()
+       
+       """ forces ui update, kinda... god this is AWESOME """
+       def force_ui_update(self):
+               while gtk.events_pending():
+                       gtk.main_iteration(False)
+                               
+       """ create app menu for mms viewing window """
+       def create_mms_menu(self, fname):
+               menu = hildon.AppMenu()
+               
+               headers = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+               headers.set_label("Headers")
+               headers.connect('clicked', self.mms_menu_button_clicked, fname)
+               
+               menu.append(headers)
+       
+               menu.show_all()
+               
+               return menu             
+       
+       """ actions for mms menu """
+       def mms_menu_button_clicked(self, button, fname):
+               buttontext = button.get_label()
+               if buttontext == "Headers":
+                       ret = self.create_headers_dialog(fname)
+
+       """ show headers in a dialog """
+       def create_headers_dialog(self, fname):
+               dialog = gtk.Dialog()
+               dialog.set_title("Headers")
+               
+               dialogVBox = gtk.VBox()
+               
+               pan = hildon.PannableArea()
+               #pan.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH)
+               pan.set_property("size-request-policy", hildon.SIZE_REQUEST_CHILDREN)
+               
+               allVBox = gtk.VBox()
+               #leftBox = gtk.VBox()
+               #rightBox = gtk.VBox()
+               headerlist = self.cont.get_mms_headers(fname)
+               for line in headerlist:
+                       hbox = gtk.HBox()
+                       titel = gtk.Label(line)
+                       titel.set_alignment(0, 0)
+                       titel.set_width_chars(18)
+                       label = gtk.Label(headerlist[line])
+                       label.set_line_wrap(True)
+                       label.set_alignment(0, 0)
+                       hbox.pack_start(titel, False, False, 0)
+                       hbox.pack_start(label, False, False, 0)
+                       allVBox.pack_start(hbox)
+                       #leftBox.pack_start(titel, False, False, 0)
+                       #rightBox.pack_start(label, False, False, 0)
+                       #pan.add(label)
+                       
+               #allHBox.pack_start(leftBox, False, False, 0)
+               #allHBox.pack_start(rightBox, True, True, 0)
+               allVBox.show_all()
+               
+               pan.add_with_viewport(allVBox)
+               dialog.vbox.add(pan)
+               dialog.vbox.show_all()
+               ret = dialog.run()
+               
+               dialog.destroy()
+               return ret
+       
+       """ parse mms and push each part to the container 
+           fetches the mms if its not downloaded         """
+       def _parse_mms(self, filename, container):
+               hildon.hildon_gtk_window_set_progress_indicator(self.window, 1)
+               self.force_ui_update()
+               
+               if not self.cont.is_fetched_push_by_transid(filename):  
+                       self.cont.get_mms_from_push(filename)
+                               
+               textview = gtk.TextView()
+               textview.set_editable(False)
+               textview.set_cursor_visible(False)
+               textview.set_wrap_mode(gtk.WRAP_WORD)
+               textbuffer = gtk.TextBuffer()
+               direction = self.cont.get_direction_mms(filename)
+               if direction == fMMSController.MSG_DIRECTION_OUT:
+                       path = self._outdir + filename
+               else:
+                       path = self._mmsdir + filename
+               filelist = self.cont.get_mms_attachments(filename)
+               print "filelist:", filelist
+               for fname in filelist:
+                       (name, ext) = os.path.splitext(fname)
+                       fnpath = os.path.join(path, fname)
+                       isText = False
+                       isImage = False
+                       try:
+                               filetype = gnomevfs.get_mime_type(fnpath)
+                               print "filetype:", filetype
+                               if filetype != None:
+                                       if filetype.startswith("image") or filetype.startswith("sketch"):
+                                               isImage = True
+                                       if filetype.startswith("text"):
+                                               isText = True
+                       except Exception, e:
+                               filetype = None
+                               print type(e), e
+                       
+                       if isImage or ext == ".wbmp":
+                               """ insert the image in an eventbox so we can get signals """
+                               ebox = gtk.EventBox()
+                               img = gtk.Image()
+                               img.set_from_file(path + "/" + fname)
+                               fullpath = path + "/" + fname
+                               ebox.add(img)
+                               ## TODO: make this menu proper without this ugly
+                               # args passing
+                               menu = self.mms_img_menu(fullpath)
+                               ebox.tap_and_hold_setup(menu)
+                               container.add(ebox)
+                       elif isText or ext.startswith(".txt"):
+                               fp = open(path + "/" + fname, 'r')
+                               contents = fp.read()
+                               fp.close()
+                               #print contents
+                               textbuffer.insert(textbuffer.get_end_iter(), contents)
+                       elif name != "message" and name != "headers" and not ext.startswith(".smil") and filetype != "application/smil":
+                               attachButton = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_HORIZONTAL, fname)
+                               attachButton.connect('clicked', self.mms_img_clicked, fnpath)
+                               container.pack_end(attachButton, False, False, 0)
+                               
+               textview.set_buffer(textbuffer)
+               container.add(textview)
+               hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
+               
+               
+       """ action on click on image/button """
+       def mms_img_clicked(self, widget, data):
+               print widget, data
+               path = "file://" + data
+               # gnomevfs seems to be better than mimetype when guessing mimetype for us
+               file_mimetype = gnomevfs.get_mime_type(path)
+               if file_mimetype != None:
+                       if file_mimetype.startswith("video") or file_mimetype.startswith("audio"):
+                               rpc = osso.Rpc(self.osso_c)
+                               rpc.rpc_run("com.nokia.mediaplayer", "/com/nokia/mediaplayer", "com.nokia.mediaplayer", "mime_open", (str, path))       
+                       elif file_mimetype.startswith("image"):
+                               rpc = osso.Rpc(self.osso_c)
+                               rpc.rpc_run("com.nokia.image_viewer", "/com/nokia/image_viewer", "com.nokia.image_viewer", "mime_open", (str, path))
+               else:
+                       # TODO: how to solve this?
+                       # move .mms to ~/MyDocs? change button to copy file to ~/MyDocs?
+                       #rpc = osso.Rpc(self.osso_c)
+                       #path = os.path.dirname(path).replace("file://", "")
+                       print path
+                       #rpc.rpc_run("com.nokia.osso_filemanager", "/com/nokia/osso_filemanager", "com.nokia.osso_filemanager", "open_folder", (str, path))
+
+
+       """ long press on image creates this """
+       def mms_img_menu(self, data=None):
+               print "menu created"
+               menu = gtk.Menu()
+               menu.set_title("hildon-context-sensitive-menu")
+
+               openItem = gtk.MenuItem("Open")
+               menu.append(openItem)
+               openItem.connect("activate", self.mms_img_clicked, data)
+               openItem.show()
+               menu.show_all()
+               return menu
+
+       def run(self):
+               self.window.show_all()
+               gtk.main()
+               
+if __name__ == "__main__":
+       app = fMMS_Viewer("fname", True)
+       app.run()
\ No newline at end of file
diff --git a/src/fmmsd.py b/src/fmmsd.py
new file mode 100644 (file)
index 0000000..1634b4d
--- /dev/null
@@ -0,0 +1,44 @@
+#!/usr/bin/env python2.5
+# -*- coding: utf-8 -*-
+""" daemon for fMMS
+
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU GPL
+"""
+import dbus
+import gobject
+import dbus.mainloop.glib
+import dbus.service
+
+from wappushhandler import PushHandler
+
+class MMSHandler(dbus.service.Object):
+       def __init__(self):
+               # Here the service name
+               bus_name = dbus.service.BusName('se.frals.mms', bus=dbus.SystemBus())
+               # Here the object path
+               dbus.service.Object.__init__(self, bus_name, '/se/frals/mms')
+
+
+       # TODO: This should filter by bearer and not number of arguments, really, it should.
+       # Here the interface name, and the method is named same as on dbus.
+       """ According to wappushd.h SMS PUSH is one less argument """
+       @dbus.service.method(dbus_interface='com.nokia.WAPPushHandler')
+       def HandleWAPPush(self, bearer, source, srcport, dstport, header, payload):
+               handler = PushHandler()
+               ret = handler._incoming_sms_push(source, srcport, dstport, header, payload)
+               return 0
+
+       """ According to wappushd.h IP PUSH is one more argument 
+       @dbus.service.method(dbus_interface='com.nokia.WAPPushHandler')
+       def HandleWAPPush(self, bearer, source, dest, srcport, dstport, header, payload):
+               handler = PushHandler()
+               ret = handler._incoming_ip_push(source, dest, srcport, dstport, header, payload)
+               return 0
+       """
+
+dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+bus = dbus.SystemBus()
+loop = gobject.MainLoop()
+server = MMSHandler()
+loop.run()
diff --git a/src/mms/COPYING b/src/mms/COPYING
new file mode 100644 (file)
index 0000000..d511905
--- /dev/null
@@ -0,0 +1,339 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/src/mms/COPYING.LESSER b/src/mms/COPYING.LESSER
new file mode 100644 (file)
index 0000000..e8bec28
--- /dev/null
@@ -0,0 +1,165 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE\r
+                       Version 3, 29 June 2007\r
+\r
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\r
+ Everyone is permitted to copy and distribute verbatim copies\r
+ of this license document, but changing it is not allowed.\r
+\r
+\r
+  This version of the GNU Lesser General Public License incorporates\r
+the terms and conditions of version 3 of the GNU General Public\r
+License, supplemented by the additional permissions listed below.\r
+\r
+  0. Additional Definitions.\r
+\r
+  As used herein, "this License" refers to version 3 of the GNU Lesser\r
+General Public License, and the "GNU GPL" refers to version 3 of the GNU\r
+General Public License.\r
+\r
+  "The Library" refers to a covered work governed by this License,\r
+other than an Application or a Combined Work as defined below.\r
+\r
+  An "Application" is any work that makes use of an interface provided\r
+by the Library, but which is not otherwise based on the Library.\r
+Defining a subclass of a class defined by the Library is deemed a mode\r
+of using an interface provided by the Library.\r
+\r
+  A "Combined Work" is a work produced by combining or linking an\r
+Application with the Library.  The particular version of the Library\r
+with which the Combined Work was made is also called the "Linked\r
+Version".\r
+\r
+  The "Minimal Corresponding Source" for a Combined Work means the\r
+Corresponding Source for the Combined Work, excluding any source code\r
+for portions of the Combined Work that, considered in isolation, are\r
+based on the Application, and not on the Linked Version.\r
+\r
+  The "Corresponding Application Code" for a Combined Work means the\r
+object code and/or source code for the Application, including any data\r
+and utility programs needed for reproducing the Combined Work from the\r
+Application, but excluding the System Libraries of the Combined Work.\r
+\r
+  1. Exception to Section 3 of the GNU GPL.\r
+\r
+  You may convey a covered work under sections 3 and 4 of this License\r
+without being bound by section 3 of the GNU GPL.\r
+\r
+  2. Conveying Modified Versions.\r
+\r
+  If you modify a copy of the Library, and, in your modifications, a\r
+facility refers to a function or data to be supplied by an Application\r
+that uses the facility (other than as an argument passed when the\r
+facility is invoked), then you may convey a copy of the modified\r
+version:\r
+\r
+   a) under this License, provided that you make a good faith effort to\r
+   ensure that, in the event an Application does not supply the\r
+   function or data, the facility still operates, and performs\r
+   whatever part of its purpose remains meaningful, or\r
+\r
+   b) under the GNU GPL, with none of the additional permissions of\r
+   this License applicable to that copy.\r
+\r
+  3. Object Code Incorporating Material from Library Header Files.\r
+\r
+  The object code form of an Application may incorporate material from\r
+a header file that is part of the Library.  You may convey such object\r
+code under terms of your choice, provided that, if the incorporated\r
+material is not limited to numerical parameters, data structure\r
+layouts and accessors, or small macros, inline functions and templates\r
+(ten or fewer lines in length), you do both of the following:\r
+\r
+   a) Give prominent notice with each copy of the object code that the\r
+   Library is used in it and that the Library and its use are\r
+   covered by this License.\r
+\r
+   b) Accompany the object code with a copy of the GNU GPL and this license\r
+   document.\r
+\r
+  4. Combined Works.\r
+\r
+  You may convey a Combined Work under terms of your choice that,\r
+taken together, effectively do not restrict modification of the\r
+portions of the Library contained in the Combined Work and reverse\r
+engineering for debugging such modifications, if you also do each of\r
+the following:\r
+\r
+   a) Give prominent notice with each copy of the Combined Work that\r
+   the Library is used in it and that the Library and its use are\r
+   covered by this License.\r
+\r
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license\r
+   document.\r
+\r
+   c) For a Combined Work that displays copyright notices during\r
+   execution, include the copyright notice for the Library among\r
+   these notices, as well as a reference directing the user to the\r
+   copies of the GNU GPL and this license document.\r
+\r
+   d) Do one of the following:\r
+\r
+       0) Convey the Minimal Corresponding Source under the terms of this\r
+       License, and the Corresponding Application Code in a form\r
+       suitable for, and under terms that permit, the user to\r
+       recombine or relink the Application with a modified version of\r
+       the Linked Version to produce a modified Combined Work, in the\r
+       manner specified by section 6 of the GNU GPL for conveying\r
+       Corresponding Source.\r
+\r
+       1) Use a suitable shared library mechanism for linking with the\r
+       Library.  A suitable mechanism is one that (a) uses at run time\r
+       a copy of the Library already present on the user's computer\r
+       system, and (b) will operate properly with a modified version\r
+       of the Library that is interface-compatible with the Linked\r
+       Version.\r
+\r
+   e) Provide Installation Information, but only if you would otherwise\r
+   be required to provide such information under section 6 of the\r
+   GNU GPL, and only to the extent that such information is\r
+   necessary to install and execute a modified version of the\r
+   Combined Work produced by recombining or relinking the\r
+   Application with a modified version of the Linked Version. (If\r
+   you use option 4d0, the Installation Information must accompany\r
+   the Minimal Corresponding Source and Corresponding Application\r
+   Code. If you use option 4d1, you must provide the Installation\r
+   Information in the manner specified by section 6 of the GNU GPL\r
+   for conveying Corresponding Source.)\r
+\r
+  5. Combined Libraries.\r
+\r
+  You may place library facilities that are a work based on the\r
+Library side by side in a single library together with other library\r
+facilities that are not Applications and are not covered by this\r
+License, and convey such a combined library under terms of your\r
+choice, if you do both of the following:\r
+\r
+   a) Accompany the combined library with a copy of the same work based\r
+   on the Library, uncombined with any other library facilities,\r
+   conveyed under the terms of this License.\r
+\r
+   b) Give prominent notice with the combined library that part of it\r
+   is a work based on the Library, and explaining where to find the\r
+   accompanying uncombined form of the same work.\r
+\r
+  6. Revised Versions of the GNU Lesser General Public License.\r
+\r
+  The Free Software Foundation may publish revised and/or new versions\r
+of the GNU Lesser General Public License from time to time. Such new\r
+versions will be similar in spirit to the present version, but may\r
+differ in detail to address new problems or concerns.\r
+\r
+  Each version is given a distinguishing version number. If the\r
+Library as you received it specifies that a certain numbered version\r
+of the GNU Lesser General Public License "or any later version"\r
+applies to it, you have the option of following the terms and\r
+conditions either of that published version or of any later version\r
+published by the Free Software Foundation. If the Library as you\r
+received it does not specify a version number of the GNU Lesser\r
+General Public License, you may choose any version of the GNU Lesser\r
+General Public License ever published by the Free Software Foundation.\r
+\r
+  If the Library as you received it specifies that a proxy can decide\r
+whether future versions of the GNU Lesser General Public License shall\r
+apply, that proxy's public statement of acceptance of any version is\r
+permanent authorization for you to choose that version for the\r
+Library.\r
diff --git a/src/mms/WSP.py b/src/mms/WSP.py
new file mode 100644 (file)
index 0000000..980d190
--- /dev/null
@@ -0,0 +1,337 @@
+# -*- coding: utf-8 -*-
+#\r
+# This library is free software, distributed under the terms of\r
+# the GNU Lesser General Public License Version 2.\r
+# See the COPYING.LESSER file included in this archive\r
+#\r
+# The docstrings in this module contain epytext markup; API documentation\r
+# may be created by processing this file with epydoc: http://epydoc.sf.net\r
+"""\r
+Library for WAP transport, original by Francois Aucamp, modified by Nick Leppänen Larsson\r
+for use in Maemo5/Fremantle on the Nokia N900.\r
+\r
+@author: Francois Aucamp <faucamp@csir.co.za>\r
+@author: Nick Leppänen Larsson <frals@frals.se>\r
+@license: GNU LGPL\r
+"""\r
+from WTP import WTP\r
+import sys\r
+import array\r
+import socket, time\r
+\r
+from wsp_pdu import Decoder, Encoder, WSPEncodingAssignments\r
+from iterator import PreviewIterator\r
+\r
+class WSP:\r
+    """ This class implements a very limited subset of the WSP layer.\r
+    \r
+    It uses python-mms's WSP PDU encoding module for almost all encodings,\r
+    and essentially just glues it together into a limited WSP layer. """\r
+    def __init__(self, wapGatewayHost, wapGatewayPort=9201):\r
+        self.serverSessionID = -1\r
+        self.capabilities = {'ClientSDUSize': 261120,\r
+                             'ServerSDUSize': 261120}\r
+        self.headers = [('User-Agent', 'Nokia N900'),\r
+                        ('Accept', 'text/plain'),\r
+                        ('Accept', 'application/vnd.wap.mms-message')]\r
+        self.wtp = WTP(wapGatewayHost, wapGatewayPort)\r
+\r
+    def connect(self):\r
+        """ Sends a WSP Connect message to the gateway, including any\r
+        configured capabilities. It also updates the WSP object to reflect\r
+        the status of the WSP connection """\r
+        print '>> WSP: Connect'\r
+        response = self.wtp.invoke(self.encodeConnectPDU())\r
+        self._decodePDU(response)\r
+        \r
+\r
+    def disconnect(self):\r
+        """ Sends a WSP Connect message to the gateway, including any\r
+        configured capabilities. It also updates the WSP object to reflect\r
+        the status of the WSP connection """\r
+        print '>> WSP: Disconnect'\r
+        self.wtp.invoke(self.encodeDisconnectPDU(self.serverSessionID))\r
+        self.serverSessionID = -1\r
+        \r
+    def post(self, uri, contentType, data):\r
+        """ Performs a WSP POST """\r
+        if type(data) == array.array:\r
+            data = data.tolist()\r
+        print '>> WSP: Post'\r
+        pdu = self.encodePostPDU(uri, contentType) + data\r
+        response = self.wtp.invoke(pdu)\r
+        self._decodePDU(response)\r
+        \r
+    def get(self, uri):\r
+        """ Performs a WSP GET """\r
+        response = self.wtp.invoke(self.encodeGetPDU(uri))\r
+        self._decodePDU(response)\r
+\r
+    def encodeConnectPDU(self):\r
+        """ Sends a WSP connect request (S-Connect.req, i.e. Connect PDU) to\r
+        the WAP gateway \r
+        \r
+        This PDU is described in WAP-230, section 8.2.2, and is sent to\r
+        initiate the creation of a WSP session. Its field structure::\r
+         \r
+            Field Name       Type               Description\r
+            ===============  =================  =================\r
+            Version          uint8              WSP protocol version\r
+            CapabilitiesLen  uintvar            Length of the Capabilities field\r
+            HeadersLen       uintvar            Length of the Headers field\r
+            Capabilities     <CapabilitiesLen>\r
+                              octets            S-Connect.req::Requested Capabilities\r
+            Headers          <HeadersLen>\r
+                              octets            S-Connect.req::Client Headers\r
+        """\r
+        pdu = []\r
+        pdu.append(0x01) # Type: "Connect"\r
+        # Version field - we are using version 1.0\r
+        pdu.extend(Encoder.encodeVersionValue('1.0'))\r
+        # Add capabilities\r
+        capabilities = []\r
+        for capability in self.capabilities:\r
+            # Unimplemented/broken capabilities are not added\r
+            try:\r
+                exec 'capabilities.extend(WSP._encodeCapabilty%s(self.capabilities[capability]))' % capability\r
+            except:\r
+                pass\r
+        # Add and encode headers\r
+        headers = array.array('B')\r
+        for hdr, hdrValue in self.headers:\r
+            headers.extend(Encoder.encodeHeader(hdr, hdrValue))\r
+        # Add capabilities and headers to PDU (including their lengths)\r
+        pdu.extend(Encoder.encodeUintvar(len(capabilities)))\r
+        pdu.extend(Encoder.encodeUintvar(len(headers)))\r
+        pdu.extend(capabilities)\r
+        pdu.extend(headers)\r
+        return pdu\r
+    \r
+    @staticmethod\r
+    def encodePostPDU(uri, contentType):\r
+        """ Builds a WSP POST PDU\r
+        \r
+        @note: This method does not add the <Data> part at the end of the PDU;\r
+               this should be appended manually to the result of this method.\r
+        \r
+        The WSP Post PDU is defined in WAP-230, section 8.2.3.2::\r
+                                     Table 10. Post Fields\r
+         Name        Type                       Source\r
+         ==========  ========================   ========================================\r
+         UriLen      uintvar                    Length of the URI field\r
+         HeadersLen  uintvar                    Length of the ContentType and Headers fields\r
+                                                combined\r
+         Uri         UriLen octets              S-MethodInvoke.req::Request URI or\r
+                                                S-Unit-MethodInvoke.req::Request URI\r
+         ContentType multiple octets            S-MethodInvoke.req::Request Headers or\r
+                                                S-Unit-MethodInvoke.req::Request Headers\r
+         Headers     (HeadersLen - length of    S-MethodInvoke.req::Request Headers or\r
+                     ContentType) octets        S-Unit-MethodInvoke.req::Request Headers\r
+         Data        multiple octets            S-MethodInvoke.req::Request Body or\r
+                                                S-Unit-MethodInvoke.req::Request Body\r
+\r
+        """\r
+        #TODO: remove this, or make it dynamic or something:\r
+        headers = [('Accept', 'application/vnd.wap.mms-message')]\r
+        pdu = [0x60] # Type: "Post"\r
+        # UriLen:\r
+        pdu.extend(Encoder.encodeUintvar(len(uri)))\r
+        # HeadersLen:\r
+        encodedContentType = Encoder.encodeContentTypeValue(contentType, {})\r
+        encodedHeaders = []\r
+        for hdr, hdrValue in headers:\r
+            encodedHeaders.extend(Encoder.encodeHeader(hdr, hdrValue))\r
+        headersLen = len(encodedContentType) + len(encodedHeaders)\r
+        pdu.extend(Encoder.encodeUintvar(headersLen))\r
+        # URI - this should NOT be null-terminated (according to WAP-230 section 8.2.3.2)\r
+        for char in uri:\r
+            pdu.append(ord(char))\r
+        # Content-Type:\r
+        pdu.extend(encodedContentType)\r
+        # Headers:\r
+        pdu.extend(encodedHeaders)\r
+        return pdu\r
+    \r
+    @staticmethod\r
+    def encodeGetPDU(uri):\r
+        """ Builds a WSP GET PDU \r
+        \r
+        The WSP Get PDU is defined in WAP-230, section 8.2.3.1::\r
+         Name    Type          Source\r
+         ======  ============  =======================\r
+         URILen  uintvar       Length of the URI field\r
+         URI     URILen octets S-MethodInvoke.req::Request URI or\r
+                               S-Unit-MethodInvoke.req::Request URI\r
+         Headers multiple      S-MethodInvoke.req::Request Headers or\r
+                 octets        S-Unit-MethodInvoke.req::Request Headers\r
+        """\r
+        pdu = self\r
+        # UriLen:\r
+        pdu.extend(Encoder.encodeUintvar(len(uri)))\r
+        # URI - this should NOT be null-terminated (according to WAP-230 section 8.2.3.1)\r
+        for char in uri:\r
+            pdu.append(ord(char))\r
+        headers = []\r
+        #TODO: not sure if these should go here...\r
+        for hdr, hdrValue in pdu.headers:\r
+            headers.extend(Encoder.encodeHeader(hdr, hdrValue))\r
+        pdu.extend(headers)\r
+        return pdu\r
+    \r
+    @staticmethod\r
+    def encodeDisconnectPDU(serverSessionID):\r
+        """ Builds a WSP Disconnect PDU\r
+        \r
+        The Disconnect PDU is sent to terminate a session. It structure is\r
+        defined in WAP-230, section 8.2.2.4::\r
+         Name             Type     Source\r
+         ===============  =======  ===================\r
+         ServerSessionId  uintvar  Session_ID variable\r
+        """\r
+        pdu = [0x05] # Type: "Disconnect"\r
+        pdu.extend(Encoder.encodeUintvar(serverSessionID))\r
+        return pdu\r
+    \r
+    def _decodePDU(self, byteIter):\r
+        """ Reads and decodes a WSP PDU from the sequence of bytes starting at\r
+        the byte pointed to by C{dataIter.next()}.\r
+        \r
+        @param byteIter: an iterator over a sequence of bytes\r
+        @type byteIteror: mms.iterator.PreviewIterator\r
+        \r
+        @note: If the PDU type is correctly determined, byteIter will be\r
+               modified in order to read past the amount of bytes required\r
+               by the PDU type.\r
+        """\r
+        pduType = Decoder.decodeUint8(byteIter)\r
+        if pduType not in WSPEncodingAssignments.wspPDUTypes:\r
+            #TODO: maybe raise some error or something\r
+            print 'Error - unknown WSP PDU type: %s' % hex(pduType)\r
+            raise TypeError\r
+        pduType = WSPEncodingAssignments.wspPDUTypes[pduType]\r
+        print '<< WSP: %s' % pduType\r
+        pduValue = None\r
+        try:\r
+            exec 'pduValue = self._decode%sPDU(byteIter)' % pduType\r
+        except:\r
+            print 'A fatal error occurred, probably due to an unimplemented feature.\n'\r
+            raise\r
+        return pduValue\r
+    \r
+    def _decodeConnectReplyPDU(self, byteIter):\r
+        """ The WSP ConnectReply PDU is sent in response to a S-Connect.req\r
+        PDU. It is defined in WAP-230, section 8.2.2.2.\r
+        \r
+        All WSP PDU headers start with a type (uint8) byte (we do not\r
+        implement connectionless WSP, thus we don't prepend TIDs to the WSP\r
+        header). The WSP PDU types are specified in WAP-230, table 34.\r
+        \r
+        ConnectReply PDU Fields::\r
+         Name            Type               Source\r
+         =============== =================  =====================================\r
+         ServerSessionId Uintvar            Session_ID variable\r
+         CapabilitiesLen Uintvar            Length of Capabilities field\r
+         HeadersLen      Uintvar            Length of the Headers field\r
+         Capabilities    <CapabilitiesLen>  S-Connect.res::Negotiated Capabilities\r
+                          octets\r
+         Headers         <HeadersLen>       S-Connect.res::Server Headers\r
+                          octets\r
+\r
+        @param byteIters: an iterator over the sequence of bytes containing\r
+                          the ConnectReply PDU\r
+        @type bytes: mms.iterator.PreviewIterator\r
+        """\r
+        self.serverSessionID = Decoder.decodeUintvar(byteIter)\r
+        capabilitiesLen = Decoder.decodeUintvar(byteIter)\r
+        headersLen = Decoder.decodeUintvar(byteIter)\r
+        # Stub to decode capabilities (currently we ignore these)\r
+        cFieldBytes = []\r
+        for i in range(capabilitiesLen):\r
+            cFieldBytes.append(byteIter.next())\r
+        cIter = PreviewIterator(cFieldBytes)\r
+        # Stub to decode headers (currently we ignore these)\r
+        hdrFieldBytes = []\r
+        for i in range(headersLen):\r
+            hdrFieldBytes.append(byteIter.next())\r
+        hdrIter = PreviewIterator(hdrFieldBytes)\r
+    \r
+    \r
+    def _decodeReplyPDU(self, byteIter):\r
+        """ The WSP Reply PDU is the generic response PDU used to return\r
+        information from the server in response to a request. It is defined in\r
+        WAP-230, section 8.2.3.3.\r
+        \r
+        All WSP PDU headers start with a type (uint8) byte (we do not\r
+        implement connectionless WSP, thus we don't prepend TIDs to the WSP\r
+        header). The WSP PDU types are specified in WAP-230, table 34.\r
+        \r
+        Reply PDU Fields::\r
+         Name            Type\r
+         =============== =================\r
+         Status          Uint8\r
+         HeadersLen      Uintvar\r
+         ContentType     multiple octects\r
+         Headers         <HeadersLen> - len(ContentType) octets\r
+         Data            multiple octects\r
+\r
+        @param byteIters: an iterator over the sequence of bytes containing\r
+                          the ConnectReply PDU\r
+        @type bytes: mms.iterator.PreviewIterator\r
+        """\r
+        status = Decoder.decodeUint8(byteIter)\r
+        headersLen = Decoder.decodeUintvar(byteIter)\r
+        \r
+        # Stub to decode headers (currently we ignore these)\r
+        hdrFieldBytes = []\r
+        for i in range(headersLen):\r
+            hdrFieldBytes.append(byteIter.next())\r
+        hdrIter = PreviewIterator(hdrFieldBytes)\r
+        contentType, parameters = Decoder.decodeContentTypeValue(hdrIter)\r
+        while True:\r
+            try:\r
+                hdr, value = Decoder.decodeHeader(hdrIter)\r
+            except StopIteration:\r
+                break\r
+        # Read the data\r
+        data = []\r
+        while True:\r
+            try:\r
+                data.append(byteIter.next())\r
+            except StopIteration:\r
+                break\r
+    \r
+    @staticmethod\r
+    def _encodeCapabiltyClientSDUSize(size):\r
+        """ Encodes the Client-SDU-Size capability (Client Service Data Unit);\r
+        described in WAP-230, section 8.3.2.1\r
+        \r
+        This defines the maximum size (in octets) of WTP Service Data Units\r
+        \r
+        @param size: The requested SDU size to negotiate (in octets)\r
+        @type size: int\r
+        """\r
+        identifier = Encoder.encodeShortInteger(0x00)\r
+        parameters = Encoder.encodeUintvar(size)\r
+        length = Encoder.encodeUintvar(len(identifier) + len(parameters))\r
+        capability = length\r
+        capability.extend(identifier)\r
+        capability.extend(parameters)\r
+        return capability\r
+     \r
+    @staticmethod\r
+    def _encodeCapabilityServerSDUSize(size):\r
+        """ Encodes the Client-SDU-Size capability (Server Service Data Unit);\r
+        described in WAP-230, section 8.3.2.1.\r
+        \r
+        This defines the maximum size (in octets) of WTP Service Data Units\r
+        \r
+        @param size: The requested SDU size to negotiate (in octets)\r
+        @type size: int\r
+        """\r
+        identifier = Encoder.encodeShortInteger(0x01)\r
+        parameters = Encoder.encodeUintvar(size)\r
+        length = Encoder.encodeUintvar(len(identifier) + len(parameters))\r
+        capability = length\r
+        capability.extend(identifier)\r
+        capability.extend(parameters)\r
+        return capability\r
diff --git a/src/mms/WTP.py b/src/mms/WTP.py
new file mode 100644 (file)
index 0000000..6bd02f2
--- /dev/null
@@ -0,0 +1,360 @@
+# -*- coding: utf-8 -*-
+#
+# This library is free software, distributed under the terms of
+# the GNU Lesser General Public License Version 2.
+# See the COPYING.LESSER 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
+"""
+Library for WAP transport, original by Francois Aucamp, modified by Nick Leppänen Larsson
+for standalone use.
+
+@author: Francois Aucamp <faucamp@csir.co.za>
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU LGPL
+"""
+import sys
+import array
+import socket, time
+from iterator import PreviewIterator
+
+class WTP:
+    """ This class implements a very limited subset of the WTP layer """
+    pduTypes = {0x00: None, # Not Used
+                0x01: 'Invoke',
+                0x02: 'Result',
+                0x03: 'Ack',
+                0x04: 'Abort',
+                0x05: 'Segmented Invoke',
+                0x06: 'Segmented Result',
+                0x07: 'Negative Ack'}
+    
+    abortTypes = {0x00: 'PROVIDER',
+                  0x01: 'USER'}
+    
+    abortReasons = {0x00: 'UNKNOWN',
+                    0x01: 'PROTOERR',
+                    0x02: 'INVALIDTID',
+                    0x03: 'NOTIMPLEMENTEDCL2',
+                    0x04: 'NOTIMPLEMENTEDSAR',
+                    0x05: 'NOTIMPLEMENTEDUACK',
+                    0x06: 'WTPVERSIONONE',
+                    0x07: 'CAPTEMPEXCEEDED',
+                    0x08: 'NORESPONSE',
+                    0x09: 'MESSAGETOOLARGE',
+                    0x10: 'NOTIMPLEMENTEDESAR'}
+    
+    def __init__(self, gatewayHost, gatewayPort=9201):
+        self.udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+        self.tidCounter = 0
+        # Currently "active" WTP transactions (their IDs)
+        self.activeTransactions = []
+        self.gatewayHost = gatewayHost
+        self.gatewayPort = gatewayPort
+    
+    def invoke(self, wspPDU):
+        """ Invoke (send) a request via WTP, and get the response.
+        
+        This method automatically assigns a new unique transaction ID to the 
+        transmitted PDU.
+        
+        @return: an iterator over the bytes read from the response
+        @rtype: mms.iterator.previewIterator
+        """
+        self.tidCounter += 1
+        print '>> WTP: Invoke, transaction ID: %d' % self.tidCounter
+        pdu = self.encodeInvokePDU(self.tidCounter) + wspPDU
+        self._sendPDU(pdu)
+        print '>> WTP: Sent PDU'
+        self.activeTransactions.append(self.tidCounter)    
+        return self._parseResponse(self._receiveData())
+    
+    def ack(self, transactionID):
+        print '>> WTP: Ack, transaction ID: %d' % transactionID
+        self._sendPDU(self.encodeAckPDU(transactionID))
+        
+    def _sendPDU(self, pdu):
+        """ Transmits a PDU through the socket
+        
+        @param pdu: The PDU to send (a sequence of bytes)
+        @type pdu: list
+        """
+        data = ''
+        for char in pdu:
+            data += chr(char)
+        self.udpSocket.sendto(data, (self.gatewayHost, self.gatewayPort))
+    
+    def _receiveData(self):
+        """ Read data from the UDP socket
+        
+        @return: The data read from the socket
+        @rtype: str
+        """
+        #done = False
+        done = True
+        response = ''
+        print '>> WTP: Receiving data'
+        while not done:
+            buff = self.udpSocket.recv(1024)
+            print buff
+            response += buff
+            if len(buff) < 1024:
+                done = True
+        return response
+
+    def _parseResponse(self, responseData):
+        """ Interpret data read from the socket (at the WTP layer level) 
+        
+        @param responseData: A buffer containing data to interpret
+        @type responseData: str
+        """
+        byteArray = array.array('B')
+        print responseData
+        for char in responseData:
+            byteArray.append(ord(char))
+        byteIter = PreviewIterator(byteArray)
+        pduType, transactionID = self._decodePDU(byteIter)
+        if pduType == 'Result':
+            self.ack(transactionID)
+        return byteIter
+        
+    
+    @staticmethod
+    def encodeInvokePDU(tid):
+        """ Builds a WTP Invoke PDU
+        
+        @param tid: The transaction ID for this PDU
+        @type tid: int
+        
+        @return: the WTP invoke PDU as a sequence of bytes
+        @rtype: list
+        
+        The WTP Invoke PDU structure is defined in WAP-224, section 8.3.1::
+             Bit| 0 |  1  |  2   |  3   |  4   | 5 | 6 | 7
+         Octet  |   |     |      |      |      |   |   |
+         1      |CON|    PDU Type = Invoke     |GTR|TDR|RID
+         2      |                TID
+         3      |
+         4      |Version  |TIDnew| U/P  |  RES |RES|  TCL
+         
+         ...where bit 0 is the most significant bit.
+        
+        Invoke PDU type = 0x01 = 0 0 0 1
+        GTR  is 0 and TDR is 1  (check: maybe make both 1: segmentation not supported)
+        RID is set to 0 (not retransmitted)
+        TCL is 0x02  == 1 0 (transaction class 2)
+        Version is 0x00 (according to WAP-224, section 8.3.1)
+        Thus, for our Invoke, this is::
+             Bit| 0 |  1  |  2   |  3   |  4   | 5 | 6 | 7
+         Octet  |   |     |      |      |      |   |   |
+         1      | 0 |  0  |  0   |  0   |  1   | 0 | 1 | 0
+         2      |   TID
+         3      |   TID
+         4      | 0 |  0  |  0   |  1   |  0   | 0 | 1 | 0 
+        """
+        #TODO: check GTR and TDR values (probably should rather be 11, for segmentation not supported)
+        pdu = [0x0a] # 0000 1010
+        pdu.extend(WTP._encodeTID(tid))
+        pdu.append(0x12) # 0001 0010
+        return pdu
+    
+    @staticmethod
+    def encodeAckPDU(tid):
+        """ Builds a WTP Ack PDU (acknowledge)
+        
+        @param tid: The transaction ID for this PDU
+        @type tid: int
+        
+        @return: the WTP invoke PDU as a sequence of bytes
+        @rtype: list
+        
+        The WTP PDU structure is defined in WAP-224, section 8
+        The ACK PDU structure is described in WAP-224, section 8.3.3::
+             Bit| 0 |  1  |  2   |  3   |  4   |   5   | 6 | 7
+         Octet  |   |     |      |      |      |       |   |
+         1      |CON|PDU Type = Acknowledgement|Tve/Tok|RES|RID
+         2                       TID
+         3
+
+         ...where bit 0 is the most significant bit.
+        
+        Thus, for our ACK, this is::
+             Bit| 0 |  1  |  2   |  3   |  4   | 5 | 6 | 7
+         Octet  |   |     |      |      |      |   |   |
+         1      | 0 |  0  |  0   |   1  |   1  | 0 | 0 | 0
+                    |  PDU type = 0x03 = 0011  |
+         2         TID
+         3         TID
+        """
+        pdu = [0x18] # binary: 00011000
+        pdu.extend(WTP._encodeTID(tid))
+        return pdu
+    
+    def _decodePDU(self, byteIter):
+        """ Reads and decodes a WTP PDU from the sequence of bytes starting at
+        the byte pointed to by C{dataIter.next()}.
+        
+        @param byteIter: an iterator over a sequence of bytes
+        @type byteIteror: mms.iterator.PreviewIterator
+        
+        @note: If the PDU type is correctly determined, byteIter will be
+               modified in order to read past the amount of bytes required
+               by the PDU type.
+        
+        @return: The PDU type, and the transaction ID, in the format:
+                 (str:<pdu_type>, int:<transaction_id>)
+        @rtype: tuple
+        """
+        byte = byteIter.preview()
+        byteIter.resetPreview()
+        # Get the PDU type
+        pduType = (byte >> 3) & 0x0f
+        pduValue = (None, None)
+        if pduType not in WTP.pduTypes:
+            #TODO: maybe raise some error or something
+            print 'Error - unknown WTP PDU type: %s' % hex(pduType)
+        else:
+            print '<< WTP: %s' % WTP.pduTypes[pduType],
+            try:
+                exec 'pduValue = self._decode%sPDU(byteIter)' % WTP.pduTypes[pduType]
+            except:
+                print 'A fatal error occurred, probably due to an unimplemented feature.\n'
+                raise
+        # after this follows the WSP pdu(s)....
+        return pduValue
+    
+    def _decodeResultPDU(self, byteIter):
+        """ Decodes a WTP Result PDU
+        
+        @param byteIter: an iterator over a sequence of bytes
+        @type byteIteror: mms.iterator.PreviewIterator
+        
+        The WTP Result PDU structure is defined in WAP-224, section 8.3.2::
+             Bit| 0 |  1  |  2   |  3   |  4   |   5   | 6 | 7
+         Octet  |   |     |      |      |      |       |   |
+         1      |CON|    PDU Type = Result     |Tve/Tok|RES|RID
+         2                       TID
+         3
+        
+        The WTP Result PDU Type is 0x02, according to WAP-224, table 11
+        """
+        # Read in 3 bytes
+        bytes = []
+        for i in range(3):
+            bytes.append(byteIter.next())
+        pduType = (bytes[0] >> 3) & 0x0f
+        # Get the transaction ID
+        transactionID = WTP._decodeTID(bytes[1:])
+        print 'transaction ID: %d' % transactionID
+        if transactionID in self.activeTransactions:
+            self.activeTransactions.remove(transactionID)
+        return (WTP.pduTypes[pduType], transactionID)
+    
+    def _decodeAckPDU(self, byteIter):
+        """ Decodes a WTP Result PDU
+        
+        @param byteIter: an iterator over a sequence of bytes
+        @type byteIteror: mms.iterator.PreviewIterator
+        
+        The ACK PDU structure is described in WAP-224, section 8.3.3::
+             Bit| 0 |  1  |  2   |  3   |  4   |   5   | 6 | 7
+         Octet  |   |     |      |      |      |       |   |
+         1      |CON|PDU Type = Acknowledgement|Tve/Tok|RES|RID
+         2                       TID
+         3
+        
+        The WTP Result PDU Type is 0x03, according to WAP-224, table 11
+        """
+        # Read in 3 bytes
+        bytes = []
+        for i in range(3):
+            bytes.append(byteIter.next())
+        pduType = (bytes[0] >> 3) & 0x0f
+        # Get the transaction ID
+        transactionID = WTP._decodeTID(bytes[1:])
+        print 'transaction ID: %d' % transactionID
+        if transactionID not in self.activeTransactions:
+            self.activeTransactions.append(transactionID)
+        return (WTP.pduTypes[pduType], transactionID)
+    
+    def _decodeAbortPDU(self, byteIter):
+        """ Decodes a WTP Abort PDU
+        
+        @param byteIter: an iterator over a sequence of bytes
+        @type byteIteror: mms.iterator.PreviewIterator
+        
+        The WTP Result PDU structure is defined in WAP-224, section 8.3.2::
+             Bit| 0 |  1  |  2   |  3   |  4   |   5   | 6 | 7
+         Octet  |   |     |      |      |      |       |   |
+         1      |CON|    PDU Type = Result     |   Abort type
+         2                       TID
+         3
+         4                  Abort reason
+        
+        The WTP Abort PDU Type is 0x04, according to WAP-224, table 11
+        """
+        # Read in 4 bytes
+        bytes = []
+        for i in range(4):
+            bytes.append(byteIter.next())
+        pduType = (bytes[0] >> 3) & 0x0f
+        abortType = bytes[0] & 0x07
+        abortReason = bytes[3]
+        if abortType in self.abortTypes:
+            abortType = self.abortTypes[abortType]
+        else:
+            abortType = str(abortType)
+        if abortReason in self.abortReasons:
+            abortReason = self.abortReasons[abortReason]
+        else:
+            abortReason = str(abortReason)
+        # Get the transaction ID
+        transactionID = WTP._decodeTID(bytes[1:3])
+        print 'transaction ID: %d' % transactionID
+        if transactionID in self.activeTransactions:
+            self.activeTransactions.remove(transactionID)
+        print 'WTP: Abort, type: %s, reason: %s' % (abortType, abortReason)
+        return (WTP.pduTypes[pduType], transactionID)
+    
+    @staticmethod
+    def _encodeTID(transactionID):
+        """ Encodes the specified transaction ID into the format used in
+        WTP PDUs (makes sure it spans 2 bytes)
+        
+        From WAP-224, section 7.8.1: The TID is 16-bits but the high order bit
+        is used to indicate the direction. This means that the TID space is
+        2**15. The TID is an unsigned integer.
+        
+        @param transactionID: The transaction ID to encode
+        @type transactionID: int
+        
+        @return: The encoded transaction ID as a sequence of bytes
+        @rtype: list
+        """
+        if transactionID > 0x7FFF:
+            raise ValueError, 'Transaction ID too large (must fit into 15 bits): %d' % transactionID
+        else:
+            encodedTID = [transactionID & 0xFF]
+            encodedTID.insert(0, transactionID >> 8)
+            return encodedTID
+
+    @staticmethod
+    def _decodeTID(bytes):
+        """ Decodes the transaction ID contained in <bytes>
+        
+        From WAP-224, section 7.8.1: The TID is 16-bits but the high order bit
+        is used to indicate the direction. This means that the TID space is
+        2**15. The TID is an unsigned integer.
+
+        @param bytes: The byte sequence containing the transaction ID
+        @type bytes: list
+        
+        @return: The decoded transaction ID
+        @rtype: int
+        """
+        tid = bytes[0] << 8
+        tid |= bytes[1]
+        # make unsigned
+        tid &= 0x7f
+        return tid
diff --git a/src/mms/__init__.py b/src/mms/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/mms/iterator.py b/src/mms/iterator.py
new file mode 100644 (file)
index 0000000..012e19d
--- /dev/null
@@ -0,0 +1,64 @@
+#
+# This library is free software, distributed under the terms of
+# the GNU Lesser General Public License Version 2.
+# See the COPYING.LESSER 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
+
+"""
+PyMMS library: Iterator with "value preview" capability.
+
+@version: 0.1
+@author: Francois Aucamp C{<faucamp@csir.co.za>}
+@license: GNU Lesser General Public License, version 2
+"""
+
+class PreviewIterator(object):
+    """ An C{iter} wrapper class providing a "previewable" iterator.
+    
+    This "preview" functionality allows the iterator to return successive
+    values from its C{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, *args):
+        self._it = iter(*args)
+        self._cachedValues = []
+        self._previewPos = 0
+    def __iter__(self):
+        return self
+    def next(self):
+        self.resetPreview()
+        if len(self._cachedValues) > 0:
+            return self._cachedValues.pop(0)
+        else:
+            return self._it.next()
+    
+    def preview(self):
+        """ Return the next item in the C{iteratable} object, but do not modify
+        the actual iterator (i.e. do not intefere with C{iter.next()}.
+        
+        Successive calls to C{preview()} will return successive values from
+        the C{iterable} object, exactly in the same way C{iter.next()} does.
+        
+        However, C{preview()} will always return the next item from
+        C{iterable} after the item returned by the previous C{preview()} or
+        C{next()} call, whichever was called the most recently. 
+        To force the "preview() iterator" to synchronize with the "next()
+        iterator" (without calling C{next()}), use C{resetPreview()}.
+        """
+        if self._previewPos < len(self._cachedValues):
+            value = self._cachedValues[self._previewPos]
+        else:
+            value = self._it.next()
+            self._cachedValues.append(value)
+        self._previewPos += 1
+        return value
+    
+    def resetPreview(self):
+        self._previewPos = 0
diff --git a/src/mms/message.py b/src/mms/message.py
new file mode 100644 (file)
index 0000000..471b047
--- /dev/null
@@ -0,0 +1,532 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# This library is free software, distributed under the terms of
+# the GNU Lesser General Public License Version 2.
+# See the COPYING.LESSER 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
+"""
+Library for MMS message creation, original by Francois Aucamp
+Modified by Nick Leppänen Larsson for use in Maemo5/Fremantle on the Nokia N900.
+
+@author: Francois Aucamp <faucamp@csir.co.za>
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU LGPL
+"""
+""" High-level MMS-message creation/manipulation classes """
+
+import xml.dom.minidom
+import os
+import mimetypes
+import array
+import random
+
+class MMSMessage:
+    """ An MMS message
+    
+    @note: References used in this class: [1][2][3][4][5]
+    """
+    def __init__(self, noHeaders=None):
+       self.noHeaders = noHeaders
+        transactionid = random.randint(0,100000)
+        self._pages = []
+        self._dataParts = []
+        self._metaTags = {}
+        if self.noHeaders == None:
+               self.headers = {'Message-Type' : 'm-send-req',
+                               'Transaction-Id' : transactionid,
+                               'MMS-Version' : '1.0',
+                               'Content-Type' : ('application/vnd.wap.multipart.mixed', {'Start': '<smil>', 'Type': 'application/smil'})}
+       else:
+               self.headers = {}
+        self.width = 320
+        self.height = 240
+        self.transactionID = transactionid
+        self.subject = 'Subject'
+        
+    # contentType property
+    @property
+    def contentType(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 the message's C{headers} attribute.
+        
+        This is equivalent to calling DataPart.headers['Content-Type'][0]
+        """
+        return self.headers['Content-Type'][0]
+
+    def addPage(self, page):
+        """ Adds a single page/slide (MMSMessagePage object) to the message 
+        
+        @param page: The message slide/page to add
+        @type page: MMSMessagPage
+        """
+        if self.contentType != 'application/vnd.wap.multipart.related':
+            self.headers['Content-Type'] = ('application/vnd.wap.multipart.related', {'Start': '<smil>', 'Type': 'application/smil'})
+            #self.headers['Content-Type'] = ('application/vnd.wap.multipart.related', {})
+        self._pages.append(page)
+    
+    @property
+    def pages(self):
+        """ Returns a list of all the pages in this message """
+        return self._pages
+        
+    def addDataPart(self, dataPart):
+        """ 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.
+        
+        @param dataPart: The data part to add
+        @type dataPart: DataPart
+        
+        @note: It is not necessary to explicitly add data parts to the message
+               using this function if "addPage" 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"
+        """
+        self._dataParts.append(dataPart)
+    
+    @property
+    def dataParts(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) > 0:
+            parts.append(self.smil())        
+            for slide in self._mmsMessage._pages:
+                parts.extend(slide.dataParts())
+        parts.extend(self._dataParts)
+        return parts
+        
+    
+    def smil(self):
+        """ Returns the text of the message's SMIL file """
+        impl = xml.dom.minidom.getDOMImplementation()
+        smilDoc = impl.createDocument(None, "smil", None)
+  
+        # Create the SMIL header
+        headNode = smilDoc.createElement('head')
+        # Add metadata to header
+        for tagName in self._metaTags:
+            metaNode = smilDoc.createElement('meta')
+            metaNode.setAttribute(tagName, self._metaTags[tagName])
+            headNode.appendChild(metaNode)
+        # Add layout info to header
+        layoutNode = smilDoc.createElement('layout')
+        rootLayoutNode = smilDoc.createElement('root-layout')
+        rootLayoutNode.setAttribute('width', str(self.width))
+        rootLayoutNode.setAttribute('height', str(self.height))
+        layoutNode.appendChild(rootLayoutNode)
+        #for regionID, left, top, width, height in (('Image', '0', '0', '176', '144'), ('Text', '176', '144', '176', '76')):
+        (regionID, left, top, width, height) = ('Text', '0', '0', '320', '240')
+       regionNode = smilDoc.createElement('region')
+       regionNode.setAttribute('id', regionID)
+       regionNode.setAttribute('left', left)
+       regionNode.setAttribute('top', top)
+       regionNode.setAttribute('width', width)
+       regionNode.setAttribute('height', height)
+       layoutNode.appendChild(regionNode)
+        headNode.appendChild(layoutNode)
+        smilDoc.documentElement.appendChild(headNode)
+        
+        # Create the SMIL body
+        bodyNode = smilDoc.createElement('body')
+        # Add pages to body
+        for page in self._pages:
+            parNode = smilDoc.createElement('par')
+            parNode.setAttribute('duration', str(page.duration))
+            # Add the page content information
+            if page.image != 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
+                imageNode = smilDoc.createElement('img')
+                imageNode.setAttribute('src', src)
+                imageNode.setAttribute('region', 'Image')
+                if begin > 0 or end > 0:
+                    if end > page.duration:
+                        end = page.duration
+                    imageNode.setAttribute('begin', str(begin))
+                    imageNode.setAttribute('end', str(end))
+                parNode.appendChild(imageNode)
+            if page.text != None:
+                part, begin, end = page.text
+                #src = part.data
+                textNode = smilDoc.createElement('text')
+                #textNode.setAttribute('src', src)
+                textNode.setAttribute('src', 'text.txt')
+                textNode.setAttribute('region', 'Text')
+                if begin > 0 or end > 0:
+                    if end > page.duration:
+                        end = page.duration
+                    textNode.setAttribute('begin', str(begin))
+                    textNode.setAttribute('end', str(end))
+                parNode.appendChild(textNode)
+            if page.audio != 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']
+                    pass
+                else:
+                    src = part.data
+                audioNode = smilDoc.createElement('audio')
+                audioNode.setAttribute('src', src)
+                if begin > 0 or end > 0:
+                    if end > page.duration:
+                        end = page.duration
+                    audioNode.setAttribute('begin', str(begin)) 
+                    audioNode.setAttribute('end', str(end))
+                parNode.appendChild(textNode)
+                parNode.appendChild(audioNode)
+            bodyNode.appendChild(parNode)
+        smilDoc.documentElement.appendChild(bodyNode)
+        
+        return smilDoc.documentElement.toprettyxml()
+
+
+    def encode(self):
+        """ Convenience funtion that binary-encodes this MMS message
+        
+        @note: This uses the C{mms_pdu.MMSEncoder} class internally
+        
+        @return: The binary-encode MMS data, as an array of bytes
+        @rtype array.array('B')
+        """
+        import mms_pdu
+        encoder = mms_pdu.MMSEncoder(self.noHeaders)
+        return encoder.encode(self)
+
+
+    def toFile(self, filename):
+        """ Convenience funtion that writes this MMS message to disk in 
+        binary-encoded form.
+        
+        @param filename: The name of the file in which to store the message
+                         data
+        @type filename: str
+    
+        @note: This uses the C{mms_pdu.MMSEncoder} class internally
+        
+        @return: The binary-encode MMS data, as an array of bytes
+        @rtype array.array('B')
+        """
+        f = open(filename, 'wb')
+        self.encode().tofile(f)
+        f.close()
+    
+    @staticmethod
+    def fromFile(filename):
+        """ Convenience static funtion that loads the specified MMS message
+        file from disk, decodes its data, and returns a new MMSMessage object,
+        which can then be manipulated and re-encoded, for instance.
+        
+        @param filename: The name of the file to load
+        @type filename: str
+    
+        @note: This uses the C{mms_pdu.MMSDecoder} class internally
+        """
+        import mms_pdu
+        decoder = mms_pdu.MMSDecoder(os.path.dirname(filename))
+        return decoder.decodeFile(filename)
+
+    @staticmethod
+    def fromData(inputdata):
+        """ Convenience static funtion that loads the specified MMS message
+        file from input, decodes its data, and returns a new MMSMessage object,
+        which can then be manipulated and re-encoded, for instance.
+        
+        @param input: Input data to decode
+        @type input: str
+    
+        @note: This uses the C{mms_pdu.MMSDecoder} class internally
+        """
+        import mms_pdu
+        decoder = mms_pdu.MMSDecoder()
+        data = array.array('B')
+        data.fromstring(inputdata)
+        return decoder.decodeData(data)
+
+class MMSMessagePage:
+    """ A single page (or "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].
+    
+    @note: The default slide duration is set to 4 seconds; use setDuration()
+           to change this.
+    
+    @note: References used in this class: [1]
+    """
+    def __init__(self):
+        self.duration = 4000
+        self.image = None
+        self.audio = None
+        self.text = None
+    
+    @property
+    def dataParts(self):
+        """ Returns a list of the data parts in this slide """
+        parts = []
+        for part in (self.image, self.audio, self.text):
+            if part != None:
+                parts.append(part)
+        return parts
+
+    def numberOfParts(self):
+        """ This function calculates the amount of data "parts" (or elements)
+        in this slide.
+            
+        @return: The number of data parts in this slide
+        @rtype: int
+        """
+        numParts = 0
+        for item in (self.image, self.audio, self.text):
+            if item != None:
+                numParts += 1
+        return numParts
+        
+    #TODO: find out what the "ref" element in SMIL does (seen in conformance doc)
+    
+    #TODO: add support for "alt" element; also make sure what it does
+    def addImage(self, filename, timeBegin=0, timeEnd=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 timeBegin: 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 timeBegin: int
+        @param timeEnd: 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 timeEnd: int
+        
+        @raise TypeError: An inappropriate variable type was passed in of the
+                          parameters
+        """
+        if type(filename) != str or type(timeBegin) != type(timeEnd) != int:
+            raise TypeError
+        if not os.path.isfile(filename):
+            raise OSError
+        if timeEnd > 0 and timeEnd < timeBegin:
+            raise ValueError, 'timeEnd cannot be lower than timeBegin'
+        self.image = (DataPart(filename), timeBegin, timeEnd)
+    
+    def addAudio(self, filename, timeBegin=0, timeEnd=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 timeBegin: 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 timeBegin: int
+        @param timeEnd: 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 timeEnd: int
+        
+        @raise TypeError: An inappropriate variable type was passed in of the
+                          parameters
+        """
+        if type(filename) != str or type(timeBegin) != type(timeEnd) != int:
+            raise TypeError
+        if not os.path.isfile(filename):
+            raise OSError
+        if timeEnd > 0 and timeEnd < timeBegin:
+            raise ValueError, 'timeEnd cannot be lower than timeBegin'
+        self.audio = (DataPart(filename), timeBegin, timeEnd)
+    
+    def addText(self, text, timeBegin=0, timeEnd=0):
+        """ Adds a block of text to this slide.
+        @param text: The text to add to the slide.
+        @type text: str
+        @param timeBegin: 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 timeBegin: int
+        @param timeEnd: 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 timeEnd: int
+        
+        @raise TypeError: An inappropriate variable type was passed in of the
+                          parameters
+        """
+        if type(text) != str or type(timeBegin) != type(timeEnd) != int:
+            raise TypeError
+        if timeEnd > 0 and timeEnd < timeBegin:
+            raise ValueError, 'timeEnd cannot be lower than timeBegin'
+        tData = DataPart()
+        tData.setText(text)
+        self.text = (tData, timeBegin, timeEnd)
+    
+    def setDuration(self, duration):
+        """ Sets the maximum duration of this slide (i.e. how long this slide
+        should be displayed)
+        
+        @param duration: the maxium slide duration, in milliseconds
+        @type duration: int
+        
+        @raise TypeError: <duration> must be an integer
+        @raise ValueError: the requested duration is invalid (must be a
+                           non-zero, positive integer)
+        """
+        if type(duration) != int:
+            raise TypeError
+        elif duration < 1:
+            raise ValueError, 'duration may not be 0 or negative'
+        self.duration = duration
+
+class DataPart:
+    """ This class represents a data entry in the MMS body.
+    
+    A DataPart objectencapsulates any data content that is to be added to the
+    MMS (e.g. an image file, raw image data, audio clips, text, etc).
+    
+    A DataPart object can be queried using the Python built-in C{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, srcFilename=None):
+        """ @param srcFilename: If specified, load the content of the file
+                                with this name
+            @type srcFilename: str
+        """
+        #self.contentTypeParameters = {}
+        self.headers = {'Content-Type': ('application/octet-stream', {})}
+        self._filename = None
+        self._data = None
+        self._part = "<fMMSpart" + str(random.randint(0,10000)) + ">"
+        if srcFilename != None:
+            self.fromFile(srcFilename)
+    
+    # contentType property
+    def _getContentType(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 _setContentType(self, value):
+        """ Convenience method that sets the content type string, with no
+        parameters """
+        self.headers['Content-Type'] = (value, {})
+       contentType = property(_getContentType, _setContentType) 
+    
+    def fromFile(self, filename):
+        """ Load the data contained in the specified file
+        
+        @note: 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)  
+        #self.contentType = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+        #print mimetypes.guess_type(filename)[0]
+        if mimetypes.guess_type(filename)[0] == 'text/plain':
+               self.headers['Content-Type'] = ('text/plain', {'Charset': 'utf-8', 'Name': os.path.basename(filename)})
+        else:
+               self.headers['Content-Type'] = (mimetypes.guess_type(filename)[0] or 'application/octet-stream', {'Name': os.path.basename(filename)})
+        #self.headers['Content-Type'] = (mimetypes.guess_type(filename)[0] or 'application/octet-stream', {})
+        #self.headers['Content-ID'] = self._part
+        self._filename = filename
+    
+    def setData(self, data, contentType, ctParameters={}):
+        """ Explicitly set the data contained by this part
+        
+        @note: This function clears any previously-set header entries.
+        
+        @param data: The data to hold
+        @type data: str
+        @param contentType: The MIME content type of the specified data
+        @type contentType: str
+        @param ctParameters: A dictionary containing any content type header
+                             parmaters to add, in the format:
+                             C{{<parameter_name> : <parameter_value>}}
+        @type ctParameters: dict
+        """
+        self.headers = {}
+        self._filename = None
+        # self._data = data
+        # self._data = self._part + "\0"
+        self._data = data
+        if contentType == "application/smil":
+               #self.headers['Content-Type'] = (contentType, {'Charset': 'utf-8', 'Name': 'smil.smil'})
+               self.headers['Content-Type'] = (contentType, {})
+        else:
+               self.headers['Content-Type'] = (contentType, ctParameters)
+               self.headers['Content-ID'] = (self._part)
+        
+    def setText(self, text):
+        """ Convenience wrapper method for setData()
+        
+        This method sets the DataPart object to hold the specified text
+        string, with MIME content type "text/plain".
+        
+        @param text: The text to hold
+        @type text: str
+        """
+        self.setData(text, 'text/plain', {'Charset': 'utf-8', 'Name': 'text.txt'})
+    
+    def __len__(self):
+        """ Provides the length of the data encapsulated by this object """
+        if self._filename != None:
+            return int(os.stat(self._filename)[6])
+        else:
+            return len(self.data)
+    
+    @property
+    def data(self):
+        """ @return: the data of this part
+        @rtype: str
+        """
+        if self._data != None:
+            if type(self._data) == array.array:
+                self._data = self._data.tostring()
+            return self._data
+        elif self._filename != None:
+            f = open(self._filename, 'r')
+            self._data = f.read()
+            f.close()
+            return self._data
+        else:
+            return ''
diff --git a/src/mms/mms_pdu.py b/src/mms/mms_pdu.py
new file mode 100644 (file)
index 0000000..5fc1797
--- /dev/null
@@ -0,0 +1,1145 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# This library is free software, distributed under the terms of
+# the GNU Lesser General Public License Version 2.
+# See the COPYING.LESSER 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
+
+""" MMS Data Unit structure encoding and decoding classes 
+Original by Francois Aucamp
+Modified by Nick Leppänen Larsson for use in Maemo5/Fremantle on the Nokia N900.
+
+@author: Francois Aucamp <faucamp@csir.co.za>
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU LGPL
+"""
+
+import os, array
+import wsp_pdu
+import message
+from iterator import PreviewIterator
+
+class MMSEncodingAssignments:
+    fieldNames = {0x01 : ('Bcc', 'EncodedStringValue'),
+                  0x02 : ('Cc', 'EncodedStringValue'),
+                  0x03 : ('Content-Location', 'UriValue'),
+                  0x04 : ('Content-Type','ContentTypeValue'),
+                  0x05 : ('Date', 'DateValue'),
+                  0x06 : ('Delivery-Report', 'BooleanValue'),
+                  0x07 : ('Delivery-Time', None),
+                  0x08 : ('Expiry', 'ExpiryValue'),
+                  0x09 : ('From', 'FromValue'),
+                  0x0a : ('Message-Class', 'MessageClassValue'),
+                  0x0b : ('Message-ID', 'TextString'),
+                  0x0c : ('Message-Type', 'MessageTypeValue'),
+                  0x0d : ('MMS-Version', 'VersionValue'),
+                  0x0e : ('Message-Size', 'LongInteger'),
+                  0x0f : ('Priority', 'PriorityValue'),
+                  0x10 : ('Read-Reply', 'BooleanValue'),
+                  0x11 : ('Report-Allowed', 'BooleanValue'),
+                  0x12 : ('Response-Status', 'ResponseStatusValue'),
+                  0x13 : ('Response-Text', 'EncodedStringValue'),
+                  0x14 : ('Sender-Visibility', 'SenderVisibilityValue'),
+                  0x15 : ('Status', 'StatusValue'),
+                  0x16 : ('Subject', 'EncodedStringValue'),
+                  0x17 : ('To', 'EncodedStringValue'),
+                  0x18 : ('Transaction-Id', 'TextString'),
+                  0x40 : ('Content-ID', 'TextString')}
+
+
+class MMSDecoder(wsp_pdu.Decoder):
+    """ A decoder for MMS messages """
+    def __init__(self, filename=None, noHeaders=None):
+        """ @param filename: If specified, decode the content of the MMS
+                             message file with this name
+            @type filename: str
+        """
+        self._mmsData = array.array('B')
+        self._mmsMessage = message.MMSMessage(noHeaders)
+        self._parts = []
+        self._path = filename
+
+    def decodeFile(self, filename):
+        """ Load the data contained in the specified file, and decode it.
+        
+        @param filename: The name of the MMS message file to open
+        @type filename: str
+        
+        @raises OSError: The filename is invalid
+        
+        @return: The decoded MMS data
+        @rtype: MMSMessage
+        """
+        nBytes = os.stat(filename)[6]
+        data = array.array('B')
+        f = open(filename, 'rb')
+        data.fromfile(f, nBytes)
+        f.close()
+        return self.decodeData(data)
+        
+    def decodeData(self, data):
+        """ Decode the specified MMS message data
+        
+        @note: This creates a 'headers' file containing
+               MMS headers as plain-text
+     
+        @param data: The MMS message data to decode
+        @type filename: array.array('B')
+        
+        @return: The decoded MMS data
+        @rtype: MMSMessage
+        """
+        self._mmsMessage = message.MMSMessage()
+        self._mmsData = data
+        bodyIter = self.decodeMessageHeader()
+        #print "body begins at: ", bodyIter._it, bodyIter._previewPos
+        # TODO: separate this in to own method?
+       if self._path != None:
+               hdrlist = self.decodeMessageHeaderToList(data)
+               fp = open(self._path + "/headers", 'w')
+               for k in hdrlist:
+                       #print "MMS HEADER:", k, hdrlist[k]     
+                       fp.write(str(k) + " " + str(hdrlist[k]) + "\n")
+               fp.close()
+        self._mmsMessage.attachments = self.decodeMessageBodyToPath(bodyIter)
+        #print self._mmsMessage.headers
+        return self._mmsMessage
+    
+    def decodeCustom(self, data):
+        list = self.decodeMessageHeaderToList(data)
+        return list
+   
+    def decodeMessageHeaderToList(self, data):
+        """ Decodes the (full) MMS header data
+        
+        @note: This B{must} be called before C{_decodeBody()}, as it sets
+        certain internal variables relating to data lengths, etc.
+        """
+        dataIter = PreviewIterator(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
+        # If the message type is a M-Notification* or M-Acknowledge
+        #type we don't expect any ContentType and should break on
+        # StopIteration exception
+        contentTypeFound = False
+        contentLocationFound = False
+        while (contentTypeFound == False):
+            try:
+               header, value = self.decodeHeader(dataIter)
+            except StopIteration, e:
+               print e, e.args
+               break
+            if header == MMSEncodingAssignments.fieldNames[0x04][0]:
+                contentTypeFound = True
+            elif header == MMSEncodingAssignments.fieldNames[0x03][0]:
+                contentLocationFound = True
+                break
+                #pass
+            else:
+               try:
+                       self._mmsMessage.headers[header] = value
+                       #print '%s: %s' % (header, str(value))
+               except StopIteration, e:
+                       print e, e.args
+                       break
+        
+        cType = value[0]
+        #print '%s: %s' % (header, cType)
+        params = value[1]
+        #for parameter in params:
+        #    print '    %s: %s' % (parameter, str(params[parameter]))
+
+        self._mmsMessage.headers[header] = (value)
+        return self._mmsMessage.headers
+    
+    """ messy stuff """
+    # TODO: clean up
+    def decodeResponseHeader(self, data):
+               dataIter = PreviewIterator(data)
+               length = len(data)
+               headers = {}
+               eof = False
+               while 1:
+                       try:
+                               try:
+                                       byte = wsp_pdu.Decoder.decodeShortIntegerFromByte(dataIter.preview())
+                               except StopIteration:
+                                       break
+                               if byte in MMSEncodingAssignments.fieldNames:
+                                   dataIter.next()
+                                   mmsFieldName = MMSEncodingAssignments.fieldNames[byte][0]
+                               else:
+                                   dataIter.resetPreview()
+                                   raise wsp_pdu.DecodeError, 'Invalid MMS Header: could not decode MMS field name'
+                               try:
+                                   #print MMSEncodingAssignments.fieldNames[byte][1]
+                                   exec 'mmsValue = MMSDecoder.decode%s(dataIter)' % MMSEncodingAssignments.fieldNames[byte][1]
+                               except wsp_pdu.DecodeError, msg:
+                                   raise wsp_pdu.DecodeError, 'Invalid MMS Header: Could not decode MMS-value: %s' % msg
+                               except:
+                                   print 'A fatal error occurred, probably due to an unimplemented decoding operation. Tried to decode header: %s' % mmsFieldName
+                                   raise
+                       except wsp_pdu.DecodeError:
+                               mmsFieldName, mmsValue = wsp_pdu.Decoder.decodeHeader(dataIter) #MMSDecoder.decodeApplicationHeader(byteIter)
+                       headers[mmsFieldName] = mmsValue
+               return headers
+    
+    def decodeMessageHeader(self):
+        """ Decodes the (full) MMS header data
+        
+        @note: This B{must} be called before C{_decodeBody()}, as it sets
+        certain internal variables relating to data lengths, etc.
+        """
+        dataIter = PreviewIterator(self._mmsData)
+        
+        # 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
+        contentTypeFound = False
+        while contentTypeFound == False:
+            header, value = self.decodeHeader(dataIter)
+            #print (header, value)
+            if header == MMSEncodingAssignments.fieldNames[0x04][0]:
+                contentTypeFound = True
+            else:
+                self._mmsMessage.headers[header] = value
+                #print '%s: %s' % (header, str(value))
+        
+        cType = value[0]
+        #print '%s: %s' % (header, cType)
+        params = value[1]
+        #for parameter in params:
+        #    print '    %s: %s' % (parameter, str(params[parameter]))
+
+        self._mmsMessage.headers[header] = (cType, params)
+        #print self._mmsMessage.headers
+        return dataIter
+    
+            
+    def decodeMessageBody(self, dataIter):
+        """ Decodes the MMS message body
+        
+        @param dataIter: an iterator over the sequence of bytes of the MMS
+                         body
+        @type dataIteror: iter
+        """
+        ######### MMS body: headers ###########
+        # Get the number of data parts in the MMS body
+        nEntries = self.decodeUintvar(dataIter)
+        #print 'Number of data entries (parts) in MMS body:', nEntries
+        
+        ########## MMS body: entries ##########
+        # For every data "part", we have to read the following sequence:
+        # <length of content-type + other possible headers>,
+        # <length of data>,
+        # <content-type + other possible headers>,
+        # <data>
+        for partNum in range(nEntries):
+            #print '\nPart %d:\n------' % partNum
+            headersLen = self.decodeUintvar(dataIter)
+            dataLen = self.decodeUintvar(dataIter)
+            
+            # Prepare to read content-type + other possible headers
+            ctFieldBytes = []
+            for i in range(headersLen):
+                ctFieldBytes.append(dataIter.next())
+#            ctIter = iter(ctFieldBytes)
+            ctIter = PreviewIterator(ctFieldBytes)
+            # Get content type
+            contentType, ctParameters = self.decodeContentTypeValue(ctIter)
+            headers = {'Content-Type' : (contentType, ctParameters)}
+            #print 'Content-Type:', contentType
+            #for param in ctParameters:
+            #    print '    %s: %s' % (param, str(ctParameters[param]))
+                
+            # Now read other possible headers until <headersLen> bytes have been read
+            while True:
+                try:
+                    hdr, value = self.decodeHeader(ctIter)
+                    headers[hdr] = value
+                    #print '%s: %s' % (otherHeader, otherValue)
+                except StopIteration:
+                    break
+            #print 'Data length:', dataLen, 'bytes'
+            
+            # Data (note: this is not null-terminated)
+            data = array.array('B')
+            for i in range(dataLen):
+                data.append(dataIter.next())
+            
+            part = message.DataPart()
+            part.setData(data, contentType)
+            part.contentTypeParameters = ctParameters
+            part.headers = headers
+            self._mmsMessage.addDataPart(part)
+            # TODO: Make this pretty
+            #extension = 'dump'
+            if contentType == 'image/jpeg':
+                extension = 'jpg'
+            #if contentType == 'image/gif':
+            #    extension = 'gif'
+            #elif contentType == 'audio/wav':
+            #    extension = 'wav'
+            #elif contentType == 'audio/midi':
+            #    extension = 'mid'
+            elif contentType == 'text/plain':
+                extension = 'txt'
+            elif contentType == 'application/smil':
+                extension = 'smil'
+            
+            f = open('part%d.%s' % (partNum, extension), 'wb')
+            data.tofile(f)
+            f.close()
+    
+    def decodeMessageBodyToPath(self, dataIter):
+        """ Decodes the MMS message body
+        
+        @param dataIter: an iterator over the sequence of bytes of the MMS
+                         body
+        @type dataIteror: iter
+        """
+        ######### MMS body: headers ###########
+        # Get the number of data parts in the MMS body
+        nEntries = self.decodeUintvar(dataIter)
+        #print 'Number of data entries (parts) in MMS body:', nEntries
+        
+        attachments = []
+        
+        ########## MMS body: entries ##########
+        # For every data "part", we have to read the following sequence:
+        # <length of content-type + other possible headers>,
+        # <length of data>,
+        # <content-type + other possible headers>,
+        # <data>
+        for partNum in range(nEntries):
+            #print '\nPart %d:\n------' % partNum
+            headersLen = self.decodeUintvar(dataIter)
+            dataLen = self.decodeUintvar(dataIter)
+            
+            # Prepare to read content-type + other possible headers
+            ctFieldBytes = []
+            for i in range(headersLen):
+                ctFieldBytes.append(dataIter.next())
+#            ctIter = iter(ctFieldBytes)
+            ctIter = PreviewIterator(ctFieldBytes)
+            # Get content type
+            contentType, ctParameters = self.decodeContentTypeValue(ctIter)
+            headers = {'Content-Type' : (contentType, ctParameters)}
+            #print 'Content-Type:', contentType
+            #for param in ctParameters:
+            #    print '    %s: %s' % (param, str(ctParameters[param]))
+                
+            # Now read other possible headers until <headersLen> bytes have been read
+            while True:
+                try:
+                    hdr, value = self.decodeHeader(ctIter)
+                    headers[hdr] = value
+                    #print '%s: %s' % (otherHeader, otherValue)
+                except StopIteration:
+                    break
+            #print 'Data length:', dataLen, 'bytes'
+            
+            # Data (note: this is not null-terminated)
+            data = array.array('B')
+            for i in range(dataLen):
+                data.append(dataIter.next())
+            
+            part = message.DataPart()
+            part.setData(data, contentType)
+            part.contentTypeParameters = ctParameters
+            part.headers = headers
+            self._mmsMessage.addDataPart(part)
+            # TODO: Make this pretty
+            #extension = 'dump'
+            if contentType == 'image/jpeg':
+                extension = 'jpg'
+            if contentType == 'image/gif':
+                extension = 'gif'
+            elif contentType == 'audio/wav':
+                extension = 'wav'
+            elif contentType == 'audio/midi':
+                extension = 'mid'
+            elif contentType == 'text/plain':
+                extension = 'txt'
+            elif contentType == 'application/smil':
+                extension = 'smil'
+            else:
+               extension = 'unknown'
+            
+            
+            ## TODO: FIX THIS ##
+            dirname = self._path
+            #dirname = self._path + "_dir"
+            ## TODO: FIX THIS ##
+            
+            if not os.path.isdir(dirname):
+                os.makedirs(dirname)
+            filename = None
+            #print "MMSBODY HEADERS:", headers
+            
+            ### loop through the headers, if we find a "name" header
+            ### using it seems like a good idea, right?
+           try:
+                       for k in headers:
+                               print k, headers[k]
+                               h = headers[k][1]
+                               if h.__class__ == dict:
+                                   filename = h['Name']
+           except:
+                       # this shouldnt really happen, but just in case...
+                       pass
+
+            
+            if filename == None:    
+               filename = str(partNum) + "." + str(extension)
+            f = open(dirname + '/%s' % (filename), 'wb')
+            data.tofile(f)
+            f.close()
+            attachments.append(filename)
+               return attachments
+    
+    @staticmethod
+    def decodeHeader(byteIter):
+        """ Decodes a header entry from an MMS message, starting at the byte
+        pointed to by C{byteIter.next()}
+        
+        From [4], section 7.1:
+        C{Header = MMS-header | Application-header}
+        
+        @raise DecodeError: This uses C{decodeMMSHeader()} and
+                            C{decodeApplicationHeader()}, and will raise this
+                            exception under the same circumstances as
+                            C{decodeApplicationHeader()}. C{byteIter} will
+                            not be modified in this case.
+        
+        @note: 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).
+        
+        @return: The decoded header entry from the MMS, in the format:
+                 (<str:header name>, <str/int/float:header value>)
+        @rtype: tuple
+        """
+        header = ''
+        value = ''
+        try:
+            header, value = MMSDecoder.decodeMMSHeader(byteIter)
+        except wsp_pdu.DecodeError:
+            header, value = wsp_pdu.Decoder.decodeHeader(byteIter) #MMSDecoder.decodeApplicationHeader(byteIter)
+        return (header, value)
+    
+    @staticmethod
+    def decodeMMSHeader(byteIter):
+        """ 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
+        
+        This method takes into account the assigned number values for MMS
+        field names, as specified in [4], section 7.3, table 8.            
+        
+        @raise wsp_pdu.DecodeError: The MMS field name could not be parsed.
+                                C{byteIter} will not be modified in this case.
+        
+        @return: The decoded MMS header, in the format:
+                 (<str:MMS-field-name>, <str:MMS-value>)
+        @rtype: tuple
+        """
+        # Get the MMS-field-name
+        mmsFieldName = ''
+        byte = wsp_pdu.Decoder.decodeShortIntegerFromByte(byteIter.preview())
+        #byte = wsp_pdu.Decoder.decodeShortInteger(byteIter)
+        if byte in MMSEncodingAssignments.fieldNames:
+            byteIter.next()
+            mmsFieldName = MMSEncodingAssignments.fieldNames[byte][0]
+#            byteIter.next()
+        else:
+            byteIter.resetPreview()
+            raise wsp_pdu.DecodeError, 'Invalid MMS Header: could not decode MMS field name'
+        # Now get the MMS-value
+        mmsValue = ''
+        try:
+            #print MMSEncodingAssignments.fieldNames[byte][1]
+            exec 'mmsValue = MMSDecoder.decode%s(byteIter)' % MMSEncodingAssignments.fieldNames[byte][1]
+        except wsp_pdu.DecodeError, msg:
+            raise wsp_pdu.DecodeError, 'Invalid MMS Header: Could not decode MMS-value: %s' % msg
+        except:
+            print 'A fatal error occurred, probably due to an unimplemented decoding operation. Tried to decode header: %s' % mmsFieldName
+            raise
+        return (mmsFieldName, mmsValue)
+
+    @staticmethod
+    def decodeEncodedStringValue(byteIter):
+        """ From [4], section 7.2.9:
+        C{Encoded-string-value = Text-string | Value-length Char-set Text-string}
+        The Char-set values are registered by IANA as MIBEnum value.
+        
+        @note: 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
+        """
+        decodedString = ''
+        try:
+            # First try "Value-length Char-set Text-string"
+            valueLength = wsp_pdu.Decoder.decodeValueLength(byteIter)
+            #TODO: *probably* have to include proper support for charsets...
+            try:
+                charSetValue = wsp_pdu.Decoder.decodeWellKnownCharset(byteIter)
+            except wsp_pdu.DecodeError, msg:
+                raise Exception, 'EncodedStringValue decoding error: Could not decode Char-set value; %s' % msg
+            decodedString = wsp_pdu.Decoder.decodeTextString(byteIter)
+        except wsp_pdu.DecodeError:
+            # Fall back on just "Text-string"
+            decodedString = wsp_pdu.Decoder.decodeTextString(byteIter)
+        return decodedString
+    
+    #TODO: maybe change this to boolean values
+    @staticmethod
+    def decodeBooleanValue(byteIter):
+        """ From [4], section 7.2.6::
+         Delivery-report-value = Yes | No
+         Yes = <Octet 128>
+         No = <Octet 129>
+        
+        A lot of other yes/no fields use this encoding (read-reply, 
+        report-allowed, etc)
+        
+        @raise wsp_pdu.DecodeError: The boolean value could not be parsed.
+                                C{byteIter} will not be modified in this case.
+        
+        @return The value for the field: 'Yes' or 'No'
+        @rtype: str
+        """
+        value = ''
+#        byteIter, localIter = itertools.tee(byteIter)
+#        byte = localIter.next()
+        byte = byteIter.preview()
+        if byte not in (128, 129):
+            byteIter.resetPreview()
+            raise wsp_pdu.DecodeError, 'Error parsing boolean value for byte: %s' % hex(byte)
+        else:
+            byte = byteIter.next()
+            if byte == 128:
+                value = 'Yes'
+            elif byte == 129:
+                value = 'No'
+        return value
+
+    @staticmethod
+    def decodeFromValue(byteIter):
+        """ From [4], section 7.2.11:
+        From-value = Value-length (Address-present-token Encoded-string-value | Insert-address-token )
+        Address-present-token = <Octet 128>
+        Insert-address-token = <Octet 129>
+        
+        @return: The "From" address value
+        @rtype: str
+        """
+        fromValue = ''
+        valueLength = wsp_pdu.Decoder.decodeValueLength(byteIter)
+        # See what token we have
+        byte = byteIter.next()
+        if byte == 129: # Insert-address-token
+            fromValue = '<not inserted>'
+        else:
+            fromValue = MMSDecoder.decodeEncodedStringValue(byteIter)
+        return fromValue
+    
+    @staticmethod
+    def decodeMessageClassValue(byteIter):
+        """ From [4], section 7.2.12:
+        Message-class-value = Class-identifier | Token-text
+        Class-identifier = Personal | Advertisement | Informational | Auto
+        Personal = <Octet 128>
+        Advertisement = <Octet 129>
+        Informational = <Octet 130>
+        Auto = <Octet 131>
+        The token-text is an extension method to the message class.
+        
+        @return: The decoded message class
+        @rtype: str
+        """
+        classIdentifiers = {128 : 'Personal',
+                            129 : 'Advertisement',
+                            130 : 'Informational',
+                            131 : 'Auto'}
+        msgClass = ''
+#        byteIter, localIter = itertools.tee(byteIter)
+#        byte = localIter.next()
+        byte = byteIter.preview()
+        if byte in classIdentifiers:
+            byteIter.next()
+            msgClass = classIdentifiers[byte]
+        else:
+            byteIter.resetPreview()
+            msgClass = wsp_pdu.Decoder.decodeTokenText(byteIter)
+        return msgClass
+
+    @staticmethod
+    def decodeMessageTypeValue(byteIter):
+        """ Defined in [4], section 7.2.14.
+
+        @return: The decoded message type, or '<unknown>'
+        @rtype: str
+        """
+        messageTypes = {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 = byteIter.preview()
+        if byte in messageTypes:
+            byteIter.next()
+            return messageTypes[byte]
+        else:
+            byteIter.resetPreview()
+            return '<unknown>'
+    
+    @staticmethod
+    def decodePriorityValue(byteIter):
+        """ Defined in [4], section 7.2.17
+        
+        @raise wsp_pdu.DecodeError: The priority value could not be decoded;
+                                C{byteIter} is not modified in this case.
+        
+        @return: The decoded priority value
+        @rtype: str
+        """
+        priorities = {128 : 'Low',
+                      129 : 'Normal',
+                      130 : 'High'}
+#        byteIter, localIter = itertools.tee(byteIter)
+        byte = byteIter.preview()
+        if byte in priorities:
+            byte = byteIter.next()
+            return priorities[byte]
+        else:
+            byteIter.resetPreview()
+            raise wsp_pdu.DecodeError, 'Error parsing Priority value for byte:',byte
+        
+    @staticmethod
+    def decodeSenderVisibilityValue(byteIter):
+        """ Defined in [4], section 7.2.22::
+         Sender-visibility-value = Hide | Show
+         Hide = <Octet 128>
+         Show = <Octet 129>
+        
+        @raise wsp_pdu.DecodeError: The sender visibility value could not be
+                                parsed.
+                                C{byteIter} will not be modified in this case.
+        
+        @return: The sender visibility: 'Hide' or 'Show'
+        @rtype: str
+        """
+        value = ''
+#        byteIter, localIter = itertools.tee(byteIter)
+#        byte = localIter.next()
+        byte = byteIter.preview()
+        if byte not in (128, 129):
+            byteIter.resetPreview()
+            raise wsp_pdu.DecodeError, 'Error parsing sender visibility value for byte: %s' % hex(byte)
+        else:
+            byte = byteIter.next()
+            if byte == 128:
+                value = 'Hide'
+            elif byte == 129:
+                value = 'Show'
+        return value
+    
+    @staticmethod
+    def decodeResponseStatusValue(byteIter):
+        """ Defined in [4], section 7.2.20
+        
+        Used to decode the "Response Status" MMS header.
+        
+        @raise wsp_pdu.DecodeError: The sender visibility value could not be
+                                parsed.
+                                C{byteIter} will not be modified in this case.
+        
+        @return: The decoded Response-status-value
+        @rtype: str
+        """
+        responseStatusValues = {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 = byteIter.preview()
+        if byte in responseStatusValues:
+            byteIter.next()
+            return responseStatusValues[byte]
+        else:
+            byteIter.next()
+            # Return an unspecified error if the response is not recognized
+            return responseStatusValues[0x81]
+        
+    @staticmethod
+    def decodeStatusValue(byteIter):
+        """ Defined in [4], section 7.2.23
+        
+        Used to decode the "Status" MMS header.
+        
+        @raise wsp_pdu.DecodeError: The sender visibility value could not be
+                                parsed.
+                                C{byteIter} will not be modified in this case.
+        
+        @return: The decoded Status-value
+        @rtype: str
+        """
+        
+        statusValues = {0x80 : 'Expired',
+                        0x81 : 'Retrieved',
+                        0x82 : 'Rejected',
+                        0x83 : 'Deferred',
+                        0x84 : 'Unrecognised'}
+        
+        byte = byteIter.preview()
+        if byte in statusValues:
+            byteIter.next()
+            return statusValues[byte]
+        else:
+            byteIter.next()
+            # Return an unrecognised state if it couldn't be decoded
+            return statusValues[0x84]
+    
+    
+    @staticmethod
+    def decodeExpiryValue(byteIter):
+        """ Defined in [4], section 7.2.10
+        
+        Used to decode the "Expiry" MMS header.
+        
+        From [4], section 7.2.10:
+        Expiry-value = Value-length (Absolute-token Date-value | Relative-token Delta-seconds-value)
+        Absolute-token = <Octet 128>
+        Relative-token = <Octet 129>
+
+        @raise wsp_pdu.DecodeError: The Expiry-value could not be decoded
+        
+        @return: The decoded Expiry-value, either as a date, or as a delta-seconds value
+        @rtype: str or int
+        """
+        valueLength = MMSDecoder.decodeValueLength(byteIter)
+        token = byteIter.next()
+        
+        if token == 0x80: # Absolute-token
+            data = MMSDecoder.decodeDateValue(byteIter)
+        elif token == 0x81: # Relative-token
+            data = MMSDecoder.decodeDeltaSecondsValue(byteIter)
+        else:
+            raise wsp_pdu.DecodeError, 'Unrecognized token value: %s' % hex(token)
+        return data
+        
+    
+class MMSEncoder(wsp_pdu.Encoder):
+    def __init__(self, noHeaders=None):
+        self.noHeaders = noHeaders
+        self._mmsMessage = message.MMSMessage(noHeaders)
+    
+    def encode(self, mmsMessage):
+        """ Encodes the specified MMS message
+        
+        @param mmsMessage: The MMS message to encode
+        @type mmsMessage: MMSMessage
+        
+        @return: The binary-encoded MMS data, as a sequence of bytes
+        @rtype: array.array('B')
+        """
+        self._mmsMessage = mmsMessage
+        msgData = self.encodeMessageHeader()
+        if self.noHeaders == None:
+               msgData.extend(self.encodeMessageBody())
+        return msgData
+    
+    def encodeMessageHeader(self):
+        """ Binary-encodes the MMS header data.
+        
+        @note: 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
+        fromTypes = {'Address-present-token' : 0x80,
+                     'Insert-address-token'  : 0x81}
+            
+        contentTypes = {'application/vnd.wap.multipart.related' : 0xb3}
+        
+        # Create an array of 8-bit values
+        messageHeader = array.array('B')
+    
+        headersToEncode = self._mmsMessage.headers
+        
+        # If the user added any of these to the message manually (X- prefix), rather use those
+        for hdr in ('X-Mms-Message-Type', 'X-Mms-Transaction-Id', 'X-Mms-Version'):
+            if hdr in headersToEncode:
+                if hdr == 'X-Mms-Version':
+                    cleanHeader = 'MMS-Version'
+                else:
+                    cleanHeader = hdr.replace('X-Mms-', '', 1)
+                headersToEncode[cleanHeader] = headersToEncode[hdr]
+                del headersToEncode[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 headersToEncode:
+            # Default to 'm-retrieve-conf'; we don't need a To/CC field for this
+            # (see WAP-209, section 6.3, table 5)
+            headersToEncode['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 he/she is doing...
+        if headersToEncode['Message-Type'] == 'm-send-req':
+            foundDestAddress = False
+            for addressType in ('To', 'Cc', 'Bc'):
+                if addressType in headersToEncode:
+                    foundDestAddress = True
+                    break
+            if not foundDestAddress:
+                headersToEncode['Message-Type'] = 'm-retrieve-conf'
+        ### End of Message-Type verification
+        
+        ### Start of Transaction-Id verification
+        if 'Transaction-Id' not in headersToEncode:
+            import random
+            headersToEncode['Transaction-Id'] = str(random.randint(1000, 9999))
+        ### End of Transaction-Id verification
+        
+        ### Start of MMS-Version verification
+        if 'MMS-Version' not in headersToEncode:
+            headersToEncode['MMS-Version'] = '1.0'
+        
+        messageType = headersToEncode['Message-Type']
+        
+        # Encode the first three headers, in correct order
+        for hdr in ('Message-Type', 'Transaction-Id', 'MMS-Version'):
+            messageHeader.extend(MMSEncoder.encodeHeader(hdr, headersToEncode[hdr]))
+            del headersToEncode[hdr]
+    
+        # Encode all remaining MMS message headers, except "Content-Type"
+        # -- this needs to be added last, according [2] and [4]
+        for hdr in headersToEncode:
+            if hdr == 'Content-Type':
+                continue
+            messageHeader.extend(MMSEncoder.encodeHeader(hdr, headersToEncode[hdr]))
+        
+        # Ok, now only "Content-type" should be left
+        # No content-type if it's a notifyresp-ind
+        if messageType != 'm-notifyresp-ind' and messageType != 'm-acknowledge-ind':
+               ctType = headersToEncode['Content-Type'][0]
+               ctParameters = headersToEncode['Content-Type'][1]
+               messageHeader.extend(MMSEncoder.encodeMMSFieldName('Content-Type'))
+               #print (ctType, ctParameters)
+               messageHeader.extend(MMSEncoder.encodeContentTypeValue(ctType, ctParameters))
+        return messageHeader
+    
+    def encodeMessageBody(self):
+        """ Binary-encodes the MMS body data.
+        
+        @note: The MMS body is of type C{application/vnd.wap.multipart}
+        (C{mixed} or C{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
+         -------      -------        -----------
+         nEntries     Uintvar        number of entries in the multipart entity
+        
+        The MMS body's multipart entries structure::
+         name             type                   purpose
+         -------          -----                  -----------
+         HeadersLen       Uintvar                length of the ContentType and 
+                                                 Headers fields combined
+         DataLen          Uintvar                length of the Data field
+         ContentType      Multiple octets        the content type of the data
+         Headers          (<HeadersLen> 
+                           - length of 
+                          <ContentType>) octets  the part's headers
+         Data             <DataLen> octets       the part's data
+        
+        @note: The MMS body's header should not be confused with the actual
+               MMS header, as returned by C{_encodeHeader()}.
+        
+        @note: 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.
+               
+        @return: The binary-encoded MMS PDU body, as an array of bytes
+        @rtype: array.array('B')
+        """
+
+        messageBody = array.array('B')
+        
+        #TODO: enable encoding of MMSs without SMIL file
+        ########## MMS body: header ##########
+        # Parts: SMIL file + <number of data elements in each slide>
+        nEntries = 1
+        for page in self._mmsMessage._pages:
+            nEntries += page.numberOfParts()
+        for dataPart in self._mmsMessage._dataParts:
+            nEntries += 1
+            
+        messageBody.extend(self.encodeUintvar(nEntries))
+        
+        ########## MMS body: entries ##########
+        # For every data "part", we have to add the following sequence:
+        # <length of content-type + other possible headers>,
+        # <length of data>,
+        # <content-type + other possible headers>,
+        # <data>.
+
+        # Gather the data parts, adding the MMS message's SMIL file
+        smilPart = message.DataPart()
+        smil = self._mmsMessage.smil()
+        smilPart.setData(smil, 'application/smil')
+        # TODO: make this dynamic....
+        #smilPart.headers['Content-ID'] = '<0000>'
+        parts = [smilPart]
+        for slide in self._mmsMessage._pages:
+            for partTuple in (slide.image, slide.audio, slide.text):
+                if partTuple != None:
+                    parts.append(partTuple[0])
+    
+        for part in parts:
+            partContentType = self.encodeContentTypeValue(part.headers['Content-Type'][0], part.headers['Content-Type'][1])
+            encodedPartHeaders = []
+            for hdr in part.headers:
+                if hdr == 'Content-Type':
+                    continue
+                encodedPartHeaders.extend(wsp_pdu.Encoder.encodeHeader(hdr, part.headers[hdr]))
+        
+            # HeadersLen entry (length of the ContentType and Headers fields combined)
+            headersLen = len(partContentType) + len(encodedPartHeaders)
+            messageBody.extend(self.encodeUintvar(headersLen))
+            # DataLen entry (length of the Data field)
+            messageBody.extend(self.encodeUintvar(len(part)))
+            # ContentType entry
+            messageBody.extend(partContentType)
+            # Headers
+            messageBody.extend(encodedPartHeaders)
+            # Data (note: we do not null-terminate this)
+            for char in part.data:
+                messageBody.append(ord(char))
+        return messageBody
+
+    
+    @staticmethod
+    def encodeHeader(headerFieldName, headerValue):
+        """ Encodes a header entry for an MMS message
+        
+        From [4], section 7.1:
+        C{Header = MMS-header | Application-header}
+        C{MMS-header = MMS-field-name MMS-value}
+        C{MMS-field-name = Short-integer}
+        C{MMS-value = Bcc-value | Cc-value | Content-location-value |
+                      Content-type-value | etc}
+                    
+        @raise DecodeError: This uses C{decodeMMSHeader()} and
+                            C{decodeApplicationHeader()}, and will raise this
+                            exception under the same circumstances as
+                            C{decodeApplicationHeader()}. C{byteIter} will
+                            not be modified in this case.
+        
+        @note: 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).
+        
+        @return: The decoded header entry from the MMS, in the format:
+                 (<str:header name>, <str/int/float:header value>)
+        @rtype: tuple
+        """
+        encodedHeader = []
+        # First try encoding the header as a "MMS-header"...
+        for assignedNumber in MMSEncodingAssignments.fieldNames:
+            if MMSEncodingAssignments.fieldNames[assignedNumber][0] == headerFieldName:
+                encodedHeader.extend(wsp_pdu.Encoder.encodeShortInteger(assignedNumber))
+                # Now encode the value
+                expectedType = MMSEncodingAssignments.fieldNames[assignedNumber][1]
+                try:
+                    exec 'encodedHeader.extend(MMSEncoder.encode%s(headerValue))' % expectedType
+                except wsp_pdu.EncodeError, msg:
+                    raise wsp_pdu.EncodeError, 'Error encoding parameter value: %s' % msg
+                except:
+                    print 'A fatal error occurred, probably due to an unimplemented encoding operation'
+                    raise
+                break
+        # See if the "MMS-header" encoding worked
+        if len(encodedHeader) == 0:
+            # ...it didn't. Use "Application-header" encoding
+            encodedHeaderName = wsp_pdu.Encoder.encodeTokenText(headerFieldName)
+            encodedHeader.extend(encodedHeaderName)
+            # Now add the value
+            encodedHeader.extend(wsp_pdu.Encoder.encodeTextString(headerValue))
+        return encodedHeader
+    
+    @staticmethod
+    def encodeMMSFieldName(fieldName):
+        """ Encodes an MMS header field name, using the "assigned values" for
+        well-known MMS headers as specified in [4].
+        
+        From [4], section 7.1:
+        C{MMS-field-name = Short-integer}
+        
+        @raise EncodeError: The specified header field name is not a
+                            well-known MMS header.
+        
+        @param fieldName: The header field name to encode
+        @type fieldName: str
+        
+        @return: The encoded header field name, as a sequence of bytes
+        @rtype: list
+        """
+        encodedMMSFieldName = []
+        for assignedNumber in MMSEncodingAssignments.fieldNames:
+            if MMSEncodingAssignments.fieldNames[assignedNumber][0] == fieldName:
+                encodedMMSFieldName.extend(wsp_pdu.Encoder.encodeShortInteger(assignedNumber))
+                break
+        if len(encodedMMSFieldName) == 0:
+            raise wsp_pdu.EncodeError, 'The specified header field name is not a well-known MMS header field name'
+        return encodedMMSFieldName
+    
+    @staticmethod
+    def encodeFromValue(fromValue=''):
+        """ From [4], section 7.2.11:
+        From-value = Value-length (Address-present-token Encoded-string-value | Insert-address-token )
+        Address-present-token = <Octet 128>
+        Insert-address-token = <Octet 129>
+        
+        @param fromValue: 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 fromValue: str
+        
+        @return: The encoded "From" address value, as a sequence of bytes
+        @rtype: list
+        """
+        encodedFromValue = []
+        if len(fromValue) == 0:
+            valueLength = wsp_pdu.Encoder.encodeValueLength(1)
+            encodedFromValue.extend(valueLength)
+            encodedFromValue.append(129) # Insert-address-token
+        else:
+            encodedAddress = MMSEncoder.encodeEncodedStringValue(fromValue)
+            length = len(encodedAddress) + 1 # the "+1" is for the Address-present-token
+            valueLength = wsp_pdu.Encoder.encodeValueLength(length)
+            encodedFromValue.extend(valueLength)
+            encodedFromValue.append(128) # Address-present-token
+            encodedFromValue.extend(encodedAddress)
+        return encodedFromValue
+
+    @staticmethod
+    def encodeEncodedStringValue(stringValue):
+        """ From [4], section 7.2.9:
+        C{Encoded-string-value = Text-string | Value-length Char-set Text-string}
+        The Char-set values are registered by IANA as MIBEnum value.
+        
+        @param stringValue: The text string to encode
+        @type stringValue: str
+        
+        @note: This function is currently a simple wrappper to
+               C{encodeTextString()}
+        
+        @return: The encoded string value, as a sequence of bytes
+        @rtype: list
+        """
+        return wsp_pdu.Encoder.encodeTextString(stringValue)
+    
+    @staticmethod
+    def encodeMessageTypeValue(messageType):
+        """ Defined in [4], section 7.2.14.
+        
+        @note: Unknown message types are discarded; thus they will be encoded
+               as 0x80 ("m-send-req") by this function
+        
+        @param messageType: The MMS message type to encode
+        @type messageType: str
+
+        @return: The encoded message type, as a sequence of bytes
+        @rtype: list
+        """
+        messageTypes = {'m-send-req' : 0x80, 
+                        'm-send-conf' : 0x81,
+                        'm-notification-ind' : 0x81,
+                        'm-notifyresp-ind' : 0x83,
+                        'm-retrieve-conf' : 0x84,
+                        'm-acknowledge-ind' : 0x85,
+                        'm-delivery-ind' : 0x86}
+        if messageType in messageTypes:
+            return [messageTypes[messageType]]
+        else:
+            return [0x80]
+
+    @staticmethod
+    def encodeStatusValue(statusValue):
+        """ Defined in [4], section 7.2.#
+        
+        Used to encode the "Status" MMS header.
+        
+        @return: The encoded Status-value, or 0x84 ('Unrecognised') if none
+        @rtype: str
+        """
+        
+        statusValues = {'Expired' : 0x80,
+                        'Retrieved' : 0x81,
+                        'Rejected' : 0x82,
+                        'Deferred' : 0x83,
+                        'Unrecognised' : 0x84}
+        
+        if statusValue in statusValues:
+               return [statusValues[statusValue]]
+        else:
+               return [0x84]
\ No newline at end of file
diff --git a/src/mms/wsp_pdu.py b/src/mms/wsp_pdu.py
new file mode 100644 (file)
index 0000000..6ae098a
--- /dev/null
@@ -0,0 +1,1966 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# This library is free software, distributed under the terms of
+# the GNU Lesser General Public License Version 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
+"""
+Original by Francois Aucamp
+Modified by Nick Leppänen Larsson for use in Maemo5/Fremantle on the Nokia N900.
+
+@author: Francois Aucamp C{<faucamp@csir.co.za>}
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license:  GNU Lesser General Public License, version 2
+@note: This is part of the PyMMS library
+
+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.
+
+@note: 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 iterator import PreviewIterator
+#import itertools
+
+
+     
+
+class WSPEncodingAssignments:
+    """ Static class containing the constant values defined in [5] for
+    well-known content types, parameter names, etc.
+    
+    It also defines some function for combining assigned number-tables for
+    specific WSP encoding versions, where appropriate.
+    
+    This is used by both the Encoder and Decoder classes during well-known
+    assigned number lookups (usually these functions have the string
+    C{WellKnown} in their names).
+    
+        - Assigned parameters are stored in a dictionary, C{wkParameters},
+          containing all assigned values for WSP encoding versions 1.1 - 1.4,
+          in the format:
+          C{{<int>assigned number: (<str>name, <str>expected value type)}}
+          A "encoding versioned"-version of this dictionary can be retrieved
+          by calling the C{wellKnowParameters()} function with an appropriate
+          WSP encoding version as parameter.
+        - Assigned content types are stored in a list, C{wkContentTypes}, in
+          order; thus, their index in the list is equal to their assigned
+          value.
+    
+    """
+    wspPDUTypes = {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)
+    wkParameters = {0x00: ('Q', 'QValue'),
+                    0x01: ('Charset', 'WellKnownCharset'),
+                    0x02: ('Level', 'VersionValue'),
+                    0x03: ('Type', 'IntegerValue'),
+                    0x05: ('Name', 'TextString'),
+                    0x06: ('Filename', 'TextString'),
+                    0x07: ('Differences', 'Field-name'),
+                    0x08: ('Padding', 'ShortInteger'),
+                    0x09: ('Type', 'ConstrainedEncoding'), # encoding version 1.2
+                    0x0a: ('Start', 'TextString'),
+                    0x0b: ('Start-info', 'TextString'),
+                    0x0c: ('Comment', 'TextString'), # encoding version 1.3
+                    0x0d: ('Domain', 'TextString'),
+                    0x0e: ('Max-Age', 'DeltaSecondsValue'),
+                    0x0f: ('Path', 'TextString'),
+                    0x10: ('Secure', 'NoValue'),
+                    0x11: ('SEC', 'ShortInteger'), # encoding version 1.4
+                    0x12: ('MAC', 'TextValue'),
+                    0x13: ('Creation-date', 'DateValue'),
+                    0x14: ('Modification-date', 'DateValue'),
+                    0x15: ('Read-date', 'DateValue'),
+                    0x16: ('Size', 'IntegerValue'),
+                    0x17: ('Name', 'TextValue'),
+                    0x18: ('Filename', 'TextValue'),
+                    0x19: ('Start', 'TextValue'),
+                    0x1a: ('Start-info', 'TextValue'),
+                    0x1b: ('Comment', 'TextValue'),
+                    0x1c: ('Domain', 'TextValue'),
+                    0x1d: ('Path', 'TextValue'),
+                    0x40: ('Content-ID', 'QuotedString')}
+    
+    # Content type assignments ([5], table 40)
+    wkContentTypes = ['*/*', '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 {<assinged_number> : <charset>}
+    # 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
+    wkCharSets = {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)
+    hdrFieldNames = ['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 hdrFieldNames table (same as well known parameter assignments)
+    # Temporary fix to allow different types of header field values to be dynamically decoded
+    hdrFieldEncodings = {'Accept': 'AcceptValue',
+                         'Pragma': 'PragmaValue',
+                         'Content-ID': 'QuotedString'}
+
+    @staticmethod
+    def wellKnownParameters(encodingVersion = '1.2'):
+        """ Formats list of assigned values for well-known parameter names,
+        for the specified WSP encoding version.
+        
+        @param encodingVersion: 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 encodingVersion: 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:
+                 C{{<int:assigned_number> : (<str:param_name>, <str:expected_type>)}}
+        @rtype: dict
+        """
+        if encodingVersion not in ('1.1', '1.2', '1.3', '1.4'):
+            raise ValueError, 'encodingVersion must be "1.1", "1.2", "1.3" or "1.4"'
+        else:
+            version = int(encodingVersion.split('.')[1])
+        wkVersionedParameters = dict(WSPEncodingAssignments.wkParameters)
+        if version <= 3:
+            for assignedNumber in range(0x11, 0x1e):
+                del wkVersionedParameters[assignedNumber]
+        if version <= 2:
+            for assignedNumber in range(0x0c, 0x11):
+               del wkVersionedParameters[assignedNumber]
+        if version == 1:
+            for assignedNumber in range(0x09, 0x0c):
+                del wkVersionedParameters[assignedNumber]
+        return wkVersionedParameters
+    
+    @staticmethod
+    def headerFieldNames(encodingVersion = '1.2'):
+        """ Formats list of assigned values for header field names, for the
+        specified WSP encoding version.
+        
+        @param encodingVersion: 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 encodingVersion: 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 encodingVersion not in ('1.1', '1.2', '1.3', '1.4'):
+            raise ValueError, 'encodingVersion must be "1.1", "1.2", "1.3" or "1.4"'
+        else:
+            version = int(encodingVersion.split('.')[1])
+        versionedHdrFieldNames = list(WSPEncodingAssignments.hdrFieldNames)
+        ### TODO: uncomment and fix 
+        """if version == 3:
+            versionedHdrFieldNames = versionedHdrFieldNames[:0x44]
+        elif version == 2:
+            versionedHdrFieldNames = versionedHdrFieldNames[:0x38]
+        elif version == 1:
+            versionedHdrFieldNames = versionedHdrFieldNames[:0x2f]"""
+        return versionedHdrFieldNames
+
+
+class DecodeError(Exception):
+    """ The decoding operation failed; most probably due to an invalid byte in
+    the sequence provided for decoding """
+    
+class EncodeError(Exception):
+    """ The encoding operation failed; most probably due to an invalid value
+    provided for encoding """
+
+class Decoder:
+    """ A WSP Data unit decoder """
+    @staticmethod
+    def decodeUint8(byteIter):
+        """ Decodes an 8-bit unsigned integer from the byte pointed to by
+        C{byteIter.next()}
+    
+        @note: this function will move the iterator passed as C{byteIter} one
+               byte forward.
+        
+        @param byteIter: an iterator over a sequence of bytes
+        @type byteIteror: iter
+        
+        @return: the decoded 8-bit unsigned integer
+        @rtype: int
+        """
+        # Make the byte unsigned
+        return byteIter.next() & 0xff
+    @staticmethod
+    def decodeUintvar(byteIter):
+        """ Decodes the variable-length unsigned integer starting at the
+        byte pointed to by C{byteIter.next()}
+        
+        See C{wsp.Encoder.encodeUintvar()} for a detailed description of the
+        encoding scheme used for C{Uintvar} sequences.
+       
+        @note: this function will move the iterator passed as C{byteIter} to
+               the last octet in the uintvar sequence; thus, after calling
+               this, that iterator's C{next()} function will return the first
+               byte B{after}the uintvar sequence.
+        
+        @param byteIter: an iterator over a sequence of bytes
+        @type byteIteror: iter
+        
+        @return: the decoded unsigned integer
+        @rtype: int
+        """
+        uint = 0 
+        byte = byteIter.next()
+        while (byte >> 7) == 0x01:
+            uint = uint << 7
+            uint |= byte & 0x7f
+            byte = byteIter.next()
+        uint = uint << 7
+        uint |= byte & 0x7f
+        return uint
+
+
+    @staticmethod
+    def decodeShortInteger(byteIter):
+        """ Decodes the short-integer value starting at the byte pointed to
+        by C{byteIter.next()}.
+        
+        The encoding for a long integer is specified in [5], section 8.4.2.1:
+            C{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.
+                            C{byteIter} will not be modified if this is raised
+        
+        @return: The decoded short integer
+        @rtype: int
+        """
+        byte = byteIter.preview()
+        if not byte & 0x80:
+            byteIter.resetPreview()
+            raise DecodeError, 'Not a valid short-integer: most significant bit not set'
+        byte = byteIter.next()
+        return byte & 0x7f
+    
+    @staticmethod
+    def decodeShortIntegerFromByte(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 most significant
+                            isn't set to 1.
+        @return: The decoded short integer
+        @rtype: int
+        """
+        if not byte & 0x80:
+            raise DecodeError, 'Not a valid short-integer: most significant bit not set'
+        return byte & 0x7f
+
+    @staticmethod
+    def decodeLongInteger(byteIter):
+        """ Decodes the long integer value starting at the byte pointed to
+        by C{byteIter.next()}.
+        
+        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     <Short-length> bytes
+        
+         The Short-length indicates the length of the Multi-octet-integer.
+
+        @raise DecodeError: The byte pointed to by C{byteIter.next()} does
+                            not indicate the start of a valid long-integer
+                            sequence (short-length is invalid). If this is
+                            raised, the iterator passed as C{byteIter} will
+                            not be modified.
+    
+        @note: If this function returns successfully, it will move the
+               iterator passed as C{byteIter} to the last octet in the encoded
+               long integer sequence; thus, after calling this, that
+               iterator's C{next()} function will return the first byte
+               B{after}the encoded long integer sequence.
+               
+        @param byteIter: an iterator over a sequence of bytes
+        @type byteIteror: iter
+        
+        @return: The decoded long integer
+        @rtype: int
+        """
+        try:
+            shortLength = Decoder.decodeShortLength(byteIter)
+        except DecodeError:
+            raise DecodeError, 'Not a valid long-integer: short-length byte is invalid'
+        longInt = 0
+        # Decode the Multi-octect-integer
+        for i in range(shortLength):
+            longInt = longInt << 8
+            longInt |= byteIter.next()
+        return longInt
+
+    @staticmethod
+    def decodeTextString(byteIter):
+        """ Decodes the null-terminated, binary-encoded string value starting
+        at the byte pointed to by C{dataIter.next()}.
+        
+        This follows the basic encoding rules specified in [5], section
+        8.4.2.1
+        
+        @note: this function will move the iterator passed as C{byteIter} to
+               the last octet in the encoded string sequence; thus, after
+               calling this, that iterator's C{next()} function will return
+               the first byte B{after}the encoded string sequence.
+               
+        @param byteIter: an iterator over a sequence of bytes
+        @type byteIteror: iter
+        
+        @return: The decoded text string
+        @rtype: str
+        """
+        decodedString = ''
+        byte = byteIter.next()
+        # Remove Quote character (octet 127), if present
+        if byte == 127:
+            byte = byteIter.next()
+        while byte != 0x00:
+            decodedString += chr(byte)
+            byte = byteIter.next()
+        return decodedString
+    
+    @staticmethod
+    def decodeQuotedString(byteIter):
+        """ From [5], section 8.4.2.1:
+        Quoted-string = <Octet 34> *TEXT End-of-string
+        The TEXT encodes an RFC2616 Quoted-string with the enclosing
+        quotation-marks <"> removed
+        
+        @return: The decoded text string
+        @rtype: str
+        """
+#        byteIter, localIter = itertools.tee(byteIter)
+        # look for the quote character
+        byte = byteIter.preview()
+        if byte != 34:
+            byteIter.resetPreview()
+            raise DecodeError, 'Invalid quoted string; must start with <octect 34>'
+        else:
+            byteIter.next()
+            # CHECK: should the quotation chars be pre- and appended before returning/
+            # *technically* we should not check for quote characters. oh well.
+            return Decoder.decodeTextString(byteIter)
+
+    
+    @staticmethod
+    def decodeTokenText(byteIter):
+        """ From [5], section 8.4.2.1:
+        Token-text = Token End-of-string
+        
+        @raise DecodeError: invalid token; in this case, byteIter is not modified
+                 
+        @return: The token string if successful, or the byte that was read if not
+        @rtype: str or int
+        """
+        separators = (11, 32, 40, 41, 44, 47, 58, 59, 60, 61, 62, 63, 64, 91,
+                      92, 93, 123, 125)
+        token = ''
+#        byteIter, localIter = itertools.tee(byteIter)
+#        byte = localIter.next()
+        byte = byteIter.preview()
+        if byte <= 31 or byte in separators:
+            byteIter.resetPreview()
+            raise DecodeError, 'Invalid token'
+        byte = byteIter.next()
+        while byte > 31 and byte not in separators:
+            token += chr(byte)
+            byte = byteIter.next()
+        return token
+    
+    @staticmethod
+    def decodeExtensionMedia(byteIter):
+        """ From [5], section 8.4.2.1:
+        Extension-media = *TEXT End-of-string
+        This encoding is used for media values, which have no well-known
+        binary encoding
+        
+        @raise DecodeError: The TEXT started with an invalid character.
+                            C{byteIter} is not modified if this happens.
+        
+        @return: The decoded media type value
+        @rtype: str
+        """
+        mediaValue = ''
+#        byteIter, localIter = itertools.tee(byteIter)
+#        byte = localIter.next()
+        byte = byteIter.preview()
+        if byte < 32 or byte == 127:
+            byteIter.resetPreview()
+            raise DecodeError, 'Invalid Extension-media: TEXT starts with invalid character: %d' % byte
+        byte = byteIter.next()
+        while byte != 0x00:
+            mediaValue += chr(byte)
+            byte = byteIter.next()
+        return mediaValue
+
+
+    @staticmethod
+    def decodeConstrainedEncoding(byteIter):
+        """ 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
+        #backupIter, localIter = itertools.tee(byteIter)
+        try:
+            #byteIter, localIter = itertools.tee(byteIter)
+            # First try and see if this is just a short-integer
+            result = Decoder.decodeShortInteger(byteIter)
+            #byteIter = localIter
+        except DecodeError, msg:
+            # Ok, it should be Extension-Media then
+            try:
+                #backupIter, localIter = itertools.tee(byteIter)
+                result = Decoder.decodeExtensionMedia(byteIter)
+            except DecodeError, msg:
+                # Give up
+                #fakeByte =localIter.next()
+                #fakeByte= localIter.next()
+                #fakeByte = localIter.next()
+                #byte = byteIter.next()
+                #byte = byteIter.next()
+                raise DecodeError, 'Not a valid Constrained-encoding sequence'
+        #byteIter = localIter
+        return result
+
+    @staticmethod
+    def decodeShortLength(byteIter):
+        """ From [5], section 8.4.2.2:
+        Short-length = <Any octet 0-30>
+        
+        @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 C{byteIter} is not modified.
+        
+        @note: If this function returns successfully, the iterator passed as
+               C{byteIter} is moved one byte forward.
+        
+        @return The decoded short-length
+        @rtype: int
+        """
+#        byteIter, localIter = itertools.tee(byteIter)
+        # Make sure it's a valid short-length
+#        byte = localIter.next()
+        byte = byteIter.preview()
+        if byte > 30:
+            byteIter.resetPreview()
+            raise DecodeError, 'Not a valid short-length; should be in octet range 0-30'
+        else:
+            return byteIter.next()
+
+    @staticmethod
+    def decodeValueLength(byteIter):
+        """ Decodes the value length indicator starting at the byte pointed to
+        by C{byteIter.next()}.
+        
+        "Value length" is used to indicate the length of a value to follow, as
+        used in the C{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
+                       <Any octet 0-30>          <Octet 31>   Uintvar-integer
+                       
+        @raise DecodeError: The ValueLength could not be decoded. If this
+                            happens, C{byteIter} is not modified.
+        
+        @return: The decoded value length indicator
+        @rtype: int
+        """
+        lengthValue = 0      
+        # Check for short-length
+        try:
+            lengthValue = Decoder.decodeShortLength(byteIter)
+        except DecodeError:
+            byte = byteIter.preview()
+            #CHECK: this strictness MAY cause issues, but it is correct
+            if byte == 31:
+                byteIter.next() # skip past the length-quote
+                lengthValue = Decoder.decodeUintvar(byteIter)
+            else:
+                byteIter.resetPreview()
+                raise DecodeError, 'Invalid Value-length: not short-length, and no length-quote present'
+        return lengthValue
+    
+    @staticmethod
+    def decodeIntegerValue(byteIter):
+        """ From [5], section 8.4.2.3:
+        Integer-Value = Short-integer | Long-integer
+        
+        @raise DecodeError: The sequence of bytes starting at
+                            C{byteIter.next()} does not contain a valid
+                            integervalue. If this is raised, the iterator
+                            passed as C{byteIter} is not modified.
+        
+        @note: If successful, this function will move the iterator passed as
+               C{byteIter} to the last octet in the integer value sequence;
+               thus, after calling this, that iterator's C{next()} function
+               will return the first byte B{after}the integer value sequence.
+        
+        @return: The decoded integer value
+        @rtype: int
+        """
+        integer = 0
+        # First try and see if it's a short-integer
+        try:
+            integer = Decoder.decodeShortInteger(byteIter)
+        except DecodeError:
+            try:
+                integer = Decoder.decodeLongInteger(byteIter)
+            except DecodeError:
+                raise DecodeError, 'Not a valid integer value'
+        return integer
+    
+    @staticmethod
+    def decodeContentTypeValue(byteIter):
+        """ Decodes an encoded content type value.
+
+        From [5], section 8.4.2.24:
+        C{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:
+                 C{{<str:parameter_name>: <str/int/float:parameter_value>}}.
+                 The final returned tuple is in the format:
+                 (<str:media_type>, <dict:parameter_dict>)
+        @rtype: tuple
+        """
+        # First try do decode it as Constrained-media
+        contentType = ''
+        parameters = {}
+        try:
+            contentType = Decoder.decodeConstrainedMedia(byteIter)
+        except DecodeError:
+            # Try the general form
+            contentType, parameters = Decoder.decodeContentGeneralForm(byteIter)
+        return (contentType, parameters)
+
+
+    @staticmethod
+    def decodeWellKnownMedia(byteIter):
+        """ 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 byteIter: an iterator over a sequence of bytes
+        @type byteIteror: 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 C{byteIter} is not modified.
+        
+        @note: If successful, this function will move the iterator passed as
+               C{byteIter} to the last octet in the content type value
+               sequence; thus, after calling this, that iterator's C{next()}
+               function will return the first byte B{after}the content type
+               value sequence.
+        
+        @return: the decoded MIME content type name
+        @rtype: str
+        """
+#        byteIter, localIter = itertools.tee(byteIter)
+        try:
+#            wkContentTypeValue = Decoder.decodeIntegerValue(localIter)
+            wkContentTypeValue = Decoder.decodeIntegerValue(byteIter)
+        except DecodeError:
+            raise DecodeError, 'Invalid well-known media: could not read integer value representing it' 
+  
+        if wkContentTypeValue in range(len(WSPEncodingAssignments.wkContentTypes)):
+            decodedContentType = WSPEncodingAssignments.wkContentTypes[wkContentTypeValue]
+#            # Only iterate the main iterator now that everything is ok
+#            byteIter.next()
+        else:
+            raise DecodeError, 'Invalid well-known media: could not find content type in table of assigned values'
+        return decodedContentType
+
+
+    @staticmethod
+    def decodeMediaType(byteIter):
+        """ From [5], section 8.2.4.24:
+        Media-type = (Well-known-media | Extension-Media) *(Parameter)
+        
+        @param byteIter: an iterator over a sequence of bytes
+        @type byteIteror: iter
+        
+        @note: Used by C{decodeContentGeneralForm()}
+        
+        @return: The decoded media type
+        @rtype: str
+        """
+        try:
+            mediaType = Decoder.decodeWellKnownMedia(byteIter)
+        except DecodeError:
+            mediaType = Decoder.decodeExtensionMedia(byteIter)
+        return mediaType
+
+    @staticmethod
+    def decodeConstrainedMedia(byteIter):
+        """ From [5], section 8.4.2.7:
+        Constrained-media = Constrained-encoding
+        It is encoded using values from the "Content Type Assignments" table.
+        
+        @raise DecodeError: Invalid constrained media sequence
+        
+        @return: The decoded media type
+        @rtype: str
+        """
+        constrainedMedia = ''
+        try:
+            constrainedMediaValue = Decoder.decodeConstrainedEncoding(byteIter)
+        except DecodeError, msg:
+            #byte = byteIter.next()
+            raise DecodeError, 'Invalid Constrained-media: %s' % msg
+        if type(constrainedMediaValue) == int:
+            if constrainedMediaValue in range(len(WSPEncodingAssignments.wkContentTypes)):
+                constrainedMedia = WSPEncodingAssignments.wkContentTypes[constrainedMediaValue]
+            else:
+                raise DecodeError, 'Invalid constrained media: could not find well-known content type'
+        else:
+            constrainedMedia = constrainedMediaValue
+        return constrainedMedia
+
+    @staticmethod
+    def decodeContentGeneralForm(byteIter):
+        """ From [5], section 8.4.2.24:
+        Content-general-form = Value-length Media-type
+        
+        @note Used in decoding Content-type fields and their parameters;
+              see C{decodeContentTypeValue}
+        
+        @note: Used by C{decodeContentTypeValue()}
+        
+        @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:
+                 C{{<str:parameter_name>: <str/int/float:parameter_value>}}.
+                 The final returned tuple is in the format:
+                 (<str:media_type>, <dict:parameter_dict>)
+        @rtype: tuple
+        """
+        # This is the length of the (encoded) media-type and all parameters
+        #try:
+        valueLength = Decoder.decodeValueLength(byteIter)
+        #except DecodeError:
+            #CHECK: this is being very leniet, based on real-world tests (specs don't mention this):
+        #    valueLength = Decoder.decodeIntegerValue(byteIter)
+
+        # Read parameters, etc, until <valueLength> is reached
+        ctFieldBytes = array.array('B')
+        for i in range(valueLength):
+            ctFieldBytes.append(byteIter.next())
+#        contentTypeIter = iter(ctFieldBytes)
+        ctIter = PreviewIterator(ctFieldBytes)
+        # Now, decode all the bytes read
+        mediaType = Decoder.decodeMediaType(ctIter)
+        # Decode the included paramaters (if any)
+        parameters = {}
+        while True:
+            try:
+                parameter, value = Decoder.decodeParameter(ctIter)
+                parameters[parameter] = value
+            except StopIteration:
+                break
+        return (mediaType, parameters)
+    
+    @staticmethod
+    def decodeParameter(byteIter):
+        """ From [5], section 8.4.2.4:
+        Parameter = Typed-parameter | Untyped-parameter
+        
+        @return: The name of the parameter, and its value, in the format:
+                 (<parameter name>, <parameter value>)
+        @rtype: tuple
+        """
+        try:
+            parameter, value = Decoder.decodeTypedParameter(byteIter)
+        except DecodeError:
+            parameter, value = Decoder.decodeUntypedParameter(byteIter)
+        return (parameter, value)
+
+    @staticmethod
+    def decodeTypedParameter(byteIter):
+        """ From [5], section 8.4.2.4:
+        C{Typed-parameter = Well-known-parameter-token Typed-value}
+        The actual expected type of the value is implied by the well-known
+        parameter.
+        
+        @note: This is used in decoding parameters; see C{decodeParameter}
+        
+        @return: The name of the parameter, and its value, in the format:
+                 (<parameter name>, <parameter value>)
+        @rtype: tuple
+        """
+        parameterToken, expectedValueType = Decoder.decodeWellKnownParameter(byteIter)
+        typedValue = ''
+        try:
+            # Split the iterator; sometimes the exec call seems to mess up with itertools if this not done here
+            # (to replicate: trace the program from here to decodeShortInteger(); the itertools.tee command there
+            # doesn't copy the iterator as it should - it creates pointers to the same memory)
+            #byteIter, execIter = itertools.tee(byteIter)
+            exec 'typedValue = Decoder.decode%s(byteIter)' % expectedValueType
+        except DecodeError, msg:
+            raise DecodeError, 'Could not decode Typed-parameter: %s' % msg
+        except:
+            print 'A fatal error occurred, probably due to an unimplemented decoding operation'
+            raise
+        return (parameterToken, typedValue)
+    
+    @staticmethod
+    def decodeUntypedParameter(byteIter):
+        """ From [5], section 8.4.2.4:
+        C{Untyped-parameter = Token-text Untyped-value}
+        The type of the value is unknown, but it shall be encoded as an
+        integer, if that is possible.
+        
+        @note: This is used in decoding parameters; see C{decodeParameter}
+        
+        @return: The name of the parameter, and its value, in the format:
+                 (<parameter name>, <parameter value>)
+        @rtype: tuple 
+        """
+        parameterToken = Decoder.decodeTokenText(byteIter)
+        parameterValue = Decoder.decodeUntypedValue(byteIter)
+        return (parameterToken, parameterValue)
+
+    @staticmethod
+    def decodeUntypedValue(byteIter):
+        """ From [5], section 8.4.2.4:
+        Untyped-value = Integer-value | Text-value
+        
+        @note: This is used in decoding parameter values; see
+               C{decodeUntypedParameter}
+        @return: The decoded untyped-value
+        @rtype: int or str
+        """
+        try:
+            value = Decoder.decodeIntegerValue(byteIter)
+        except DecodeError:
+            value = Decoder.decodeTextValue(byteIter)
+        return value
+
+    @staticmethod
+    def decodeWellKnownParameter(byteIter, encodingVersion='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].
+        
+        From [5], section 8.4.2.4:
+        Well-known-parameter-token = Integer-value
+        The code values used for parameters are specified in [5], table 38
+        
+        @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 C{byteIter} is not modified.
+
+        @param encodingVersion: 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 encodingVersion: str
+        
+        @return: the decoded parameter name, and its expected value type, in
+                 the format (<parameter name>, <expected type>)
+        @rtype: tuple
+        """
+        decodedParameterName = ''
+        expectedValue = ''
+#        byteIter, localIter = itertools.tee(byteIter)
+        try:
+#            wkParameterValue = Decoder.decodeIntegerValue(localIter)
+            wkParameterValue = Decoder.decodeIntegerValue(byteIter)
+        except DecodeError:
+            raise DecodeError, 'Invalid well-known parameter token: could not read integer value representing it'
+                
+        wkParameters = WSPEncodingAssignments.wellKnownParameters(encodingVersion)
+        if wkParameterValue in wkParameters:
+            decodedParameterName, expectedValue = wkParameters[wkParameterValue]
+            # Only iterate the main iterator now that everything is ok
+#            byteIter.next()
+        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)' % encodingVersion
+        return (decodedParameterName, expectedValue)
+
+    #TODO: somehow this should be more dynamic; we need to know what type is EXPECTED (hence the TYPED value)
+    @staticmethod
+    def decodeTypedValue(byteIter):
+        """ From [5], section 8.4.2.4:
+        Typed-value = Compact-value | Text-value
+        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.
+        
+        @note This is used in decoding parameters, see C{decodeParameter()}
+        
+        @return: The decoded Parameter Typed-value
+        @rtype: str
+        """
+        typedValue = ''
+        try:
+            typedValue = Decoder.decodeCompactValue(byteIter)
+        except DecodeError:
+            try:
+                typedValue = Decoder.decodeTextValue(byteIter)
+            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 decodeCompactValue(byteIter):
+        """ 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, C{byteIter} is unmodified
+
+        @note This is used in decoding parameters, see C{decodeTypeValue()}
+        """
+        compactValue = 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
+            compactValue = Decoder.decodeIntegerValue(byteIter)
+        except DecodeError:
+            try:
+                # Try parsing it as a Uri-value
+                compactValue = Decoder.decodeUriValue(byteIter)
+            except DecodeError:
+                raise DecodeError, 'Could not decode Parameter Compact-value'
+        return compactValue 
+
+    #TODO: the string output from this should be in the MMS format..?
+    @staticmethod
+    def decodeDateValue(byteIter):
+        """ From [5], section 8.4.2.3:
+        Date-value = Long-integer
+        The encoding of dates shall be done in number of seconds from
+        1970-01-01, 00:00:00 GMT.
+
+        @raise DecodeError: This method uses C{decodeLongInteger}, and thus
+                            raises this under the same conditions.
+
+        @return The date, in a format such as: C{Tue Nov 27 16:12:21 2007}
+        @rtype: str
+        """
+        import time
+        return time.ctime(Decoder.decodeLongInteger(byteIter))
+    
+    @staticmethod
+    def decodeDeltaSecondsValue(byteIter):
+        """ From [5], section 8.4.2.3:
+        Delta-seconds-value = Integer-value
+        @raise DecodeError: This method uses C{decodeIntegerValue}, and thus
+                            raises this under the same conditions.
+        @return the decoded delta-seconds-value
+        @rtype: int
+        """
+        return Decoder.decodeIntegerValue(byteIter)
+
+    @staticmethod
+    def decodeQValue(byteIter):
+        """ From [5], section 8.4.2.1:
+        The encoding is the same as in Uintvar-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
+        """
+        qValue = 0.0
+        qValueInt = Decoder.decodeUintvar(byteIter)
+        #TODO: limit the amount of decimal points
+        if qValueInt > 100:
+            qValue = float(qValueInt - 100) / 1000.0
+        else:
+            qValue = float(qValueInt - 1) / 100.0
+        return qValue
+
+
+    @staticmethod
+    def decodeVersionValue(byteIter):
+        """ 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: "<major_version>.<minor_version>"
+        @rtype: str
+        """
+        version = ''
+        try:
+            byteValue = Decoder.decodeShortInteger(byteIter)
+            major = (byteValue & 0x70) >> 4
+            minor = byteValue & 0x0f
+            version = '%d.%d' % (major, minor)
+        except DecodeError:
+            version = Decoder.decodeTextString(byteIter)        
+        return version
+
+    @staticmethod
+    def decodeUriValue(byteIter):
+        """ Stub for Uri-value decoding; this is a wrapper to C{decodeTextString} """
+        return Decoder.decodeTextString(byteIter)
+
+    @staticmethod
+    def decodeTextValue(byteIter):
+        """ Stub for Parameter Text-value decoding.
+        From [5], section 8.4.2.3:
+        Text-value = No-value | Token-text | Quoted-string
+        
+        This is used when decoding parameter values; see C{decodeTypedValue()}
+        
+        @return: The decoded Parameter Text-value
+        @rtype: str
+        """
+        textValue = ''
+        try:
+            textValue = Decoder.decodeTokenText(byteIter)
+        except DecodeError:
+            try:
+                textValue = Decoder.decodeQuotedString(byteIter)
+            except DecodeError:
+                # Ok, so it's a "No-value"
+                pass
+        return textValue
+
+    @staticmethod
+    def decodeNoValue(byteIter):
+        """ Basically verifies that the byte pointed to by C{byteIter.next()}
+        is 0x00.
+        
+        @note: If successful, this function will move C{byteIter} one byte
+               forward.
+        
+        @raise DecodeError: If 0x00 is not found; C{byteIter} is not modified
+                            if this is raised.
+        
+        @return: No-value, which is 0x00
+        @rtype: int
+        """
+        byteIter, localIter = byteIter.next()
+        if localIter.next() != 0x00:
+            raise DecodeError, 'Expected No-value'
+        else:
+            byteIter.next()
+        return 0x00
+    
+    @staticmethod
+    def decodeAcceptValue(byteIter):
+        """ 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 = <Octet 128>
+
+        @note: most of these things are currently decoded, but discarded (e.g
+               accept-parameters); we only return the media type
+
+        @raise DecodeError: The decoding failed. C{byteIter} will not be
+                            modified in this case.
+        @return the decoded Accept-value (media/content type)
+        @rtype: str
+        """
+        acceptValue = ''
+        # Try to use Constrained-media encoding
+        try:
+            acceptValue = Decoder.decodeConstrainedMedia(byteIter)
+        except DecodeError:
+            # ...now try Accept-general-form
+            valueLength = Decoder.decodeValueLength(byteIter)
+            try:
+                media = Decoder.decodeWellKnownMedia(byteIter)
+            except DecodeError:
+                media = Decoder.decodeExtensionMedia(byteIter)
+            # Check for the Q-Token (to see if there are Accept-parameters)
+            if byteIter.preview() == 128:
+                byteIter.next()
+                qValue = Decoder.decodeQValue(byteIter)
+                try:
+                    acceptExtension = Decoder.decodeParameter(byteIter)
+                except DecodeError:
+                    # Just set an empty iterable
+                    acceptExtension = []
+            byteIter.resetPreview()
+            acceptValue = media
+        return acceptValue
+    
+    @staticmethod
+    def decodePragmaValue(byteIter):
+        """ Defined in [5], section 8.4.2.38:
+        
+            Pragma-value = No-cache | (Value-length Parameter)
+        
+        From [5], section 8.4.2.15:
+        
+            No-cache = <Octet 128> 
+        
+        @raise DecodeError: The decoding failed. C{byteIter} will not be
+                            modified in this case.
+        @return: the decoded Pragma-value, in the format:
+                 (<parameter name>, <parameter value>)
+        @rtype: tuple
+        """
+        byte = byteIter.preview()
+        if byte == 0x80: # No-cache
+            byteIter.next()
+            #TODO: Not sure if this parameter name (or even usage) is correct
+            parameterName = 'Cache-control'
+            parameterValue = 'No-cache'
+        else:
+            byteIter.resetPreview()
+            valueLength = Decoder.decodeValueLength(byteIter)
+            parameterName, parameterValue = Decoder.decodeParameter(byteIter)
+        return parameterName, parameterValue
+    
+    @staticmethod
+    def decodeWellKnownCharset(byteIter):
+        """ From [5], section 8.4.2.8:
+        C{Well-known-charset = Any-charset | Integer-value}
+        It is encoded using values from "Character Set Assignments" table.
+        C{Any-charset = <Octet 128>}
+        Equivalent to the special RFC2616 charset value "*"
+        """
+        decodedCharSet = ''
+        # Look for the Any-charset value
+        byte = byteIter.preview()
+        byteIter.resetPreview()
+        if byte == 127:
+            byteIter.next()
+            decodcedCharSet = '*'
+        else:
+            charSetValue = Decoder.decodeIntegerValue(byteIter)
+            if charSetValue in WSPEncodingAssignments.wkCharSets:
+                decodedCharSet = WSPEncodingAssignments.wkCharSets[charSetValue]
+            else:
+                # This charset is not in our table... so just use the value (at least for now)
+                decodedCharSet = str(charSetValue)
+        return decodedCharSet
+
+    @staticmethod
+    def decodeWellKnownHeader(byteIter):
+        """ From [5], section 8.4.2.6:
+        C{Well-known-header = Well-known-field-name Wap-value}
+        C{Well-known-field-name = Short-integer}
+        C{Wap-value = <many different headers value, most not implemented>}
+        
+        @todo: Currently, "Wap-value" is decoded as a Text-string in most cases
+        
+        @return: The header name, and its value, in the format:
+                 (<str:header_name>, <str:header_value>)
+        @rtype: tuple
+        """
+        decodedHeaderFieldName = ''
+        hdrFieldValue = Decoder.decodeShortInteger(byteIter)
+        hdrFields = WSPEncodingAssignments.headerFieldNames()
+        #TODO: *technically* this can fail, but then we have already read a byte... should fix?
+        if hdrFieldValue in range(len(hdrFields)):
+            decodedHeaderFieldName = hdrFields[hdrFieldValue]
+        else:
+            raise DecodeError, 'Invalid Header Field value: %d' % hdrFieldValue
+        #TODO: make this flow better, and implement it in decodeApplicationHeader also
+        # Currently we decode most headers as TextStrings, except where we have a specific decoding algorithm implemented
+        if decodedHeaderFieldName in WSPEncodingAssignments.hdrFieldEncodings:
+            wapValueType = WSPEncodingAssignments.hdrFieldEncodings[decodedHeaderFieldName]
+            try:
+                exec 'decodedValue = Decoder.decode%s(byteIter)' % wapValueType
+            except DecodeError, msg:
+                raise DecodeError, 'Could not decode Wap-value: %s' % msg
+            except:
+                print 'An error occurred, probably due to an unimplemented decoding operation. Tried to decode header: %s' % decodedHeaderFieldName
+                raise
+        else:
+            decodedValue = Decoder.decodeTextString(byteIter)
+        return (decodedHeaderFieldName, decodedValue)
+
+    @staticmethod
+    def decodeApplicationHeader(byteIter):
+        """ From [5], section 8.4.2.6:
+        C{Application-header = Token-text Application-specific-value}
+        
+        From [4], section 7.1:
+        C{Application-header = Token-text Application-specific-value}
+        C{Application-specific-value = Text-string}
+        
+        @note: This is used when decoding generic WSP headers;
+               see C{decodeHeader()}.
+        @note: We follow [4], and decode the "Application-specific-value"
+               as a Text-string
+        
+        @return: The application-header, and its value, in the format:
+                 (<str:application_header>, <str:application_specific_value>)
+        @rtype: tuple
+        """
+        try:
+            appHeader = Decoder.decodeTokenText(byteIter)
+        #FNA: added for brute-forcing
+        except DecodeError:
+            appHeader = Decoder.decodeTextString(byteIter)
+        #appSpecificValue = Decoder.decodeTextString(byteIter)
+        try: 
+               appSpecificValue = Decoder.decodeWellKnownHeader(byteIter)
+        except:
+               appSpecificValue = Decoder.decodeTextString(byteIter)
+        return (appHeader, appSpecificValue)
+    
+    @staticmethod
+    def decodeHeader(byteIter):
+        """ Decodes a WSP header entry
+        
+        From [5], section 8.4.2.6:
+        C{Header = Message-header | Shift-sequence}
+        C{Message-header = Well-known-header | Application-header}
+        C{Well-known-header = Well-known-field-name Wap-value}
+        C{Application-header = Token-text Application-specific-value}
+
+        @note: "Shift-sequence" encoding has not been implemented
+        @note: Currently, almost all header values are treated as text-strings
+
+        @return: The decoded headername, and its value, in the format:
+                 (<str:header_name>, <str:header_value>)
+        @rtype: tuple
+        """
+        header = ''
+        value = ''
+        # First try decoding the header as a well-known-header
+        try:
+            header, value = Decoder.decodeWellKnownHeader(byteIter)
+        except DecodeError:
+            # ...now try Application-header encoding
+            header, value = Decoder.decodeApplicationHeader(byteIter)
+        return (header, value)
+
+
+class Encoder:
+    """ A WSP Data unit decoder """
+    
+    #@staticmethod
+    #def encodeUint8(uint):
+    #    """ Encodes an 8-bit unsigned integer
+    #
+    #    @param uint: The integer to encode
+    #    @type byteIteror: int
+    #    
+    #    @return: the encoded Uint8, as a sequence of bytes
+    #    @rtype: list
+    #    """
+    #    # Make the byte unsigned
+    #    return [uint & 0xff]
+    
+    
+    @staticmethod
+    def encodeUintvar(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 Uintvar, as a list of byte values
+        @rtype: list
+        """
+        uintVar = []
+        # Since this is the lowest entry, we do not set the continue bit to 1
+        uintVar.append(uint & 0x7f) 
+        uint = uint >> 7
+        # ...but for the remaining octets, we have to
+        while uint > 0:
+            uintVar.insert(0, 0x80 | (uint & 0x7f))
+            uint = uint >> 7
+        return uintVar
+    
+    @staticmethod
+    def encodeTextString(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
+        """
+        encodedString = []
+       if(string.__class__ == int):
+               string = str(string)
+
+        for char in string:
+            encodedString.append(ord(char))
+        encodedString.append(0x00)
+        return encodedString
+
+    @staticmethod
+    def encodeQuotedString(string):
+        """ Encodes a "Quoted-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
+        """
+        encodedString = []
+       if(string.__class__ == int):
+               string = str(string)
+       encodedString.append(ord('"'))
+        for char in string:
+            encodedString.append(ord(char))
+        encodedString.append(0x00)
+        return encodedString    
+    
+    
+    @staticmethod
+    def encodeShortInteger(integer):
+        """ Encodes the specified short-integer value
+        
+        The encoding for a long integer is specified in [5], section 8.4.2.1:
+            C{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.
+        
+        @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
+        encodedInteger = []
+        # Make sure the most significant bit is set
+        byte = 0x80 | integer
+        encodedInteger.append(byte)
+        return encodedInteger
+    
+    @staticmethod
+    def encodeLongInteger(integer):
+        """ Encodes a Long-integer value 
+        
+        The encoding for a long integer is specified in [5], section 8.4.2.1;
+        for a description of this encoding scheme, see
+        C{wsp.Decoder.decodeLongIntger()}.
+        
+        Basically:      
+        From [5], section 8.4.2.2:
+        Long-integer = Short-length Multi-octet-integer
+        Short-length = <Any octet 0-30>
+        
+        @raise EncodeError: <integer> 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 type(integer) != int:
+            raise EncodeError, '<integer> must be of type "int"'
+        encodedLongInt = []
+        longInt = integer
+        # Encode the Multi-octect-integer
+        while longInt > 0:
+            byte = 0xff & longInt
+            encodedLongInt.append(byte)
+            longInt = longInt >> 8
+        # Now add the SHort-length value, and make sure it's ok
+        shortLength = len(encodedLongInt)
+        if shortLength > 30:
+            raise EncodeError, 'Cannot encode Long-integer value: Short-length is too long; should be in octet range 0-30'
+        encodedLongInt.insert(0, shortLength)       
+        return encodedLongInt
+
+    @staticmethod
+    def encodeVersionValue(version):
+        """ Encodes the version-value. From [5], section 8.4.2.3:
+        Version-value = Short-integer | Text-string
+        
+        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".
+        
+        @param version: The version number to encode, e.g. "1.0"
+        @type version: str
+        
+        @raise TypeError: The specified version value was not of type C{str}
+        
+        @return: the encoded version value, as a list of byte values
+        @rtype: list
+        """
+        if type(version) != str:
+            raise TypeError, 'Parameter must be of type "str"'
+        encodedVersionValue = []
+        # First try short-integer encoding
+        try:
+            if len(version.split('.')) <= 2:
+                majorVersion = int(version.split('.')[0])
+                if majorVersion < 1 or majorVersion > 7:
+                    raise ValueError, 'Major version must be in range 1-7'
+                major = majorVersion << 4                            
+                if len(version.split('.')) == 2:
+                    minorVersion = int(version.split('.')[1])
+                    if minorVersion < 0 or minorVersion > 14:
+                        raise ValueError, 'Minor version must be in range 0-14'
+                else:
+                    minorVersion = 15
+                minor = minorVersion
+                encodedVersionValue = Encoder.encodeShortInteger(major|minor)
+        except:
+            # The value couldn't be encoded as a short-integer; use a text-string instead
+            encodedVersionValue = Encoder.encodeTextString(version)
+        return encodedVersionValue
+
+    @staticmethod
+    def encodeMediaType(contentType):
+        """ Encodes the specified MIME content type ("Media-type" value)
+        
+        From [5], section 8.2.4.24:
+        Media-type = (Well-known-media | Extension-Media) *(Parameter)
+        
+        "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].
+        
+        @param contentType: The MIME content type to encode
+        @type contentType: str
+        
+        @return: The binary-encoded content type, as a list of (integer) byte
+                 values
+        @rtype: list
+        """
+        encodedContentType = []
+        if contentType in WSPEncodingAssignments.wkContentTypes:
+            # Short-integer encoding
+            encodedContentType.extend(Encoder.encodeShortInteger(WSPEncodingAssignments.wkContentTypes.index(contentType)))
+        else:
+            encodedContentType.extend(Encoder.encodeTextString(contentType))
+        return encodedContentType
+
+    @staticmethod
+    def encodeParameter(parameterName, parameterValue, encodingVersion='1.4'):
+        """ Binary-encodes the name of a parameter of (for example) 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:
+        C{Parameter = Typed-parameter | Untyped-parameter}
+        C{Typed-parameter = Well-known-parameter-token Typed-value}
+        C{Untyped-parameter = Token-text Untyped-value}
+        C{Untyped-value = Integer-value | Text-value}
+
+        @param parameterName: The name of the parameter to encode
+        @type parameterName: str
+        @param parameterValue: The value of the parameter
+        @type parameterValue: str or int
+        
+        @param encodingVersion: 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 encodingVersion: str
+        
+        @raise ValueError: The specified encoding version is invalid.
+        
+        @return: The binary-encoded parameter name, as a list of (integer)
+                 byte values
+        @rtype: list
+        """
+        wkParameters = WSPEncodingAssignments.wellKnownParameters(encodingVersion)
+        encodedParameter = []
+        # Try to encode the parameter using a "Typed-parameter" value
+        #print wkParameters.keys()
+        #wkParamNumbers = wkParameters.keys().sort(reverse=True)
+        wkParamNumbers = wkParameters.keys()
+        #print wkParamNumbers
+               #print parameterName, parameterValue
+               #print wkParamNumbers
+        for assignedNumber in wkParamNumbers:
+            if wkParameters[assignedNumber][0] == parameterName:
+                # Ok, it's a Typed-parameter; encode the parameter name
+                if parameterName == 'Type':
+                       assignedNumber = 9
+                       # TODO: remove this ugly hack
+                       encodedParameter.extend(Encoder.encodeShortInteger(assignedNumber))
+                else:
+                       encodedParameter.extend(Encoder.encodeShortInteger(assignedNumber))
+                # ...and now the value
+                expectedType = wkParameters[assignedNumber][1]
+                try:
+                    if parameterName == 'Type':
+                       ### TODO: fix this
+                       try:
+                               exec 'encodedParameter.extend(Encoder.encode%s(parameterValue))' % expectedType
+                       except:
+                               exec 'encodedParameter.extend(Encoder.encode%s(parameterValue))' % 'ConstrainedEncoding'
+                       
+                    else:
+                       exec 'encodedParameter.extend(Encoder.encode%s(parameterValue))' % expectedType
+                except EncodeError, msg:
+                    raise EncodeError, 'Error encoding parameter value: %s' % msg
+                except:
+                    print 'A fatal error occurred, probably due to an unimplemented encoding operation'
+                    raise
+                break
+        # See if the "Typed-parameter" encoding worked
+        if len(encodedParameter) == 0:
+            # ...it didn't. Use "Untyped-parameter" encoding
+            encodedParameter.extend(Encoder.encodeTokenText(parameterName))
+            value = []
+            # First try to encode the untyped-value as an integer
+            try:
+                value = Encoder.encodeIntegerValue(parameterValue)
+            except EncodeError:
+                value = Encoder.encodeTextString(parameterValue)
+            encodedParameter.extend(value)
+        return encodedParameter    
+
+    @staticmethod
+    def encodeWellKnownCharset(value):
+       #print "encoding well known charset:", value
+       wkCharsets = WSPEncodingAssignments.wkCharSets
+       wkCharsetNumber = wkCharsets.keys()
+       for assignedNumber in wkCharsetNumber:
+               if wkCharsets[assignedNumber] == value:
+       #               print "MATCH"
+       #               return assignedNumber
+                       return Encoder.encodeLongInteger(assignedNumber)                
+       #return Encoder.encodeTextString(value)
+
+    #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 encodeTokenText(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)
+        encodedToken = Encoder.encodeTextString(text)
+        return encodedToken
+
+    @staticmethod
+    def encodeIntegerValue(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 <integer> parameter is not of type C{int}
+        
+        @return: The encoded integer value, as a list of byte values
+        @rtype: list
+        """
+        if type(integer) != int:
+            raise EncodeError, '<integer> must be of type "int"'
+        encodedInteger = []
+        # First try and see if it's a short-integer
+        try:
+            encodedInteger = Encoder.encodeShortInteger(integer)
+        except EncodeError:
+            encodedInteger = Encoder.encodeLongInteger(integer)
+        return encodedInteger
+
+    @staticmethod
+    def encodeTextValue(text):
+        """ Stub for encoding Text-values; this is equivalent to
+        C{encodeTextString} """
+        return Encoder.encodeTextString(text)
+
+    @staticmethod
+    def encodeNoValue(value=None):
+        """ Encodes a No-value, which is 0x00
+        
+        @note: This function mainly exists for use by automatically-selected
+               encoding routines (see C{encodeParameter()} for an example.
+        
+        @param value: This value is ignored; it is present so that this
+                      method complies with the format of the other C{encode}
+                      methods.
+        
+        @return: A list containing a single "No-value", which is 0x00
+        @rtype: list
+        """
+        return [0x00]
+    @staticmethod
+    def encodeHeader(headerFieldName, headerValue):
+        """ Encodes a WSP header entry, and its value
+        
+        From [5], section 8.4.2.6:
+        C{Header = Message-header | Shift-sequence}
+        C{Message-header = Well-known-header | Application-header}
+        C{Well-known-header = Well-known-field-name Wap-value}
+        C{Application-header = Token-text Application-specific-value}
+
+        @note: "Shift-sequence" encoding has not been implemented
+        @note: Currently, almost all header values are encoded as text-strings
+
+        @return: The encoded header, and its value, as a sequence of byte
+                 values
+        @rtype: list
+        """
+        encodedHeader = []
+        # First try encoding the header name as a "well-known-header"...
+        wkHdrFields = WSPEncodingAssignments.headerFieldNames()
+        if headerFieldName in wkHdrFields:
+            headerFieldValue = Encoder.encodeShortInteger(wkHdrFields.index(headerFieldName))
+            encodedHeader.extend(headerFieldValue)
+        else:
+            # ...otherwise, encode it as an "application header"
+            encodedHeaderName = Encoder.encodeTokenText(headerFieldName)
+            encodedHeader.extend(encodedHeaderName)
+        # Now add the value
+        #TODO: make this flow better (see also Decoder.decodeHeader)
+        # most header values are encoded as TextStrings, except where we have a specific Wap-value encoding implementation
+        if headerFieldName in WSPEncodingAssignments.hdrFieldEncodings:
+            wapValueType = WSPEncodingAssignments.hdrFieldEncodings[headerFieldName]
+            try:
+                exec 'encodedHeader.extend(Encoder.encode%s(headerValue))' % wapValueType
+            except EncodeError, msg:
+                raise EncodeError, 'Error encoding Wap-value: %s' % msg
+            except:
+                print 'A fatal error occurred, probably due to an unimplemented encoding operation'
+                raise
+        else:
+            encodedHeader.extend(Encoder.encodeTextString(headerValue))
+        return encodedHeader
+    
+    @staticmethod
+    def encodeContentTypeValue(mediaType, parameters):
+        """ Encodes a content type, and its parameters
+
+        From [5], section 8.4.2.24:
+        C{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 encoded Content-type-value (including parameters, if
+                 any), as a sequence of bytes
+        @rtype: list
+        """
+        encodedContentTypeValue = []
+        # First try do encode it using Constrained-media encoding
+        try:
+            if len(parameters) > 0:
+                raise EncodeError, 'Need to use Content-general-form for parameters'
+            else:
+                encodedContentTypeValue = Encoder.encodeConstrainedMedia(mediaType)
+        except EncodeError:
+            # Try the general form
+            encodedContentTypeValue = Encoder.encodeContentGeneralForm(mediaType, parameters)
+        return encodedContentTypeValue
+    
+    @staticmethod
+    def encodeConstrainedMedia(mediaType):
+        """ From [5], section 8.4.2.7:
+        Constrained-media = Constrained-encoding
+        It is encoded using values from the "Content Type Assignments" table.
+        
+        @param mediaType: The media type to encode
+        @type mediaType: str
+        
+        @raise EncodeError: Media value is unsuitable for Constrained-encoding
+        
+        @return: The encoded media type, as a sequence of bytes
+        @rtype: list
+        """
+        encodedMediaType = []
+        mediaValue = ''
+        # See if this value is in the table of well-known content types
+        if mediaType in WSPEncodingAssignments.wkContentTypes:
+            mediaValue = WSPEncodingAssignments.wkContentTypes.index(mediaType)
+        else:
+            mediaValue = mediaType
+        encodedMediaType = Encoder.encodeConstrainedEncoding(mediaValue)    
+        return encodedMediaType
+
+    @staticmethod
+    def encodeConstrainedEncoding(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: <value> cannot be encoded as a
+                            Constrained-encoding sequence
+        
+        @return: The encoded constrained-encoding token value, as a sequence
+                 of bytes
+        @rtype: list
+        """
+        encodedValue = None
+        if type(value) == int:
+            # First try and encode the value as a short-integer
+            encodedValue = Encoder.encodeShortInteger(value)
+        else:
+            # Ok, it should be Extension-Media then
+            try:
+                encodedValue = Encoder.encodeExtensionMedia(value)
+            except EncodeError:
+                # Give up
+                raise EncodeError, 'Cannot encode %s as a Constrained-encoding sequence' % str(value)
+        return encodedValue
+
+    @staticmethod
+    def encodeExtensionMedia(mediaValue):
+        """ From [5], section 8.4.2.1:
+        Extension-media = *TEXT End-of-string
+        This encoding is used for media values, which have no well-known
+        binary encoding
+        
+        @param mediaValue: The media value (string) to encode
+        @type mediaValue: 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
+        """
+        encodedMediaValue = ''
+        if type(mediaValue) != str:
+            try:
+                mediaValue = str(mediaValue)
+            except:
+                raise EncodeError, 'Invalid Extension-media: Cannot convert value to text string'    
+        char = mediaValue[0]
+        if ord(char) < 32 or ord(char) == 127:
+            raise EncodeError, 'Invalid Extension-media: TEXT starts with invalid character: %s' % ord(char)
+        encodedMediaValue = Encoder.encodeTextString(mediaValue)
+        return encodedMediaValue
+
+    @staticmethod
+    def encodeContentGeneralForm(mediaType, parameters):
+        """ From [5], section 8.4.2.24:
+        Content-general-form = Value-length Media-type
+        
+        @note Used in decoding Content-type fields and their parameters;
+              see C{decodeContentTypeValue}
+        
+        @note: Used by C{decodeContentTypeValue()}
+        
+        @return: The encoded Content-general-form, as a sequence of bytes
+        @rtype: list
+        """
+        encodedContentGeneralForm = []
+        encodedMediaType = []
+        encodedParameters = []
+        # Encode the actual content type
+        encodedMediaType = Encoder.encodeMediaType(mediaType)
+        # Encode all parameters
+        for paramName in parameters:
+            ### TODO: 
+            #print paramName, parameters[paramName]
+            encodedParameters.extend(Encoder.encodeParameter(paramName, parameters[paramName]))
+        valueLength = len(encodedMediaType) + len(encodedParameters)
+        encodedValueLength = Encoder.encodeValueLength(valueLength)
+        encodedContentGeneralForm.extend(encodedValueLength)
+        encodedContentGeneralForm.extend(encodedMediaType)
+        encodedContentGeneralForm.extend(encodedParameters)
+        return encodedContentGeneralForm
+
+    @staticmethod
+    def encodeValueLength(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 C{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
+                       <Any octet 0-30>          <Octet 31>   Uintvar-integer
+                       
+        @raise EncodeError: The ValueLength could not be encoded.
+        
+        @return: The encoded value length indicator, as a sequence of bytes
+        @rtype: list
+        """
+        encodedValueLength = []
+        # Try and encode it as a short-length
+        try:
+            encodedValueLength = Encoder.encodeShortLength(length)
+        except EncodeError:
+            # Encode it with a Length-quote and Uintvar
+            encodedValueLength.append(31) # Length-quote
+            encodedValueLength.extend(Encoder.encodeUintvar(length))
+        return encodedValueLength
+
+    @staticmethod
+    def encodeShortLength(length):
+        """ From [5], section 8.4.2.2:
+        Short-length = <Any octet 0-30>
+        
+        @raise EmcodeError: The specified <length> 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 range 0-30'
+        else:
+            return [length]
+
+    @staticmethod
+    def encodeAcceptValue(acceptValue):
+        """ 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 = <Octet 128>
+
+        @note: This implementation does not currently support encoding of
+               "Accept-parameters".
+               
+        @param acceptValue: The Accept-value to encode (media/content type)
+        @type acceptValue: str
+
+        @raise EncodeError: The encoding failed.
+
+        @return The encoded Accept-value, as a sequence of bytes
+        @rtype: list
+        """
+        encodedAcceptValue = []
+        # Try to use Constrained-media encoding
+        try:
+            encodedAcceptValue = Encoder.encodeConstrainedMedia(acceptValue)
+        except EncodeError:
+            # ...now try Accept-general-form
+            try:
+                encodedMediaRange = Encoder.encodeMediaType(acceptValue)
+            except EncodeError, msg:
+                raise EncodeError, 'Cannot encode Accept-value: %s' % msg
+            valueLength = Encoder.encodeValueLength(len(encodedMediaRange))
+            encodedAcceptValue = valueLength
+            encodedAcceptValue.extend(encodedMediaRange)
+        return encodedAcceptValue
diff --git a/src/wappushhandler.py b/src/wappushhandler.py
new file mode 100644 (file)
index 0000000..2af6985
--- /dev/null
@@ -0,0 +1,339 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+""" Class for handling wap push messages and creating MMS messages
+
+@author: Nick Leppänen Larsson <frals@frals.se>
+@license: GNU GPL
+"""
+import sys
+import os
+import dbus
+import urllib2
+import urllib
+import httplib
+import conic
+import time
+import socket
+import array
+
+from dbus.mainloop.glib import DBusGMainLoop
+
+from mms import message
+from mms.message import MMSMessage
+from mms import mms_pdu
+import fmms_config as fMMSconf
+import controller as fMMSController
+
+magic = 0xacdcacdc
+
+_DBG = True
+
+class PushHandler:
+       def __init__(self):
+               self.cont = fMMSController.fMMS_controller()
+               # TODO: get all this from controller instead of config
+               self.config = fMMSconf.fMMS_config()
+               self._mmsdir = self.config.get_mmsdir()
+               self._pushdir = self.config.get_pushdir()
+               self._apn = self.config.get_apn()
+               self._apn_nicename = self.config.get_apn_nicename()
+               self._incoming = '/home/user/.fmms/temp/LAST_INCOMING'
+               
+               if not os.path.isdir(self._mmsdir):
+                       print "creating dir", self._mmsdir
+                       os.makedirs(self._mmsdir)
+               if not os.path.isdir(self._pushdir):
+                       print "creating dir", self._pushdir
+                       os.makedirs(self._pushdir)
+
+       """ handle incoming push over sms """
+       def _incoming_sms_push(self, source, src_port, dst_port, wsp_header, wsp_payload):
+               dbus_loop = DBusGMainLoop()
+               args = (source, src_port, dst_port, wsp_header, wsp_payload)
+               
+               # TODO: dont hardcode
+               if not os.path.isdir('/home/user/.fmms/temp'):
+                       print "creating dir /home/user/.fmms/temp"
+                       os.makedirs("/home/user/.fmms/temp")
+               
+               f = open(self._incoming, 'w')
+               for arg in args:
+                   f.write(str(arg))
+                   f.write('\n')
+               f.close()
+
+               if(_DBG):
+                       print "SRC: ", source, ":", src_port
+                       print "DST: ", dst_port
+                       #print "WSPHEADER: ", wsp_header
+                       #print "WSPPAYLOAD: ", wsp_payload
+
+               binarydata = []
+               # throw away the wsp_header!
+               #for d in wsp_header:
+               #       data.append(int(d))
+               
+               for d in wsp_payload:
+                       binarydata.append(int(d))
+
+               print "decoding..."
+               
+               
+               (data, sndr, url, trans_id) = self.cont.decode_mms_from_push(binarydata)
+               
+               print "saving..."
+               # Controller should save it
+               pushid = self.cont.save_push_message(data)
+               print "notifying push..."
+               # Send a notify we got the SMS Push and parsed it A_OKEY!
+               self.notify_mms(dbus_loop, sndr, "SMS Push for MMS received")
+               print "fetching mms..."
+               path = self._get_mms_message(url, trans_id)
+               print "decoding mms... path:", path
+               message = self.cont.decode_binary_mms(path)
+               print "storing mms..."
+               mmsid = self.cont.store_mms_message(pushid, message)
+               print "notifying mms..."
+               self.notify_mms(dbus_loop, sndr, "New MMS", trans_id);
+               return 0
+
+
+       """ handle incoming ip push """
+       # TODO: implement this
+       def _incoming_ip_push(self, src_ip, dst_ip, src_port, dst_port, wsp_header, wsp_payload):
+               if(_DBG):
+                       print "SRC: " + src_ip + ":" + src_port + "\n"
+                       print "DST: " + dst_ip + ":" + dst_port + "\n"
+                       print "WSPHEADER: " + wsp_header + "\n"
+                       print "WSPPAYLOAD: " + wsp_payload + "\n"
+                       print
+
+
+       """ notifies the user with a org.freedesktop.Notifications.Notify, really fancy """
+       def notify_mms(self, dbus_loop, sender, message, path=None):
+               bus = dbus.SystemBus()
+               proxy = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
+               interface = dbus.Interface(proxy,dbus_interface='org.freedesktop.Notifications')
+               choices = ['default', 'cancel']
+               if path == None:
+                       interface.Notify('MMS', 0, '', message, sender, choices, {"category": "sms-message", "dialog-type": 4, "led-pattern": "PatternCommunicationEmail", "dbus-callback-default": "se.frals.fmms /se/frals/fmms se.frals.fmms open_gui"}, -1)
+               else:
+                       # TODO: callback should open fMMS gui
+                       interface.Notify("MMS", 0, '', message, sender, choices, {"category": "email-message", "dialog-type": 4, "led-pattern": "PatternCommunicationEmail", "dbus-callback-default": "se.frals.fmms /se/frals/fmms se.frals.fmms open_mms string:\"" + path + "\""}, -1)
+
+
+       """ get the mms message from content-location """
+       """ thanks benaranguren on talk.maemo.org for patch including x-wap-profile header """
+       def _get_mms_message(self, location, transaction):
+               print "getting file: ", location
+               try:
+                       # TODO: remove hardcoded sleep
+                       con = ConnectToAPN(self._apn_nicename)
+                       #time.sleep(6)
+                       con.connect()
+                       
+                       try:
+                               notifyresp = self._send_notify_resp(transaction)
+                               print "notifyresp sent"
+                       except:
+                               print "notify sending failed..."
+                       
+                       # TODO: configurable time-out?
+                       timeout = 60
+                       socket.setdefaulttimeout(timeout)
+                       (proxyurl, proxyport) = self.config.get_proxy_from_apn()
+                       
+                       if proxyurl == "" or proxyurl == None:
+                               print "connecting without proxy"
+                       else:
+                               proxyfull = str(proxyurl) + ":" + str(proxyport)
+                               print "connecting with proxy", proxyfull        
+                               proxy = urllib2.ProxyHandler({"http": proxyfull})
+                               opener = urllib2.build_opener(proxy)
+                               urllib2.install_opener(opener)
+                               
+                       #headers = {'x-wap-profile': 'http://mms.frals.se/n900.rdf'}
+                       #User-Agent: NokiaN95/11.0.026; Series60/3.1 Profile/MIDP-2.0 Configuration/CLDC-1.1 
+                       headers = {'User-Agent' : 'NokiaN95/11.0.026; Series60/3.1 Profile/MIDP-2.0 Configuration/CLDC-1.1', 'x-wap-profile' : 'http://mms.frals.se/n900.rdf'}
+                       req = urllib2.Request(location, headers=headers)
+                       mmsdata = urllib2.urlopen(req)
+                       try:
+                               print mmsdata.info()
+                       except:
+                               pass
+                       
+                       mmsdataall = mmsdata.read()
+                       dirname = self.cont.save_binary_mms(mmsdataall, transaction)
+                       
+                       if(_DBG):
+                               print "fetched ", location, " and wrote to file"
+                               
+                       # send acknowledge we got it ok
+                       try:
+                               ack = self._send_acknowledge(transaction)
+                               print "ack sent"
+                       except:
+                               print "sending ack failed"
+                       
+                       con.disconnect()                        
+                       
+               except Exception, e:
+                       print e, e.args
+                       bus = dbus.SystemBus()
+                       proxy = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications')
+                       interface = dbus.Interface(proxy,dbus_interface='org.freedesktop.Notifications')
+                       interface.SystemNoteInfoprint ("fMMS: Failed to download MMS message.")
+                       raise
+                       
+               return dirname
+
+
+       def _send_notify_resp(self, transid):
+               mms = MMSMessage(True)
+               mms.headers['Message-Type'] = "m-notifyresp-ind"
+               mms.headers['Transaction-Id'] = transid
+               mms.headers['MMS-Version'] = "1.3"
+               mms.headers['Status'] = "Deferred"
+               
+               print "setting up notify sender"
+               sender = MMSSender(customMMS=True)
+               print "sending notify..."
+               out = sender.sendMMS(mms)
+               print "m-notifyresp-ind:", out
+               return out
+       
+       
+       def _send_acknowledge(self, transid):
+               mms = MMSMessage(True)
+               mms.headers['Message-Type'] = "m-acknowledge-ind"
+               mms.headers['Transaction-Id'] = transid
+               mms.headers['MMS-Version'] = "1.3"
+               
+               print "setting up ack sender"
+               ack = MMSSender(customMMS=True)
+               print "sending ack..."
+               out = ack.sendMMS(mms)
+               print "m-acknowledge-ind:", out
+               return out
+
+       
+class ConnectToAPN:
+       def __init__(self, apn):
+           self._apn = apn
+           self.connection = conic.Connection()
+           
+       def connection_cb(self, connection, event, magic):
+           print "connection_cb(%s, %s, %x)" % (connection, event, magic)
+
+       
+       def disconnect(self):
+               connection = self.connection
+               connection.disconnect_by_id(self._apn)
+       
+       def connect(self):
+               global magic
+
+               # Creates the connection object and attach the handler.
+               connection = self.connection
+               iaps = connection.get_all_iaps()
+               iap = None
+               for i in iaps:
+                       if i.get_name() == self._apn:
+                               iap = i
+               
+               connection.disconnect()
+               connection.connect("connection-event", self.connection_cb, magic)
+
+               # The request_connection method should be called to initialize
+               # some fields of the instance
+               if not iap:
+                       assert(connection.request_connection(conic.CONNECT_FLAG_NONE))
+               else:
+               #print "Getting by iap", iap.get_id()
+                       assert(connection.request_connection_by_id(iap.get_id(), conic.CONNECT_FLAG_NONE))
+                       return False
+           
+""" class for sending an mms """           
+class MMSSender:
+       def __init__(self, number=None, subject=None, message=None, attachment=None, sender=None, customMMS=None):
+               print "GOT SENDER:", sender
+               print "customMMS:", customMMS
+               self.customMMS = customMMS
+               self.config = fMMSconf.fMMS_config()
+               if customMMS == None:
+                       self.number = number
+                       self.subject = subject
+                       self.message = message
+                       self.attachment = attachment
+                       self._mms = None
+                       self._sender = sender
+                       self.createMMS()
+           
+       def createMMS(self):
+               slide = message.MMSMessagePage()
+               if self.attachment != None:
+                       slide.addImage(self.attachment)
+               slide.addText(self.message)
+
+               self._mms = message.MMSMessage()
+               self._mms.headers['Subject'] = self.subject
+               self._mms.headers['To'] = str(self.number) + '/TYPE=PLMN'
+               self._mms.headers['From'] = str(self._sender) + '/TYPE=PLMN'
+               self._mms.addPage(slide)
+       
+       def sendMMS(self, customData=None):
+               mmsid = None
+               if customData != None:
+                       print "using custom mms"
+                       self._mms = customData
+       
+               mmsc = self.config.get_mmsc()
+               
+               (proxyurl, proxyport) = self.config.get_proxy_from_apn()
+               mms = self._mms.encode()
+               
+               headers = {'Content-Type':'application/vnd.wap.mms-message', 'User-Agent' : 'NokiaN95/11.0.026; Series60/3.1 Profile/MIDP-2.0 Configuration/CLDC-1.1', 'x-wap-profile' : 'http://mms.frals.se/n900.rdf'}
+               #headers = {'Content-Type':'application/vnd.wap.mms-message'}
+               if proxyurl == "" or proxyurl == None:
+                       print "connecting without proxy"
+                       mmsc = mmsc.lower()
+                       mmsc = mmsc.replace("http://", "")
+                       mmsc = mmsc.rstrip('/')
+                       mmsc = mmsc.partition('/')
+                       mmschost = mmsc[0]
+                       path = "/" + str(mmsc[2])
+                       print "mmschost:", mmschost, "path:", path, "pathlen:", len(path)
+                       conn = httplib.HTTPConnection(mmschost)
+                       conn.request('POST', path , mms, headers)
+               else:
+                       print "connecting via proxy " + proxyurl + ":" + str(proxyport)
+                       print "mmschost:", mmsc
+                       conn = httplib.HTTPConnection(proxyurl + ":" + str(proxyport))
+                       conn.request('POST', mmsc, mms, headers)
+
+               if customData == None:                  
+                       cont = fMMSController.fMMS_controller()
+                       path = cont.save_binary_outgoing_mms(mms, self._mms.transactionID)
+                       message = cont.decode_binary_mms(path)
+                       mmsid = cont.store_outgoing_mms(message)        
+                       
+               res = conn.getresponse()
+               print "MMSC STATUS:", res.status, res.reason
+               out = res.read()
+               try:
+                       decoder = mms_pdu.MMSDecoder()
+                       data = array.array('B')
+                       for b in out:
+                               data.append(ord(b))
+                       outparsed = decoder.decodeResponseHeader(data)
+                       
+                       if mmsid != None:
+                               pushid = cont.store_outgoing_push(outparsed)
+                               cont.link_push_mms(pushid, mmsid)
+                               
+               except Exception, e:
+                       print type(e), e
+                       outparsed = out
+                       
+               print "MMSC RESPONDED:", outparsed
+               return res.status, res.reason, outparsed
\ No newline at end of file