--- /dev/null
+ 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.
--- /dev/null
+claesbas (Claes Norin) - for the great logo!
+Francois Aucamp <faucamp@csir.co.za> - for the original PyMMS library
\ No newline at end of file
--- /dev/null
+#!/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)
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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()
--- /dev/null
+ 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.
--- /dev/null
+ 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
--- /dev/null
+# -*- 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
--- /dev/null
+# -*- 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
--- /dev/null
+#
+# 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
--- /dev/null
+#!/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 ''
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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