From bc8dbf62a122666e0b3960fc9b407ee1787ab084 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Nick=20Lepp=E4nen=20Larsson?= Date: Mon, 25 Jan 2010 21:30:50 +0100 Subject: [PATCH] initial commit --- src/COPYING | 339 +++++++++ src/CREDITS | 2 + src/contacts.py | 135 ++++ src/controller.py | 264 +++++++ src/dbhandler.py | 403 ++++++++++ src/fmms.png | Bin 0 -> 11535 bytes src/fmms_config.py | 196 +++++ src/fmms_gui.py | 473 ++++++++++++ src/fmms_sender_ui.py | 332 ++++++++ src/fmms_viewer.py | 229 ++++++ src/fmmsd.py | 44 ++ src/mms/COPYING | 339 +++++++++ src/mms/COPYING.LESSER | 165 ++++ src/mms/WSP.py | 337 +++++++++ src/mms/WTP.py | 360 +++++++++ src/mms/iterator.py | 64 ++ src/mms/message.py | 532 +++++++++++++ src/mms/mms_pdu.py | 1145 ++++++++++++++++++++++++++++ src/mms/wsp_pdu.py | 1966 ++++++++++++++++++++++++++++++++++++++++++++++++ src/wappushhandler.py | 339 +++++++++ 20 files changed, 7664 insertions(+) create mode 100644 src/COPYING create mode 100644 src/CREDITS create mode 100644 src/LAST_INCOMING create mode 100644 src/LAST_OUTGOING create mode 100644 src/__init__.py create mode 100644 src/contacts.py create mode 100644 src/controller.py create mode 100644 src/dbhandler.py create mode 100644 src/fmms.png create mode 100644 src/fmms_config.py create mode 100644 src/fmms_gui.py create mode 100644 src/fmms_sender_ui.py create mode 100644 src/fmms_viewer.py create mode 100644 src/fmmsd.py create mode 100644 src/mms/COPYING create mode 100644 src/mms/COPYING.LESSER create mode 100644 src/mms/WSP.py create mode 100644 src/mms/WTP.py create mode 100644 src/mms/__init__.py create mode 100644 src/mms/iterator.py create mode 100644 src/mms/message.py create mode 100644 src/mms/mms_pdu.py create mode 100644 src/mms/wsp_pdu.py create mode 100644 src/wappushhandler.py diff --git a/src/COPYING b/src/COPYING new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/src/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/src/CREDITS b/src/CREDITS new file mode 100644 index 0000000..670554f --- /dev/null +++ b/src/CREDITS @@ -0,0 +1,2 @@ +claesbas (Claes Norin) - for the great logo! +Francois Aucamp - for the original PyMMS library \ No newline at end of file diff --git a/src/LAST_INCOMING b/src/LAST_INCOMING new file mode 100644 index 0000000..e69de29 diff --git a/src/LAST_OUTGOING b/src/LAST_OUTGOING new file mode 100644 index 0000000..e69de29 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/contacts.py b/src/contacts.py new file mode 100644 index 0000000..c8a0b98 --- /dev/null +++ b/src/contacts.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python2.5 +# -*- coding: utf-8 -*- +""" Handles contacts integration for fMMS + +@author: Nick Leppänen Larsson +@license: GNU GPL +""" +import evolution +import gtk + +class ContactHandler: + + + """ wouldnt mind some nice patches against this """ + def __init__(self): + self.ab = evolution.ebook.open_addressbook("default") + self.contacts = self.ab.get_all_contacts() + self.phonedict = {} + for c in self.contacts: + #print c.get_name(), c.get_property('mobile-phone') + #print c.get_property('other-phone') + # this was a pretty clean solution as well, but oh so wrong! + mb = c.get_property('mobile-phone') + cp = c.get_property('other-phone') + nrlist = (mb, cp) + fname = c.get_name() + # TODO: this is _really_ slow... look at integration with c please + #nrlist = self.get_numbers_from_name(fname) + self.phonedict[c.get_name()] = nrlist + + """ returns all the numbers from a name, as a list """ + def get_numbers_from_name(self, fname): + search = self.ab.search(fname) + res = search[0] + # would've been nice if this got all numbers, but alas, it dont. + """props = ['assistant-phone', 'business-phone', 'business-phone-2', 'callback-phone', 'car-phone', 'company-phone', 'home-phone', 'home-phone-2', 'mobile-phone', 'other-phone', 'primary-phone'] + nrlist = [] + for p in props: + nr = res.get_property(p) + if nr != None: + nrlist.append(nr)""" + # creative use of processing power? *cough* + nrlist = {} + vcardlist = res.get_vcard_string().replace('\r', '').split('\n') + for line in vcardlist: + if line.startswith("TEL"): + #print line + nr = line.split(":")[1] + ltype = line.split(":")[0].split("=") + phonetype = "Unknown" + try: + for type in ltype: + rtype = type.replace(";TYPE", "") + if rtype != "TEL" and rtype != "VOICE": + phonetype = rtype + except: + pass + if nr != None: + nrlist[nr] = phonetype + return nrlist + + + """ returns all contact names sorted by name """ + def get_contacts_as_list(self): + retlist = [] + for contact in self.contacts: + cn = contact.get_name() + if cn != None: + retlist.append(cn) + retlist.sort(key=str.lower) + return retlist + + """ takes a number on international format (ie +46730111111) """ + def get_name_from_number(self, number): + ### do some voodoo here + # ugly way of removing country code since this + # can be 2 or 3 chars we remove 4 just in case + # 3 and the + char = 4 + numberstrip = number[4:-1] + for person in self.phonedict: + for cbnr in self.phonedict[person]: + if cbnr != None: + cbnr = cbnr.replace(" ", "") + cbnr = cbnr.lstrip('0') + if cbnr == number or numberstrip.endswith(cbnr) or number.endswith(cbnr): + return person + + return None + + def get_photo_from_name(self, pname, imgsize): + res = self.ab.search(pname) + ### do some nice stuff here + #l = [x.get_name() for x in res] + #print "search for:", pname, "gave res: ", l + if res != None: + img = res[0].get_photo(imgsize) + if img == None: + vcardlist = res[0].get_vcard_string().replace('\r', '').split('\n') # vcard for first result + for line in vcardlist: + if line.startswith("PHOTO;VALUE=uri:file://"): + imgstr = line.replace("PHOTO;VALUE=uri:file://", "") + img = gtk.gdk.pixbuf_new_from_file(imgstr) + height = img.get_height() + if height != imgsize: + newheight = imgsize + newwidth = int(newheight * img.get_width() / height) + #print "h:", height, "w:", img.get_width() + #print "newh:", newheight, "neww:", newwidth + img = img.scale_simple(newwidth, newheight, gtk.gdk.INTERP_BILINEAR) + return img + else: + return None + + +if __name__ == '__main__': + cb = ContactHandler() + #c = ab.get_contact("id") + #c.get_name() + #print cb.ab.get_all_contacts()[0].__doc__ + #asd = cb.get_contacts_as_list() + #print asd + + """r = cb.ab.search('Tom Le') # Returns List of results + print r + l = [x.get_name() for x in r] # list of results + u = r[0].get_name() # name of the first result + vcardlist = r[0].get_vcard_string().replace('\r', '').split('\n') # vcard for first result + #print vcardlist""" + #print cb.get_numbers_from_name('') + """ + for line in vcardlist: + if not line.startswith("PHOTO"): + print line + """ + #print r[0].get_photo(64) diff --git a/src/controller.py b/src/controller.py new file mode 100644 index 0000000..90c4192 --- /dev/null +++ b/src/controller.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python2.5 +# -*- coding: utf-8 -*- +""" Useful functions that shouldn't be in the UI code + + +And, yes, I know this is not really a controller. + +@author: Nick Leppänen Larsson +@license: GNU GPL +""" +import os +import array + +import dbus +from dbus.mainloop.glib import DBusGMainLoop + +import fmms_config as fMMSconf +import dbhandler as DBHandler +from mms.message import MMSMessage +from mms import mms_pdu + +#TODO: constants.py? +MSG_DIRECTION_IN = 0 +MSG_DIRECTION_OUT = 1 +MSG_UNREAD = 0 +MSG_READ = 1 + +class fMMS_controller(): + + def __init__(self): + self.config = fMMSconf.fMMS_config() + self._mmsdir = self.config.get_mmsdir() + self._pushdir = self.config.get_pushdir() + self._outdir = self.config.get_outdir() + self.store = DBHandler.DatabaseHandler() + + + def decode_mms_from_push(self, binarydata): + decoder = mms_pdu.MMSDecoder() + wsplist = decoder.decodeCustom(binarydata) + + sndr, url, trans_id = None, None, None + bus = dbus.SystemBus() + proxy = bus.get_object('org.freedesktop.Notifications', '/org/freedesktop/Notifications') + interface = dbus.Interface(proxy,dbus_interface='org.freedesktop.Notifications') + + try: + url = wsplist["Content-Location"] + print "content-location:", url + trans_id = wsplist["Transaction-Id"] + trans_id = str(trans_id) + print "transid:", trans_id + except Exception, e: + print "no content-location/transid in push; aborting...", type(e), e + interface.SystemNoteInfoprint ("fMMS: Failed to parse SMS PUSH.") + raise + try: + sndr = wsplist["From"] + print "Sender:", sndr + except Exception, e: + print "No sender value defined", type(e), e + sndr = "Unknown sender" + + self.save_binary_push(binarydata, trans_id) + return (wsplist, sndr, url, trans_id) + + + def save_binary_push(self, binarydata, transaction): + data = array.array('B') + for b in binarydata: + data.append(b) + # TODO: move to config? + if not os.path.isdir(self._pushdir): + os.makedirs(self._pushdir) + try: + fp = open(self._pushdir + transaction, 'wb') + fp.write(data) + print "saved binary push", fp + fp.close() + except Exception, e: + print "failed to save binary push:", type(e), e + raise + + def save_push_message(self, data): + """ Gets the decoded data as a list (preferably from decode_mms_from_push) + """ + pushid = self.store.insert_push_message(data) + return pushid + + + def get_push_list(self, types=None): + return self.store.get_push_list() + + + def is_fetched_push_by_transid(self, transactionid): + return self.store.is_mms_downloaded(transactionid) + + + def read_push_as_list(self, transactionid): + return self.store.get_push_message(transactionid) + + + def save_binary_mms(self, data, transaction): + dirname = self._mmsdir + transaction + if not os.path.isdir(dirname): + os.makedirs(dirname) + + fp = open(dirname + "/message", 'wb') + fp.write(data) + print "saved binary mms", fp + fp.close() + return dirname + + def save_binary_outgoing_mms(self, data, transaction): + transaction = str(transaction) + dirname = self._outdir + transaction + if not os.path.isdir(dirname): + os.makedirs(dirname) + + fp = open(dirname + "/message", 'wb') + fp.write(data) + print "saved binary mms", fp + fp.close() + return dirname + + def decode_binary_mms(self, path): + """ decodes and saves the binary mms""" + # Decode the specified file + # This also creates all the parts as files in path + print "decode_binary_mms running" + try: + message = MMSMessage.fromFile(path + "/message") + except Exception, e: + print type(e), e + raise + print "returning message!" + return message + + + def store_mms_message(self, pushid, message): + mmsid = self.store.insert_mms_message(pushid, message) + return mmsid + + def store_outgoing_mms(self, message): + mmsid = self.store.insert_mms_message(0, message, DBHandler.MSG_DIRECTION_OUT) + return mmsid + + def store_outgoing_push(self, wsplist): + pushid = self.store.insert_push_send(wsplist) + return pushid + + def link_push_mms(self, pushid, mmsid): + self.store.link_push_mms(pushid, mmsid) + + def get_direction_mms(self, transid): + return self.store.get_direction_mms(transid) + + def get_mms_from_push(self, transactionid): + plist = self.store.get_push_message(transactionid) + trans_id = plist['Transaction-Id'] + pushid = plist['PUSHID'] + url = plist['Content-Location'] + + from wappushhandler import PushHandler + p = PushHandler() + path = p._get_mms_message(url, trans_id) + print "decoding mms..." + message = self.cont.decode_binary_mms(path) + print "storing mms..." + mmsid = self.cont.store_mms_message(pushid, message) + + + def get_mms_attachments(self, transactionid, allFiles=False): + return self.store.get_mms_attachments(transactionid, allFiles) + + def get_mms_headers(self, transactionid): + return self.store.get_mms_headers(transactionid) + + def delete_mms_message(self, fname): + fullpath = self._mmsdir + fname + print fullpath + if os.path.isdir(fullpath): + print "starting deletion of", fullpath + filelist = self.get_mms_attachments(fname, allFiles=True) + if filelist == None: + filelist = [] + filelist.append("message") + for fn in filelist: + try: + fullfn = fullpath + "/" + fn + os.remove(fullfn) + except: + print "failed to remove", fullfn + try: + print "trying to remove", fullpath + os.rmdir(fullpath) + except OSError, e: + print "failed to remove dir:", type(e), e + raise + self.store.delete_mms_message(fname) + + def delete_push_message(self, fname): + fullpath = self._pushdir + fname + print fullpath + if os.path.isfile(fullpath): + print "removing", fullpath + try: + os.remove(fullpath) + except Exception, e: + raise + self.store.delete_push_message(fname) + + def wipe_message(self, transactionid): + self.delete_mms_message(transactionid) + self.delete_push_message(transactionid) + + """ DEPRECATED AS OF 0.2.10 + gets a mms from a previously received push """ + """ this function requires the fname to be the fullpath """ + # TODO: dont require fullpath + """def get_mms_from_push(self, fname): + + plist = self.read_push_as_list(fname) + try: + sndr = plist['From'] + except: + sndr = "Unknown" + url = plist['Content-Location'] + print url + trans_id = plist['Transaction-Id'] + print trans_id + + from wappushhandler import PushHandler + push = PushHandler() + path = push._get_mms_message(url, trans_id) + Push.decodeMMS(path) + + return 0""" + + """ Old function relying on files... Deprecated as of 0.2.10 + def is_fetched_push(self, filename): + this function takes the FILENAME, not the full path + path = self._mmsdir + filename + if os.path.isdir(path): + if os.path.isfile(self._mmsdir + filename + "/message"): + return True + else: + return False""" + + + """def read_push_as_list(self, fname): + # reads a saved push message into a dict + fp = open(fname, 'r') + pdict = {} + for line in fp: + line = line.replace("\n", "") + lsplit = line.partition(" ") + pdict[lsplit[0]] = lsplit[2] + fp.close() + return pdict""" + +if __name__ == '__main__': + c = fMMS_controller() + pass \ No newline at end of file diff --git a/src/dbhandler.py b/src/dbhandler.py new file mode 100644 index 0000000..36de632 --- /dev/null +++ b/src/dbhandler.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" database handler for fMMS + +@author: Nick Leppänen Larsson +@license: GNU GPL +""" +import sqlite3 +import os + +from gnome import gnomevfs + +import fmms_config as fMMSconf + + +# TODO: constants.py? +MSG_DIRECTION_IN = 0 +MSG_DIRECTION_OUT = 1 +MSG_UNREAD = 0 +MSG_READ = 1 + + +class DatabaseHandler: + + def __init__(self): + self.config = fMMSconf.fMMS_config() + self.pushdir = self.config.get_pushdir() + self.mmsdir = self.config.get_mmsdir() + self.outdir = self.config.get_outdir() + self.db = self.config.get_db_path() + self.conn = sqlite3.connect(self.db) + self.conn.row_factory = sqlite3.Row + try: + c = self.conn.cursor() + c.execute("SELECT * FROM revision") + for row in c: + if row['version'] != 1: + self.create_database_layout() + except: + self.create_database_layout() + + + def create_database_layout(self): + c = self.conn + c.execute("""CREATE TABLE "revision" ("version" INT);""") + c.execute("""INSERT INTO "revision" ("version") VALUES ('1');""") + # database layout + c.execute("""CREATE TABLE "push"( + "idpush" INTEGER PRIMARY KEY NOT NULL, + "transactionid" TEXT NOT NULL, + "content_location" TEXT NULL, + "msg_time" TIMESTAMP, + "msg_type" TEXT NOT NULL, + "file" TEXT + );""") + c.execute("""CREATE TABLE "contacts"( + "idcontacts" INTEGER PRIMARY KEY NOT NULL, + "number" INTEGER NOT NULL, + "abook_uid" INTEGER DEFAULT NULL + );""") + c.execute("""CREATE TABLE "mms"( + "id" INTEGER PRIMARY KEY NOT NULL, + "pushid" INTEGER DEFAULT NULL, + "transactionid" INTEGER DEFAULT NULL, + "msg_time" TIMESTAMP DEFAULT NULL, + "read" INTEGER DEFAULT NULL, + "direction" INTEGER DEFAULT NULL, + "size" INT DEFAULT NULL, + "contact" INTEGER DEFAULT NULL, + "file" TEXT DEFAULT NULL, + CONSTRAINT "pushid" + FOREIGN KEY("pushid") + REFERENCES "push"("idpush"), + CONSTRAINT "contact" + FOREIGN KEY("contact") + REFERENCES "contacts"("idcontacts") + );""") + c.execute("""CREATE INDEX "mms.pushid" ON "mms"("pushid");""") + c.execute("""CREATE INDEX "mms.contact" ON "mms"("contact");""") + c.execute("""CREATE TABLE "mms_headers"( + "idmms_headers" INTEGER PRIMARY KEY NOT NULL, + "mms_id" INTEGER DEFAULT NULL, + "header" TEXT DEFAULT NULL, + "value" TEXT DEFAULT NULL, + CONSTRAINT "mms_id" + FOREIGN KEY("mms_id") + REFERENCES "mms"("id") + );""") + c.execute("""CREATE INDEX "mms_headers.mms_id" ON "mms_headers"("mms_id");""") + c.execute("""CREATE TABLE "attachments"( + "idattachments" INTEGER PRIMARY KEY NOT NULL, + "mmsidattach" INTEGER DEFAULT NULL, + "file" TEXT DEFAULT NULL, + "hidden" INTEGER DEFAULT NULL, + CONSTRAINT "mmsidattach" + FOREIGN KEY("mmsidattach") + REFERENCES "mms"("id") + );""") + c.execute("""CREATE INDEX "attachments.mmsidattach" ON "attachments"("mmsidattach");""") + c.execute("""CREATE TABLE "push_headers"( + "idpush_headers" INTEGER PRIMARY KEY NOT NULL, + "push_id" INTEGER DEFAULT NULL, + "header" TEXT DEFAULT NULL, + "value" TEXT DEFAULT NULL, + CONSTRAINT "push_id" + FOREIGN KEY("push_id") + REFERENCES "push"("idpush") + );""") + c.execute("""CREATE INDEX "push_headers.push_id" ON "push_headers"("push_id");""") + self.conn.commit() + + + def get_push_list(self, types=None): + """ gets all push messages from the db and returns as a list + containing a dict for each separate push """ + c = self.conn.cursor() + retlist = [] + # TODO: better where clause + c.execute("select * from push where msg_type != 'm-notifyresp-ind' order by msg_time DESC") + pushlist = c.fetchall() + for line in pushlist: + result = {} + result['PUSHID'] = line['idpush'] + result['Transaction-Id'] = line['transactionid'] + result['Content-Location'] = line['content_location'] + result['Time'] = line['msg_time'] + result['Message-Type'] = line['msg_type'] + c.execute("select * from push_headers WHERE push_id = ?", (line['idpush'],)) + for line2 in c: + result[line2['header']] = line2['value'] + + retlist.append(result) + + return retlist + + def insert_push_message(self, pushlist): + """ Inserts a push message (from a list) + Returns the id of the inserted row + + """ + c = self.conn.cursor() + conn = self.conn + try: + transid = pushlist['Transaction-Id'] + del pushlist['Transaction-Id'] + contentloc = pushlist['Content-Location'] + del pushlist['Content-Location'] + msgtype = pushlist['Message-Type'] + del pushlist['Message-Type'] + except: + print "No transid/contentloc/message-type, bailing out!" + raise + fpath = self.pushdir + transid + vals = (transid, contentloc, msgtype, fpath) + c.execute("insert into push (transactionid, content_location, msg_time, msg_type, file) VALUES (?, ?, datetime('now'), ?, ?)", vals) + pushid = c.lastrowid + conn.commit() + print "inserted row as:", pushid + + for line in pushlist: + vals = (pushid, line, str(pushlist[line])) + c.execute("insert into push_headers (push_id, header, value) VALUES (?, ?, ?)", vals) + conn.commit() + + return pushid + + def insert_push_send(self, pushlist): + """ Inserts a push message (from a list) + Returns the id of the inserted row + + """ + c = self.conn.cursor() + conn = self.conn + try: + transid = pushlist['Transaction-Id'] + del pushlist['Transaction-Id'] + msgtype = pushlist['Message-Type'] + del pushlist['Message-Type'] + except: + print "No transid/message-type, bailing out!" + raise + fpath = self.outdir + transid + vals = (transid, 0, msgtype, fpath) + c.execute("insert into push (transactionid, content_location, msg_time, msg_type, file) VALUES (?, ?, datetime('now'), ?, ?)", vals) + pushid = c.lastrowid + conn.commit() + print "inserted row as:", pushid + + for line in pushlist: + vals = (pushid, line, str(pushlist[line])) + c.execute("insert into push_headers (push_id, header, value) VALUES (?, ?, ?)", vals) + conn.commit() + + return pushid + + def link_push_mms(self, pushid, mmsid): + c = self.conn.cursor() + c.execute("update mms set pushid = ? where id = ?", (pushid, mmsid)) + self.conn.commit() + + + def get_push_message(self, transid): + """ retrieves a push message from the db and returns it as a dict """ + c = self.conn.cursor() + retlist = {} + vals = (transid,) + c.execute("select * from push WHERE transactionid = ? LIMIT 1;", vals) + + for line in c: + pushid = line['idpush'] + retlist['Transaction-Id'] = line['transactionid'] + retlist['Content-Location'] = line['content_location'] + retlist['Message-Type'] = line['msg_type'] + retlist['Time'] = line['msg_time'] + retlist['File'] = line['file'] + retlist['PUSHID'] = pushid + + try: + c.execute("select * from push_headers WHERE push_id = ?;", (pushid, )) + except Exception, e: + raise + + for line in c: + hdr = line['header'] + val = line['value'] + retlist[hdr] = val + + return retlist + + + def is_mms_downloaded(self, transid): + c = self.conn.cursor() + vals = (transid,) + isread = None + c.execute("select * from mms where `transactionid` = ?;", vals) + for line in c: + isread = line['id'] + if isread != None: + return True + else: + return False + + + def is_message_read(self, transactionid): + c = self.conn.cursor() + vals = (transactionid,) + isread = None + c.execute("select read from mms where `transactionid` = ?;", vals) + for line in c: + isread = line['read'] + if isread == 1: + return True + else: + return False + + def insert_mms_message(self, pushid, message, direction=MSG_DIRECTION_IN): + """Takes a MMSMessage object as input, and optionally a MSG_DIRECTION_* + Returns the newly inserted rows id. + + """ + #print direction + mmslist = message.headers + attachments = message.attachments + #mmslist = message + c = self.conn.cursor() + conn = self.conn + try: + transid = mmslist['Transaction-Id'] + del mmslist['Transaction-Id'] + if direction == MSG_DIRECTION_OUT: + basedir = self.outdir + transid + else: + basedir = self.mmsdir + transid + + fpath = basedir + "/message" + size = os.path.getsize(fpath) + except: + print "No transid/message-type, bailing out!" + raise + try: + time = mmslist['Date'] + del mmslist['Date'] + except: + time = "datetime('now')" + isread = MSG_UNREAD + contact = 0 + vals = (pushid, transid, time, isread, direction, size, contact, fpath) + c.execute("insert into mms (pushid, transactionid, msg_time, read, direction, size, contact, file) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", vals) + mmsid = c.lastrowid + conn.commit() + print "inserted row as:", mmsid + + # insert all headers + for line in mmslist: + vals = (mmsid, line, str(mmslist[line])) + c.execute("insert into mms_headers (mms_id, header, value) VALUES (?, ?, ?)", vals) + conn.commit() + attachpaths = basedir + "/" + #print attachpaths + # insert the attachments + for line in attachments: + print line + filetype = gnomevfs.get_mime_type(attachpaths + line) + (fname, ext) = os.path.splitext(line) + hidden = 0 + # These files should be "hidden" from the user + if ext.startswith(".smil") or filetype == "application/smil": + hidden = 1 + vals = (mmsid, line, hidden) + c.execute("insert into attachments (mmsidattach, file, hidden) VALUES (?, ?, ?)", vals) + conn.commit() + #print "inserted", vals + + return mmsid + + def get_mms_attachments(self, transactionid, allFiles=False): + c = self.conn.cursor() + mmsid = self.get_mmsid_from_transactionid(transactionid) + if mmsid != None: + if allFiles == True: + c.execute("select * from attachments where mmsidattach == ?", (mmsid,)) + else: + c.execute("select * from attachments where mmsidattach == ? and hidden == 0", (mmsid,)) + filelist = [] + for line in c: + filelist.append(line['file']) + + return filelist + + + def get_mms_headers(self, transactionid): + c = self.conn.cursor() + mmsid = self.get_mmsid_from_transactionid(transactionid) + retlist = {} + + c.execute("select * from mms WHERE id = ? LIMIT 1;", (mmsid,)) + + for line in c: + retlist['Transaction-Id'] = line['transactionid'] + retlist['Time'] = line['msg_time'] + + if mmsid != None: + c.execute("select * from mms_headers WHERE mms_id = ?;", (mmsid, )) + for line in c: + hdr = line['header'] + val = line['value'] + retlist[hdr] = val + return retlist + + def get_mmsid_from_transactionid(self, transactionid): + c = self.conn.cursor() + c.execute("select * from mms where transactionid == ?", (transactionid, )) + res = c.fetchone() + try: + mmsid = res['id'] + return mmsid + except: + return None + + def get_direction_mms(self, transid): + c = self.conn.cursor() + c.execute("select direction from mms where transactionid = ?", (transid, )) + res = c.fetchone() + try: + direction = res['direction'] + return direction + except: + return None + + + def get_pushid_from_transactionid(self, transactionid): + c = self.conn.cursor() + c.execute("select * from push where transactionid == ?", (transactionid, )) + res = c.fetchone() + try: + mmsid = res['idpush'] + return mmsid + except: + return None + + def delete_mms_message(self, transactionid): + c = self.conn.cursor() + mmsid = self.get_mmsid_from_transactionid(transactionid) + if mmsid != None: + c.execute("delete from mms where id == ?", (mmsid,)) + c.execute("delete from attachments where mmsidattach == ?", (mmsid,)) + c.execute("delete from mms_headers where mms_id == ?", (mmsid,)) + self.conn.commit() + + def delete_push_message(self, transactionid): + c = self.conn.cursor() + pushid = self.get_pushid_from_transactionid(transactionid) + if pushid != None: + c.execute("delete from push where idpush == ?", (pushid,)) + c.execute("delete from push_headers where push_id == ?", (pushid,)) + self.conn.commit() + + +if __name__ == '__main__': + db = DatabaseHandler() + c = db.conn.cursor() + #print db.get_push_list() + #print db.get_mms_headers("1tid46730354431_2zzhez") \ No newline at end of file diff --git a/src/fmms.png b/src/fmms.png new file mode 100644 index 0000000000000000000000000000000000000000..2c1ebf965b936b929b7fb6e9f89939cd77a0538d GIT binary patch literal 11535 zcmYkibyS?c6X?CjLUD>a6n7SPEiQ`|cXxLvu7wsW?p}&}@x|Rrafij-9WLMBd+#~# zKY4QInPg%olVm>8DqtBjWFlk$0DvYZE2#zmz&O9>%|9T#r@0Jv|9uGErL^7Eoh;qG zOk6Dh;^t1K78G(0CRP?|7AEFCF2fc=0048XoTRvh_tLQek~fZ2XZ2}LI*Ay%m=q9c z6ASZ0L^D%s+ZK_#!MdKR(fRVt10Yq&(*xJ!0(e)6QdNb};xV863#E$pN(d4|CHqGh zOZI~}Ceo)(tnlG*>h$zet+$-tMJ1(vr=0)Zo(_-F4^9sc-`w90wYna)lJn@1w+6lw z^EyUbQF_7fIF!Ge`h&7NF18HaAq=)0v3ftEPKo-nlG~f4r1_ii(!MZfy?|cs3 zW$2w|&ehQW)r!8c3F83RFN1GzD}WYqz#dv$Ow95BX9z~93P2Ld13F6QQ=(-SOqr7f zd_@DFnTR?4zmho+G-f|CljRHBGnh0p3Zs6QtoLT(3kA`Aa6V#p16=G7j2->2(I+9W z&VLzNuv&@|C7U?1$YR0`C;n4L-%v$#MqNun^0soPT9+mr`oETK0buPrwX6~kGjbWU z|0iSwfFXXI-wgSTE6@altfbVa;&6ioeb4cD9lHqz^6cA(dl{1KyIqI9=4`pkV7ms3 zFV3q5;2~^c=OqZ)*;CW}xp03TNub+GO>wD+6|i)~J}$c)d-$75E(A^%pouOXNfqs2 zUf?DpCTrZrAa@J-GaJ5;*#osjWLh?EshWOMqIO4liFMa8KFB+?yJNCf&U$;;bXmk%mOB%LaPypV&U0iaI zh}!*;SoP(!dYD6^6x;%N`NuHt^u^q0G6%U8KJ)<>&ccHjMy%BS`Br-%PUd5~7B%3i zeQMFG`2x;lKuWcp8t{OuW`iRg`kffo8$O~BZU)sZmN&@b!~|QwRhvUdTJ>jT#*^!3 zlhTS~D8(I&Mz$sSIYQuzB^iQx zG0ibul$y>Q8hbna4-0JJTWzeKmeqc&MT~xSc&8?;<*1qaQ;K|Mv{VhX#=)@86}C@u zIG{{TqNSy*qaU-)x|L3(Rry7k{QW)8B?LkI)O&T|$xj!a8ewDpX3d?c4uyP9nt{chZvyO>R>YRc;>sE{n!7WFm* z{q|BKr@O6&=WfwhC1v?UW5(q&=CoX4raaQ+G24C)Pkrqmf1-oqY%-^MQiKPI9XiY> z^l)KMy4&pdfGG7cbO9cP&)Jrf3l@HL%sn=y!Z2>T&p|_0#YNWD}Sl) zZHc}{I0Cl1YuKmI@?9;t7p%X)b{}@FO1**2Mf>bT6|?2)!j9f@{uHiUgQ^ZW+0XK4ogi z=R?ud>42BP@5-dJca$^q zNpkzi8I(URJ)iIPc8DTB+dQ1=$&8mJ-991)E(0?DNCvI+1^sF57elXjW=DliUGPmZ zFkwu$+y16;Zvab*rW0ThpRrPqMX^M&dn1lf4)1p!JKh^nCz6K%wE@Qrow`KSb;T6$ zDHuC3c7QW@M?lBAFv$0?X7dta%n3K{XO~`9VmsN)$>U(nx0`zEY$d%E+tbN=cS@xs zd-4e!Pn{n~F|R0}LJPSqh*7le8#2G2QSr;HD=+SEIqGwH`q6e3XHU1mViVL(=wvnN z%V)*!+TW_IVUdgn^Yyw@&SEQG0^l-qb&CQ6e;#A51kwELWxgK08);mWsT-*02TcPK zU~gp2s-71c(MF_uz}2L?)LoNPyw+M(Z!|bX@hr9vYt&zCKQg=q-HpuG(H>V2bV4t- zMr{$$>zEIcX?GBj*9P)f6(Ohrw`o~Fo6FIE2sPMECQnuxh!Q}!qN!2JJ`@+DFdex( zGQcc@2ii*|B*+7e2kvm)*=e^Ctt>)9#M(WKHSg6eduV&n$Z%IMK3;tWjfrl|k$ocQ zmP|Is#v|7PfV=f5DM~`_0+mPudN)2@;&xl=8qjb%F$r=O`ktj*8bJ&Ug-vhn$&0EG zrhZ&wvwfqlv{JsP5?_=n2c?lOs*3n;ZdpBIP>DIx24J#B^=71p#@iuC%z|?OJdV;< zlkmb~$OY1N zS1~;|bsr-#4)#18T&`1HMB&TkLk-a{ZH2UJQ1wyPEGcPz0UT>(oBdnX!-x)xgE$@e z4)sUewSD_+x-3brdwrDko0w8IPCnIBBZ&@HJimO)_;@7xYCiNGl^7osAIcg4m#iTs zjg%1GrLfi40=&Bn&-9_CeA*3g)?tq}L>iz4CiJk90(&7hkYz;?1utdNS2sDVzvzK1 zzg4O6VhGXx^h8U?x)!W;bkw^pMib?vy{)4BVxnDks8L$1G710sn2{5ul-^;9x`E66bIMoO#I=Ls;%xoPXtXL{1Jp7ot>hr5UUss(*b|LTLE15z*lH78RM47}kb8TpJSf_kEGX zFk!?l+}}Peh9X7r+KBtxr86w-+g|wVFlRz7LsuTd5)P>WQ-52p+zDU zqTt2_S63Sc6N{WZxb{_#S#Byvf5S%mLQKwA8p?^M22ET=@rU>g(2wkDi)+IiCk95t4OmazxO(Z8&y~ot9CUwt?Qq% zptg)DWSPzHp&0Jy!jcp`(gCD$l2>R}WY6r^8#*m<0)Op_1$bEvTBs#*y?c|N7i-G} z0*N1Y@#W0xwZqo6$OzcaH&RdT);ylp3X9C#Fpl8>nyu~W2^WIJ1&>b-BWDr{|{`BpxL1U7txtaqQiPY312gz%24VL6S&@siVs(WJ?k zgAfa{>qA|R6;g zp9B5|tPzVe<=uMz^M7rBDR!`+GhEU0DUsV-TTM~(^E_Cayzrz0S8rdRzisFqsCAvlT4Fu?;stTB- zHzzI9fjH2KH%%pSNQ+_B$r^lSF3k9pTKo>rGuc|{Es!j>BRjF!iJSB6s@w`LC&z!4 zOPdV05K@>5aLAd+Y zE6W-zt^jH8Hum8|U1K}0e6C7vDT!r@dt7`=a;v6CMn*jPr4vpLDQ2A*bC-YM^l)iM z&+RgPfdKf?c*j2Hvn1n1?Q{=KILrKkr_7&%R@>|#>AJ( zg^>>()Cyx1~uQMYu3K^=vvS4mHY5T5%rJXK_9mcsI z!@2l)IatWU=>%ECGQtB9ORs8hz{`NUIsMWB9_ngN)G3sxkdY>NafmOim zT1cl1ldW`xp%D)bo+F4wPvZvJa-jyjMBrz~H0|$=)#*=NI<1|Xhy?BUel}(M4>H&F z#l0+#m*P3OWA&61k&|ws_Za&ipcoRkCJ1$m5&6-Fewx}O+1j7oIlRVtsmLn5E&*KW z4fEfK(c;|4KL^>X+*)u_tESO>+oWp1i*Y`(NX>NWPnLLjxVprihiX*%Z{)M{W{hpo zisSsAXk5zefxHm9#ra|-%_-a40bLNtnjy2KH)mlta3({+BP8M;6^SF(Fob#a0_qY3iB<)bRGGWhbW8Rr2Xer(jnos%#e?x|N24oj{N|F?0G}4gFuO> zPqQVV`h4Bw6y!;XlqtLGv$PaFu#bx7uH%u*qB759X)jq;Y&YOrnuPd z2__K6Tf`wr@~EIz4c9G$YxG|R@+UJh1ss2HMSUJJK0r8d*(F;T>92yZiwy4TV`;h+ zXF#6x-!5LkaKYHn@I{*CFgr*kR%VXd<>=_&MDOM_R^p*ATsRdY!RxPJ{Hix& zPeZ07IWj;{M+!@B%%p^B^&>9SlV*1!T+zoeZ%)ZcCrgzIx7nXLdfpHd@FS&$_$V6t zE^eb?g&-AayNw_x$_dGK1#l37RHQ>A`4alK0_>KX(vFBSp$AcMImfIV^7LcO(-D(i z_@Y5`Wc$ywkQ^;U!C@TKm^efLzS(sUJrQBp$-+#M=E8xsV7!Y}*{3#x)*7r`+;ngk zTvXSEEtcgr9>%#Q${^%|!2^pC+xM!mXkt3q7{vbNbB8mc<%mqW zr}vo(9QK@9E!9o&I`Y|V06B!-Fb>D!At~;x>L0WpY{j(ZJri`TTIlyC#30v-5lCKc z%MS*Uicohn|MI@$K=)gncsF}`qJJ8>a1SH#`0X>FD7I8U#$rt*X~x!ZzDe*(^h)Hs zU8AEWot4?B6dc9bPwfEP_j?Dh7cuTLP@-@WNlRZVqWVk+OZYl;`XePNDegYzFRA8Bhla%>yzKXv z*x^Go@bJ$TW1F|%k2Fb*=fkVtNsd}sf{gZ$1yx@FSIIvI%S2JA63PhnFcR>F7@T`S zH`G~)=|3g0jtf;;ETJMcDLQVB$lx;bOo;&ABs=TTDI4=YLd}lW)RdnH(3(S@J~ahU zB(R0M>`(N_O6^1;-Re&d*Dw=7Eb&7?8TK(R*|RO<86y-(9tUSDtcvvrFl3tVgYy(T zgmVGE6`l|e-ZGFlXgK^#Rex?t3+De80xu=q^T6X%+M6#cN)+gPaJh#N3sy+|>%b#( zb-YeqjznR1A}@3-MC>5qY*L}e0!N#78V@pR^uBi(1yEQ-KD2$}pjnC`D31}BEac>^f_XM_`jHb6&pB%S z)a!~tdm73m5cBS562pVd zj(ha=x^crO1(R9O5toEE58*&uWR<>O-8E0U_&0-pZBKcM69%aU00!T?m`)z`!ze14 zr04Ei0RT#otUc_{p<4R~;oM0XA@Mgk{dxQ43tN5mJ#+>Q^EO~EQV>?T!3%caU~PyF=6NMQRPhJbSKp04DnV$7E{&W zb4TKAb9Kn;3s2^uP)q54d~4bvJl%7IBT+~ht-VfLlfp0G>NBy7@_{0; zYA)+d3Hfg}l9ODe)#|Q8p1bL1{vk75&mf_Qw=wBmvXbb6P8cC zAlLJB^zase@rej?C=3`1*T*TFu9G=mp1KtSW6qw-=^QelD~s`Q$_g8bp*Hc+f2ECY z=UglPJ<4?PVS{jra=nuV`1Lvf`SCEw>LUg@M|xuwl#d6&R4E6 zLySBmVom&zb2FHzbbFMoy3u52(bFVh%d&_vKThKdKj(+ttHIR&;!zit9%W^TL>%`o z{B*W97x44gbkx^70i0vVzeX@w#&ZUutsXBZCJe}xnLJKZhQxY@9Zuo zdV=JcqDW4%HW5ndtO-)$^iXDmuJH9y6tb{pCk1%oLiuybzY3ZQ96cJ3*tP>DEk>M@SxPzahEXv+b6!Td_#w(V)6yW(Lg($?@B-Dww_ z-!y~l9*xZ2=!=n+Z!9cxjYB2O=vuRugw~#?hjvLZ!^|}l8!m`6W|V_GRa~n8M$lbF z@^hqD1iQqq&A!CFDTqjhe8Fi{kaXOI@eV(v^TKm!I0=^M_w<;|9-oRI;!0+D!9uf^ zWO~QNO@vRF61ed7Tk&`rJDl&C$rOAx8=*iXvXTEq|I!D2lpcuYb~`ZiMFlo}>}}2gTnQ-2g*sM}7&33CS#eHD;k;`K1J%hwO>hoR0XEO?AAS z!V`#)E%WrR;}(Ry!r4VmP7AJsU37Pu070TRI6U^)a>c|Wc1|{+3ChEd( zmqlrB!S^sgiUnDSF_zpp#EBXs;1A1;g3jd!Z@6>uq4{t0e*mX#)UbD@_<`A0&Tr|m zfwjT@Jjw`Ddx>vIriCaUe#!i`PS+w3!cNbBKI?mHOrXbxdF+J}ipF+1nKNl*86!w$ zpeI>P9d$|~ID5J>coJ#HO@1r*M=Xn3VXsn#2uY#Fk2eW0wc+wRk0`h ztd~=kIGEAWOy{>4`iW+il3Qr8LH7g2w=xVIjew`SrfK>2g!yDqU7SjWfBHKw|CE7O zuFSmta>QA8bOw>s^=h*km1A^YZvoar*vH@Ysh~B6u+1Im7UpZIhD1%VzP%pEonF4h zh>2xY1un&ll)r;4=UnwC}mv&K^Zk3HjNrTv?YC_6@3c4YcrBICFYWNXm8yWgFu~$&e6jBtHaUxnVFjxGY&sjIJ`N7usJC;mJ`JINWuBDIfU%Zw< zTmiMLaWO_F3YKVBMjy=F86kT~N=DQ=MIY2&8R%Qd8w%;7PLI&up_3%fMH3ryp=Lh; zfX^VDZ0Qw*$Se4rDVqqdX?Rcf1aX5}7|HDYq1yxjC%DcXU|eY;rqv7RdXx{e)P8@o z2WLC`o4BLJNQE`B>$L(1d}@3Eo|;ti9M2`vTJ!~mOx>Gep`_?O=q~|PUnB*aGN%RS zs6D!&^;X~Ca{g%m4fyNe5I$DMa{FTAX-OFF)=$|}E8QsWS};+t@jJ9q{iT*d)zcBX zU6eIs_MW<`7#f{&ye!ZoMDwr0XaXDEVqF?E#|rHHvY;+dR8ki)nh_|YF>Xm95sZea zOFnPmLeh7?!u&O=|Cq3D%b=a)cFkBL;W~Lv-W>?MQ!)OhmG8EmKK3U4k}9LCe^_T3 zyIX$u_7jVoa2StRE+<@3hxhPjjJ)_Z;jYqEA#>_u3nbHhQzrd7huAl6vBJIgC(ZI; zXV`m~=XRX63Bqc4?5S^N_Jo9+<2V+U9;H^Ep!Rjhl5oI$~2( zqAQ?|F_Y_G%zh|$3K-<{i?@BC!TSRpHL4ZJ=fD=p`zI&hizK9P0ARp+RmYY$?(3>w zRWfWOk@1YuelV_xXuy!*5yVhMQ86}D67S!UJY7iw6~W(^<#G<7g(F~$N)&QUubV%t zF%vc}vtSJKC)f2u>Lsx0SN%PXUAhLpB2R$!X9duHeJZF07p0!|hVbwybKHVa!QoAc z)nrBUZ}7g0@Uss$+?p@$By0&r-!7f?VM?^kbY%obJTl9=ay*8mkv{AnPgP(vjDshc zBM-w_mzQMr`qRBj{gMgd!A$n>Aw>?Ak-|CVHwT9a1bhNNL-*^2A1^ga?Kya-uwVA7 zFy`mV=BHw~y7w#tUcOMlI8Q#awWfidLkpINlzCIwmW@0bF@^48D2cqci(WfOJvZ?I z8}l!HKO9?&3pva78j<%$U$BoTOf`NvYGN?m7Oob^*!HNxHC=^gX29t?JRO>yC z^p4a5Z?%932j{C3Wr-bdxd=-2ts)%c_N24Fy~c zTz+^9_E-V!ZclO^acjmI;-OiBn1q#OTH?QwCu@Hc{zh0~yCC=eTSrSRq&#a#g@C`ma?;RNq} zF2~o0T(oGE+I~x#9liX@G#voSv)_&>?KtHS#7V{w@+Exh$tm@)nKVZQot0noz=jk{%DAIZ|G|Gma?7vi9mP^S#v_D4c<=cg0+_Z_q`lr!Q|jv;`LDT-Do6@)rssgSlSTtiGu zg#ztWhzR^FVjgVjY6whXJM8qBJu880qPGcfJkt_8Ckf>h)Bx^?UZ09jW55y|j}l z#VQg~5Hah*3eI1znzm6si zS(F3}=DhCNS{*W!qU`xtvu#a`%r=>Q;6e@xVU3_a@eV>s2jHMk7VJ2Cu|p-WL8{_j6%K^Ix^!jwU^`BdVkx zUegP$^#8IwAbo#by_P$;aM2UtpBC6|E_~g)d}VN>9$uyU-gKL;khAu530FTT5_8o_ z$2kLE(drgx8~3AhPe{Ab`mkE&nS*7#&i|M%cHsb&_bWz<+^%N5xlQPS|Bx=>7BDQ*RBJ_q^hYaY4MTBV+S=K5m+D&3?T6t z04o-0;^@%`+SkLLCv$9`j&@%BR(eBtH2}-aT<=%!<@4IealDV&e2zUx3BIE=+ZAv7 z*8{cNtO3;gI=;vOf*xvO#B)0TtbfA5S-2yu!J^C;^Yf@$yrA72uCTwulWh=YJf_PJ zMO(6z6!MMs#v2as<_sgn@s*Go_jHqsM1++x$-=xUkd&_@F+aJ@LPyV#0PG>MgZU1P zufq~-&5Eat=3Iu?;i1jwX5gN|*IaWf#BY>DM~NCxj9h>FyLD@(&3BH;DO6mp;vG&) z6?!qJ*?%Ct6XtsqO-d}ngBrBQ_kscOg^}G-V@Fn z3X_$rYTcZh4Wq*tGX*0;3A67B`D%qF};i?o$Bn+~;lT+;PIlM-fUiz5eM^97!R-WC4@pfUGX6s1_ zp5Oi%uJbj88d(SW%#lTOvEH;>Hpryea-jY{x)neM1mF{vjE2~^p^sXMw|++4U_n)% zgZ(2F$#4|h5DKFr4)-d(m%+WK<}*-aLt2xRr_w>jzJ8)O8VvMBA9iWgc?G zOOl4L_<((CC6dZ_)HVeZaBAKZg0R34zeG64w0~^H1XHxQXD&u|pn-`Dqg3QpWv{|| z`{>gX%O7hn +@license: GNU GPL +""" +import os + +try: + import gnome.gconf as gconf +except: + import gconf + + +class fMMS_config: + + + def __init__(self): + self._fmmsdir = "/apps/fmms/" + self.client = gconf.client_get_default() + self.client.add_dir(self._fmmsdir, gconf.CLIENT_PRELOAD_NONE) + if self.get_apn() == None: + self.set_apn("bogus") + if self.get_pushdir() == None: + self.set_pushdir("/home/user/.fmms/push/") + if self.get_mmsdir() == None: + self.set_mmsdir("/home/user/.fmms/mms/") + if self.get_outdir() == None: + self.set_outdir("/home/user/.fmms/sent/") + if self.get_imgdir() == None: + self.set_imgdir("/home/user/.fmms/temp/") + if self.get_mmsc() == None: + self.set_mmsc("http://") + if self.get_phonenumber() == None: + self.set_phonenumber("000") + if self.get_img_resize_width() == None: + self.set_img_resize_width(0) + if self.get_version() == None: + self.set_version("Unknown") + if self.get_db_path() == None: + self.set_db_path("/home/user/.fmms/mms.db") + # Create dirs, for good measures + if not os.path.isdir(self.get_pushdir()): + os.makedirs(self.get_pushdir()) + + if not os.path.isdir(self.get_mmsdir()): + os.makedirs(self.get_mmsdir()) + + if not os.path.isdir(self.get_outdir()): + os.makedirs(self.get_outdir()) + + if not os.path.isdir(self.get_imgdir()): + os.makedirs(self.get_imgdir()) + + def read_config(self): + pass + + def set_db_path(self, path): + self.client.set_string(self._fmmsdir + "db", path) + + def get_db_path(self): + return self.client.get_string(self._fmmsdir + "db") + + def get_version(self): + return self.client.get_string(self._fmmsdir + "version") + + def set_version(self, val): + self.client.set_string(self._fmmsdir + "version", val) + + def set_firstlaunch(self, val): + self.client.set_int(self._fmmsdir + "firstlaunch", val) + + def get_firstlaunch(self): + return self.client.get_int(self._fmmsdir + "firstlaunch") + + def set_img_resize_width(self, width): + try: + width = int(width) + except ValueError: + width = 0 + self.client.set_int(self._fmmsdir + "img_resize_width", width) + + def get_img_resize_width(self): + return self.client.get_int(self._fmmsdir + "img_resize_width") + + def set_phonenumber(self, number): + self.client.set_string(self._fmmsdir + "phonenumber", number) + + def get_phonenumber(self): + return self.client.get_string(self._fmmsdir + "phonenumber") + + def set_pushdir(self, path): + self.client.set_string(self._fmmsdir + "pushdir", path) + + def get_pushdir(self): + return self.client.get_string(self._fmmsdir + "pushdir") + + def set_mmsdir(self, path): + self.client.set_string(self._fmmsdir + "mmsdir", path) + + def get_mmsdir(self): + return self.client.get_string(self._fmmsdir + "mmsdir") + + def set_outdir(self, path): + self.client.set_string(self._fmmsdir + "outdir", path) + + def get_outdir(self): + return self.client.get_string(self._fmmsdir + "outdir") + + def set_imgdir(self, path): + self.client.set_string(self._fmmsdir + "imgdir", path) + + def get_imgdir(self): + return self.client.get_string(self._fmmsdir + "imgdir") + + """ note this takes the *id* from gconf and not the *display name* """ + def set_apn(self, apn): + #apn = apn.replace(" ", "@32@") + #self.client.set_string(self._fmmsdir + "apn_nicename", apn) + self.client.set_string(self._fmmsdir + "apn", apn) + + def get_apn_nicename(self): + #return self.client.get_string(self._fmmsdir + "apn_nicename") + apn = self.client.get_string(self._fmmsdir + "apn") + return self.client.get_string('/system/osso/connectivity/IAP/' + apn + '/name') + + def get_apn(self): + return self.client.get_string(self._fmmsdir + "apn") + + def set_mmsc(self, mmsc): + self.client.set_string(self._fmmsdir + "mmsc", mmsc) + + def get_mmsc(self): + return self.client.get_string(self._fmmsdir + "mmsc") + + def get_proxy_from_apn(self): + apn = self.get_apn() + proxy = self.client.get_string('/system/osso/connectivity/IAP/' + apn + '/proxy_http') + proxyport = self.client.get_int('/system/osso/connectivity/IAP/' + apn + '/proxy_http_port') + return proxy, proxyport + + def get_gprs_apns(self): + # get all IAP's + dirs = self.client.all_dirs('/system/osso/connectivity/IAP') + apnlist = [] + for subdir in dirs: + # get all sub entries.. this might be costy? + all_entries = self.client.all_entries(subdir) + # this is a big loop as well, possible to make it easier? + # make this faster + for entry in all_entries: + (path, sep, shortname) = entry.key.rpartition('/') + # this SHOULD always be a int + if shortname == 'type' and entry.value.type == gconf.VALUE_STRING and entry.value.get_string() == "GPRS": + # split it so we can get the id + #(spath, sep, apnid) = path.rpartition('/') + apname = self.client.get_string(path + '/name') + apnlist.append(apname) + + return apnlist + + """ get the gconf alias for the name, be it the real name or + an arbitrary string """ + def get_apnid_from_name(self, apnname): + # get all IAP's + dirs = self.client.all_dirs('/system/osso/connectivity/IAP') + + for subdir in dirs: + # get all sub entries.. this might be costy? + all_entries = self.client.all_entries(subdir) + # this is a big loop as well, possible to make it easier? + for entry in all_entries: + (path, sep, shortname) = entry.key.rpartition('/') + + # this SHOULD always be a string + if shortname == 'name': + if entry.value.type == gconf.VALUE_STRING: + _value = entry.value.get_string() + if _value == apnname: + # split it so we can get the id + (spath, sep, apnid) = path.rpartition('/') + return apnid + + return None + + +if __name__ == '__main__': + config = fMMS_config() + #config.get_apnid_from_name("Tele2 MMS") + #config.set_apn("bogus") + #config.set_pushdir("/home/user/.fmms/push/") + #config.set_mmsdir("/home/user/.fmms/mms/") + #config.set_mmsc("http://bogus") + #print config.get_apn() + #print config.get_apn_nicename() \ No newline at end of file diff --git a/src/fmms_gui.py b/src/fmms_gui.py new file mode 100644 index 0000000..690c788 --- /dev/null +++ b/src/fmms_gui.py @@ -0,0 +1,473 @@ +#!/usr/bin/env python2.5 +# -*- coding: utf-8 -*- +""" Main-view UI for fMMS + +@author: Nick Leppänen Larsson +@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 + ' (' + sender + ')' + 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 + ' ' + mtime + '\n' + fname + '', photo, fname]) + + """ lets call it quits! """ + def quit(self, *args): + gtk.main_quit() + + + """ forces ui update, kinda... god this is AWESOME """ + def force_ui_update(self): + while gtk.events_pending(): + gtk.main_iteration(False) + + + """ delete push message """ + def delete_push(self, fname): + self.cont.delete_push_message(fname) + + + """ delete mms message (eg for redownload) """ + def delete_mms(self, fname): + self.cont.delete_mms_message(fname) + + """ delete push & mms """ + def delete_push_mms(self, fname): + try: + self.cont.wipe_message(fname) + banner = hildon.hildon_banner_show_information(self.window, "", "fMMS: Message deleted") + except Exception, e: + print "Exception caught:" + print type(e), e + raise + banner = hildon.hildon_banner_show_information(self.window, "", "fMMS: Failed to delete message.") + + + """ action on delete contextmenu click """ + def liststore_delete_clicked(self, widget): + dialog = gtk.Dialog() + dialog.set_title("Confirm") + dialog.add_button(gtk.STOCK_YES, 1) + dialog.add_button(gtk.STOCK_NO, 0) + label = gtk.Label("Are you sure you want to delete the message?") + dialog.vbox.add(label) + dialog.show_all() + ret = dialog.run() + if ret == 1: + (model, miter) = self.treeview.get_selection().get_selected() + # the 4th value is the filename (start counting at 0) + filename = model.get_value(miter, 3) + print "deleting", filename + self.delete_push_mms(filename) + self.liststore.remove(miter) + dialog.destroy() + return + + """ action on redl contextmenu click """ + def liststore_redl_clicked(self, widget): + hildon.hildon_gtk_window_set_progress_indicator(self.window, 1) + dialog = gtk.Dialog() + dialog.set_title("WARNING") + dialog.add_button(gtk.STOCK_YES, 1) + dialog.add_button(gtk.STOCK_NO, 0) + label = gtk.Label("If the message is no longer on your MMSC,\n the message will be lost. Continue?") + dialog.vbox.add(label) + dialog.show_all() + ret = dialog.run() + dialog.destroy() + self.force_ui_update() + + if ret == 1: + (model, miter) = self.treeview.get_selection().get_selected() + # the 4th value is the filename (start counting at 0) + filename = model.get_value(miter, 3) + print "redownloading", filename + try: + self.delete_mms(filename) + banner = hildon.hildon_banner_show_information(self.window, "", "fMMS: Trying to download MMS...") + self.force_ui_update() + + # TODO: FIXME + + self.cont.get_mms_from_push(filename) + self.show_mms(self.treeview, model.get_path(miter)) + except Exception, e: + print type(e), e + #raise + banner = hildon.hildon_banner_show_information(self.window, "", "fMMS: Operation failed") + hildon.hildon_gtk_window_set_progress_indicator(self.window, 0) + return + + """ long press on image creates this """ + def liststore_mms_menu(self): + menu = gtk.Menu() + menu.set_title("hildon-context-sensitive-menu") + + redlItem = gtk.MenuItem("Redownload") + menu.append(redlItem) + redlItem.connect("activate", self.liststore_redl_clicked) + redlItem.show() + + separator = gtk.MenuItem() + menu.append(separator) + separator.show() + + openItem = gtk.MenuItem("Delete") + menu.append(openItem) + openItem.connect("activate", self.liststore_delete_clicked) + openItem.show() + + menu.show_all() + return menu + + """ show the selected mms """ + def show_mms(self, treeview, path): + # Show loading indicator + hildon.hildon_gtk_window_set_progress_indicator(self.window, 1) + banner = hildon.hildon_banner_show_information(self.window, "", "fMMS: Opening message") + self.force_ui_update() + + print path + model = treeview.get_model() + miter = model.get_iter(path) + # the 4th value is the transactionid (start counting at 0) + transactionid = model.get_value(miter, 3) + + try: + viewer = fMMSViewer.fMMS_Viewer(transactionid) + except Exception, e: + print type(e), e + #raise + + + hildon.hildon_gtk_window_set_progress_indicator(self.window, 0) + + def run(self): + self.window.show_all() + gtk.main() + +if __name__ == "__main__": + app = fMMS_GUI() + app.run() \ No newline at end of file diff --git a/src/fmms_sender_ui.py b/src/fmms_sender_ui.py new file mode 100644 index 0000000..83ca048 --- /dev/null +++ b/src/fmms_sender_ui.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python2.5 +# -*- coding: utf-8 -*- +""" Sender UI for fMMS + +@author: Nick Leppänen Larsson +@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" + str(size) + "kB") + except TypeError: + self.lSize.set_markup("") + + def quit(self, *args): + gtk.main_quit() + + def run(self): + self.window.show_all() + gtk.main() + +if __name__ == "__main__": + app = fMMS_GUI() + app.run() \ No newline at end of file diff --git a/src/fmms_viewer.py b/src/fmms_viewer.py new file mode 100644 index 0000000..74f13ca --- /dev/null +++ b/src/fmms_viewer.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python2.5 +# -*- coding: utf-8 -*- +""" Message-viewer UI for fMMS + +@author: Nick Leppänen Larsson +@license: GNU GPL +""" +import os + +import gtk +import hildon +import gobject +import osso +from gnome import gnomevfs + +from wappushhandler import PushHandler +import fmms_config as fMMSconf +import controller as fMMSController + + +class fMMS_Viewer(hildon.Program): + + def __init__(self, fname, standalone=False): + self.cont = fMMSController.fMMS_controller() + self.standalone = standalone + self.config = fMMSconf.fMMS_config() + self._mmsdir = self.config.get_mmsdir() + self._pushdir = self.config.get_pushdir() + self._outdir = self.config.get_outdir() + self.osso_c = osso.Context("fMMS", "0.1.0", False) + + self.window = hildon.StackableWindow() + self.window.set_title("Showing MMS: " + fname) + self.window.connect("delete_event", self.quit) + + vbox = gtk.VBox() + pan = hildon.PannableArea() + pan.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH) + + self._parse_mms(fname, vbox) + + pan.add_with_viewport(vbox) + self.window.add(pan) + + mms_menu = self.create_mms_menu(fname) + self.window.set_app_menu(mms_menu) + self.window.show_all() + + """ lets call it quits! """ + def quit(self, *args): + self.window.destroy() + if self.standalone == True: + gtk.main_quit() + + """ forces ui update, kinda... god this is AWESOME """ + def force_ui_update(self): + while gtk.events_pending(): + gtk.main_iteration(False) + + """ create app menu for mms viewing window """ + def create_mms_menu(self, fname): + menu = hildon.AppMenu() + + headers = hildon.GtkButton(gtk.HILDON_SIZE_AUTO) + headers.set_label("Headers") + headers.connect('clicked', self.mms_menu_button_clicked, fname) + + menu.append(headers) + + menu.show_all() + + return menu + + """ actions for mms menu """ + def mms_menu_button_clicked(self, button, fname): + buttontext = button.get_label() + if buttontext == "Headers": + ret = self.create_headers_dialog(fname) + + """ show headers in a dialog """ + def create_headers_dialog(self, fname): + dialog = gtk.Dialog() + dialog.set_title("Headers") + + dialogVBox = gtk.VBox() + + pan = hildon.PannableArea() + #pan.set_property("mov-mode", hildon.MOVEMENT_MODE_BOTH) + pan.set_property("size-request-policy", hildon.SIZE_REQUEST_CHILDREN) + + allVBox = gtk.VBox() + #leftBox = gtk.VBox() + #rightBox = gtk.VBox() + headerlist = self.cont.get_mms_headers(fname) + for line in headerlist: + hbox = gtk.HBox() + titel = gtk.Label(line) + titel.set_alignment(0, 0) + titel.set_width_chars(18) + label = gtk.Label(headerlist[line]) + label.set_line_wrap(True) + label.set_alignment(0, 0) + hbox.pack_start(titel, False, False, 0) + hbox.pack_start(label, False, False, 0) + allVBox.pack_start(hbox) + #leftBox.pack_start(titel, False, False, 0) + #rightBox.pack_start(label, False, False, 0) + #pan.add(label) + + #allHBox.pack_start(leftBox, False, False, 0) + #allHBox.pack_start(rightBox, True, True, 0) + allVBox.show_all() + + pan.add_with_viewport(allVBox) + dialog.vbox.add(pan) + dialog.vbox.show_all() + ret = dialog.run() + + dialog.destroy() + return ret + + """ parse mms and push each part to the container + fetches the mms if its not downloaded """ + def _parse_mms(self, filename, container): + hildon.hildon_gtk_window_set_progress_indicator(self.window, 1) + self.force_ui_update() + + if not self.cont.is_fetched_push_by_transid(filename): + self.cont.get_mms_from_push(filename) + + textview = gtk.TextView() + textview.set_editable(False) + textview.set_cursor_visible(False) + textview.set_wrap_mode(gtk.WRAP_WORD) + textbuffer = gtk.TextBuffer() + direction = self.cont.get_direction_mms(filename) + if direction == fMMSController.MSG_DIRECTION_OUT: + path = self._outdir + filename + else: + path = self._mmsdir + filename + filelist = self.cont.get_mms_attachments(filename) + print "filelist:", filelist + for fname in filelist: + (name, ext) = os.path.splitext(fname) + fnpath = os.path.join(path, fname) + isText = False + isImage = False + try: + filetype = gnomevfs.get_mime_type(fnpath) + print "filetype:", filetype + if filetype != None: + if filetype.startswith("image") or filetype.startswith("sketch"): + isImage = True + if filetype.startswith("text"): + isText = True + except Exception, e: + filetype = None + print type(e), e + + if isImage or ext == ".wbmp": + """ insert the image in an eventbox so we can get signals """ + ebox = gtk.EventBox() + img = gtk.Image() + img.set_from_file(path + "/" + fname) + fullpath = path + "/" + fname + ebox.add(img) + ## TODO: make this menu proper without this ugly + # args passing + menu = self.mms_img_menu(fullpath) + ebox.tap_and_hold_setup(menu) + container.add(ebox) + elif isText or ext.startswith(".txt"): + fp = open(path + "/" + fname, 'r') + contents = fp.read() + fp.close() + #print contents + textbuffer.insert(textbuffer.get_end_iter(), contents) + elif name != "message" and name != "headers" and not ext.startswith(".smil") and filetype != "application/smil": + attachButton = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_HORIZONTAL, fname) + attachButton.connect('clicked', self.mms_img_clicked, fnpath) + container.pack_end(attachButton, False, False, 0) + + textview.set_buffer(textbuffer) + container.add(textview) + hildon.hildon_gtk_window_set_progress_indicator(self.window, 0) + + + """ action on click on image/button """ + def mms_img_clicked(self, widget, data): + print widget, data + path = "file://" + data + # gnomevfs seems to be better than mimetype when guessing mimetype for us + file_mimetype = gnomevfs.get_mime_type(path) + if file_mimetype != None: + if file_mimetype.startswith("video") or file_mimetype.startswith("audio"): + rpc = osso.Rpc(self.osso_c) + rpc.rpc_run("com.nokia.mediaplayer", "/com/nokia/mediaplayer", "com.nokia.mediaplayer", "mime_open", (str, path)) + elif file_mimetype.startswith("image"): + rpc = osso.Rpc(self.osso_c) + rpc.rpc_run("com.nokia.image_viewer", "/com/nokia/image_viewer", "com.nokia.image_viewer", "mime_open", (str, path)) + else: + # TODO: how to solve this? + # move .mms to ~/MyDocs? change button to copy file to ~/MyDocs? + #rpc = osso.Rpc(self.osso_c) + #path = os.path.dirname(path).replace("file://", "") + print path + #rpc.rpc_run("com.nokia.osso_filemanager", "/com/nokia/osso_filemanager", "com.nokia.osso_filemanager", "open_folder", (str, path)) + + + """ long press on image creates this """ + def mms_img_menu(self, data=None): + print "menu created" + menu = gtk.Menu() + menu.set_title("hildon-context-sensitive-menu") + + openItem = gtk.MenuItem("Open") + menu.append(openItem) + openItem.connect("activate", self.mms_img_clicked, data) + openItem.show() + menu.show_all() + return menu + + def run(self): + self.window.show_all() + gtk.main() + +if __name__ == "__main__": + app = fMMS_Viewer("fname", True) + app.run() \ No newline at end of file diff --git a/src/fmmsd.py b/src/fmmsd.py new file mode 100644 index 0000000..1634b4d --- /dev/null +++ b/src/fmmsd.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python2.5 +# -*- coding: utf-8 -*- +""" daemon for fMMS + +@author: Nick Leppänen Larsson +@license: GNU GPL +""" +import dbus +import gobject +import dbus.mainloop.glib +import dbus.service + +from wappushhandler import PushHandler + +class MMSHandler(dbus.service.Object): + def __init__(self): + # Here the service name + bus_name = dbus.service.BusName('se.frals.mms', bus=dbus.SystemBus()) + # Here the object path + dbus.service.Object.__init__(self, bus_name, '/se/frals/mms') + + + # TODO: This should filter by bearer and not number of arguments, really, it should. + # Here the interface name, and the method is named same as on dbus. + """ According to wappushd.h SMS PUSH is one less argument """ + @dbus.service.method(dbus_interface='com.nokia.WAPPushHandler') + def HandleWAPPush(self, bearer, source, srcport, dstport, header, payload): + handler = PushHandler() + ret = handler._incoming_sms_push(source, srcport, dstport, header, payload) + return 0 + + """ According to wappushd.h IP PUSH is one more argument + @dbus.service.method(dbus_interface='com.nokia.WAPPushHandler') + def HandleWAPPush(self, bearer, source, dest, srcport, dstport, header, payload): + handler = PushHandler() + ret = handler._incoming_ip_push(source, dest, srcport, dstport, header, payload) + return 0 + """ + +dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) +bus = dbus.SystemBus() +loop = gobject.MainLoop() +server = MMSHandler() +loop.run() diff --git a/src/mms/COPYING b/src/mms/COPYING new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/src/mms/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/src/mms/COPYING.LESSER b/src/mms/COPYING.LESSER new file mode 100644 index 0000000..e8bec28 --- /dev/null +++ b/src/mms/COPYING.LESSER @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser 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 +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/src/mms/WSP.py b/src/mms/WSP.py new file mode 100644 index 0000000..980d190 --- /dev/null +++ b/src/mms/WSP.py @@ -0,0 +1,337 @@ +# -*- 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 use in Maemo5/Fremantle on the Nokia N900. + +@author: Francois Aucamp +@author: Nick Leppänen Larsson +@license: GNU LGPL +""" +from WTP import WTP +import sys +import array +import socket, time + +from wsp_pdu import Decoder, Encoder, WSPEncodingAssignments +from iterator import PreviewIterator + +class WSP: + """ This class implements a very limited subset of the WSP layer. + + It uses python-mms's WSP PDU encoding module for almost all encodings, + and essentially just glues it together into a limited WSP layer. """ + def __init__(self, wapGatewayHost, wapGatewayPort=9201): + self.serverSessionID = -1 + self.capabilities = {'ClientSDUSize': 261120, + 'ServerSDUSize': 261120} + self.headers = [('User-Agent', 'Nokia N900'), + ('Accept', 'text/plain'), + ('Accept', 'application/vnd.wap.mms-message')] + self.wtp = WTP(wapGatewayHost, wapGatewayPort) + + def connect(self): + """ Sends a WSP Connect message to the gateway, including any + configured capabilities. It also updates the WSP object to reflect + the status of the WSP connection """ + print '>> WSP: Connect' + response = self.wtp.invoke(self.encodeConnectPDU()) + self._decodePDU(response) + + + def disconnect(self): + """ Sends a WSP Connect message to the gateway, including any + configured capabilities. It also updates the WSP object to reflect + the status of the WSP connection """ + print '>> WSP: Disconnect' + self.wtp.invoke(self.encodeDisconnectPDU(self.serverSessionID)) + self.serverSessionID = -1 + + def post(self, uri, contentType, data): + """ Performs a WSP POST """ + if type(data) == array.array: + data = data.tolist() + print '>> WSP: Post' + pdu = self.encodePostPDU(uri, contentType) + data + response = self.wtp.invoke(pdu) + self._decodePDU(response) + + def get(self, uri): + """ Performs a WSP GET """ + response = self.wtp.invoke(self.encodeGetPDU(uri)) + self._decodePDU(response) + + def encodeConnectPDU(self): + """ Sends a WSP connect request (S-Connect.req, i.e. Connect PDU) to + the WAP gateway + + This PDU is described in WAP-230, section 8.2.2, and is sent to + initiate the creation of a WSP session. Its field structure:: + + Field Name Type Description + =============== ================= ================= + Version uint8 WSP protocol version + CapabilitiesLen uintvar Length of the Capabilities field + HeadersLen uintvar Length of the Headers field + Capabilities + octets S-Connect.req::Requested Capabilities + Headers + octets S-Connect.req::Client Headers + """ + pdu = [] + pdu.append(0x01) # Type: "Connect" + # Version field - we are using version 1.0 + pdu.extend(Encoder.encodeVersionValue('1.0')) + # Add capabilities + capabilities = [] + for capability in self.capabilities: + # Unimplemented/broken capabilities are not added + try: + exec 'capabilities.extend(WSP._encodeCapabilty%s(self.capabilities[capability]))' % capability + except: + pass + # Add and encode headers + headers = array.array('B') + for hdr, hdrValue in self.headers: + headers.extend(Encoder.encodeHeader(hdr, hdrValue)) + # Add capabilities and headers to PDU (including their lengths) + pdu.extend(Encoder.encodeUintvar(len(capabilities))) + pdu.extend(Encoder.encodeUintvar(len(headers))) + pdu.extend(capabilities) + pdu.extend(headers) + return pdu + + @staticmethod + def encodePostPDU(uri, contentType): + """ Builds a WSP POST PDU + + @note: This method does not add the part at the end of the PDU; + this should be appended manually to the result of this method. + + The WSP Post PDU is defined in WAP-230, section 8.2.3.2:: + Table 10. Post Fields + Name Type Source + ========== ======================== ======================================== + UriLen uintvar Length of the URI field + HeadersLen uintvar Length of the ContentType and Headers fields + combined + Uri UriLen octets S-MethodInvoke.req::Request URI or + S-Unit-MethodInvoke.req::Request URI + ContentType multiple octets S-MethodInvoke.req::Request Headers or + S-Unit-MethodInvoke.req::Request Headers + Headers (HeadersLen - length of S-MethodInvoke.req::Request Headers or + ContentType) octets S-Unit-MethodInvoke.req::Request Headers + Data multiple octets S-MethodInvoke.req::Request Body or + S-Unit-MethodInvoke.req::Request Body + + """ + #TODO: remove this, or make it dynamic or something: + headers = [('Accept', 'application/vnd.wap.mms-message')] + pdu = [0x60] # Type: "Post" + # UriLen: + pdu.extend(Encoder.encodeUintvar(len(uri))) + # HeadersLen: + encodedContentType = Encoder.encodeContentTypeValue(contentType, {}) + encodedHeaders = [] + for hdr, hdrValue in headers: + encodedHeaders.extend(Encoder.encodeHeader(hdr, hdrValue)) + headersLen = len(encodedContentType) + len(encodedHeaders) + pdu.extend(Encoder.encodeUintvar(headersLen)) + # URI - this should NOT be null-terminated (according to WAP-230 section 8.2.3.2) + for char in uri: + pdu.append(ord(char)) + # Content-Type: + pdu.extend(encodedContentType) + # Headers: + pdu.extend(encodedHeaders) + return pdu + + @staticmethod + def encodeGetPDU(uri): + """ Builds a WSP GET PDU + + The WSP Get PDU is defined in WAP-230, section 8.2.3.1:: + Name Type Source + ====== ============ ======================= + URILen uintvar Length of the URI field + URI URILen octets S-MethodInvoke.req::Request URI or + S-Unit-MethodInvoke.req::Request URI + Headers multiple S-MethodInvoke.req::Request Headers or + octets S-Unit-MethodInvoke.req::Request Headers + """ + pdu = self + # UriLen: + pdu.extend(Encoder.encodeUintvar(len(uri))) + # URI - this should NOT be null-terminated (according to WAP-230 section 8.2.3.1) + for char in uri: + pdu.append(ord(char)) + headers = [] + #TODO: not sure if these should go here... + for hdr, hdrValue in pdu.headers: + headers.extend(Encoder.encodeHeader(hdr, hdrValue)) + pdu.extend(headers) + return pdu + + @staticmethod + def encodeDisconnectPDU(serverSessionID): + """ Builds a WSP Disconnect PDU + + The Disconnect PDU is sent to terminate a session. It structure is + defined in WAP-230, section 8.2.2.4:: + Name Type Source + =============== ======= =================== + ServerSessionId uintvar Session_ID variable + """ + pdu = [0x05] # Type: "Disconnect" + pdu.extend(Encoder.encodeUintvar(serverSessionID)) + return pdu + + def _decodePDU(self, byteIter): + """ Reads and decodes a WSP 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. + """ + pduType = Decoder.decodeUint8(byteIter) + if pduType not in WSPEncodingAssignments.wspPDUTypes: + #TODO: maybe raise some error or something + print 'Error - unknown WSP PDU type: %s' % hex(pduType) + raise TypeError + pduType = WSPEncodingAssignments.wspPDUTypes[pduType] + print '<< WSP: %s' % pduType + pduValue = None + try: + exec 'pduValue = self._decode%sPDU(byteIter)' % pduType + except: + print 'A fatal error occurred, probably due to an unimplemented feature.\n' + raise + return pduValue + + def _decodeConnectReplyPDU(self, byteIter): + """ The WSP ConnectReply PDU is sent in response to a S-Connect.req + PDU. It is defined in WAP-230, section 8.2.2.2. + + All WSP PDU headers start with a type (uint8) byte (we do not + implement connectionless WSP, thus we don't prepend TIDs to the WSP + header). The WSP PDU types are specified in WAP-230, table 34. + + ConnectReply PDU Fields:: + Name Type Source + =============== ================= ===================================== + ServerSessionId Uintvar Session_ID variable + CapabilitiesLen Uintvar Length of Capabilities field + HeadersLen Uintvar Length of the Headers field + Capabilities S-Connect.res::Negotiated Capabilities + octets + Headers S-Connect.res::Server Headers + octets + + @param byteIters: an iterator over the sequence of bytes containing + the ConnectReply PDU + @type bytes: mms.iterator.PreviewIterator + """ + self.serverSessionID = Decoder.decodeUintvar(byteIter) + capabilitiesLen = Decoder.decodeUintvar(byteIter) + headersLen = Decoder.decodeUintvar(byteIter) + # Stub to decode capabilities (currently we ignore these) + cFieldBytes = [] + for i in range(capabilitiesLen): + cFieldBytes.append(byteIter.next()) + cIter = PreviewIterator(cFieldBytes) + # Stub to decode headers (currently we ignore these) + hdrFieldBytes = [] + for i in range(headersLen): + hdrFieldBytes.append(byteIter.next()) + hdrIter = PreviewIterator(hdrFieldBytes) + + + def _decodeReplyPDU(self, byteIter): + """ The WSP Reply PDU is the generic response PDU used to return + information from the server in response to a request. It is defined in + WAP-230, section 8.2.3.3. + + All WSP PDU headers start with a type (uint8) byte (we do not + implement connectionless WSP, thus we don't prepend TIDs to the WSP + header). The WSP PDU types are specified in WAP-230, table 34. + + Reply PDU Fields:: + Name Type + =============== ================= + Status Uint8 + HeadersLen Uintvar + ContentType multiple octects + Headers - len(ContentType) octets + Data multiple octects + + @param byteIters: an iterator over the sequence of bytes containing + the ConnectReply PDU + @type bytes: mms.iterator.PreviewIterator + """ + status = Decoder.decodeUint8(byteIter) + headersLen = Decoder.decodeUintvar(byteIter) + + # Stub to decode headers (currently we ignore these) + hdrFieldBytes = [] + for i in range(headersLen): + hdrFieldBytes.append(byteIter.next()) + hdrIter = PreviewIterator(hdrFieldBytes) + contentType, parameters = Decoder.decodeContentTypeValue(hdrIter) + while True: + try: + hdr, value = Decoder.decodeHeader(hdrIter) + except StopIteration: + break + # Read the data + data = [] + while True: + try: + data.append(byteIter.next()) + except StopIteration: + break + + @staticmethod + def _encodeCapabiltyClientSDUSize(size): + """ Encodes the Client-SDU-Size capability (Client Service Data Unit); + described in WAP-230, section 8.3.2.1 + + This defines the maximum size (in octets) of WTP Service Data Units + + @param size: The requested SDU size to negotiate (in octets) + @type size: int + """ + identifier = Encoder.encodeShortInteger(0x00) + parameters = Encoder.encodeUintvar(size) + length = Encoder.encodeUintvar(len(identifier) + len(parameters)) + capability = length + capability.extend(identifier) + capability.extend(parameters) + return capability + + @staticmethod + def _encodeCapabilityServerSDUSize(size): + """ Encodes the Client-SDU-Size capability (Server Service Data Unit); + described in WAP-230, section 8.3.2.1. + + This defines the maximum size (in octets) of WTP Service Data Units + + @param size: The requested SDU size to negotiate (in octets) + @type size: int + """ + identifier = Encoder.encodeShortInteger(0x01) + parameters = Encoder.encodeUintvar(size) + length = Encoder.encodeUintvar(len(identifier) + len(parameters)) + capability = length + capability.extend(identifier) + capability.extend(parameters) + return capability diff --git a/src/mms/WTP.py b/src/mms/WTP.py new file mode 100644 index 0000000..6bd02f2 --- /dev/null +++ b/src/mms/WTP.py @@ -0,0 +1,360 @@ +# -*- coding: utf-8 -*- +# +# This library is free software, distributed under the terms of +# the GNU Lesser General Public License Version 2. +# See the COPYING.LESSER file included in this archive +# +# The docstrings in this module contain epytext markup; API documentation +# may be created by processing this file with epydoc: http://epydoc.sf.net +""" +Library for WAP transport, original by Francois Aucamp, modified by Nick Leppänen Larsson +for standalone use. + +@author: Francois Aucamp +@author: Nick Leppänen Larsson +@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:, int:) + @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 + + From WAP-224, section 7.8.1: The TID is 16-bits but the high order bit + is used to indicate the direction. This means that the TID space is + 2**15. The TID is an unsigned integer. + + @param bytes: The byte sequence containing the transaction ID + @type bytes: list + + @return: The decoded transaction ID + @rtype: int + """ + tid = bytes[0] << 8 + tid |= bytes[1] + # make unsigned + tid &= 0x7f + return tid diff --git a/src/mms/__init__.py b/src/mms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mms/iterator.py b/src/mms/iterator.py new file mode 100644 index 0000000..012e19d --- /dev/null +++ b/src/mms/iterator.py @@ -0,0 +1,64 @@ +# +# This library is free software, distributed under the terms of +# the GNU Lesser General Public License Version 2. +# See the COPYING.LESSER file included in this archive +# +# The docstrings in this module contain epytext markup; API documentation +# may be created by processing this file with epydoc: http://epydoc.sf.net + +""" +PyMMS library: Iterator with "value preview" capability. + +@version: 0.1 +@author: Francois Aucamp C{} +@license: GNU Lesser General Public License, version 2 +""" + +class PreviewIterator(object): + """ An C{iter} wrapper class providing a "previewable" iterator. + + This "preview" functionality allows the iterator to return successive + values from its C{iterable} object, without actually mvoving forward + itself. This is very usefuly if the next item(s) in an iterator must + be used for something, after which the iterator should "undo" those + read operations, so that they can be read again by another function. + + From the user point of view, this class supersedes the builtin iter() + function: like iter(), it is called as PreviewIter(iterable). + """ + def __init__(self, *args): + self._it = iter(*args) + self._cachedValues = [] + self._previewPos = 0 + def __iter__(self): + return self + def next(self): + self.resetPreview() + if len(self._cachedValues) > 0: + return self._cachedValues.pop(0) + else: + return self._it.next() + + def preview(self): + """ Return the next item in the C{iteratable} object, but do not modify + the actual iterator (i.e. do not intefere with C{iter.next()}. + + Successive calls to C{preview()} will return successive values from + the C{iterable} object, exactly in the same way C{iter.next()} does. + + However, C{preview()} will always return the next item from + C{iterable} after the item returned by the previous C{preview()} or + C{next()} call, whichever was called the most recently. + To force the "preview() iterator" to synchronize with the "next() + iterator" (without calling C{next()}), use C{resetPreview()}. + """ + if self._previewPos < len(self._cachedValues): + value = self._cachedValues[self._previewPos] + else: + value = self._it.next() + self._cachedValues.append(value) + self._previewPos += 1 + return value + + def resetPreview(self): + self._previewPos = 0 diff --git a/src/mms/message.py b/src/mms/message.py new file mode 100644 index 0000000..471b047 --- /dev/null +++ b/src/mms/message.py @@ -0,0 +1,532 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# This library is free software, distributed under the terms of +# the GNU Lesser General Public License Version 2. +# See the COPYING.LESSER file included in this archive +# +# The docstrings in this module contain epytext markup; API documentation +# may be created by processing this file with epydoc: http://epydoc.sf.net +""" +Library for MMS message creation, original by Francois Aucamp +Modified by Nick Leppänen Larsson for use in Maemo5/Fremantle on the Nokia N900. + +@author: Francois Aucamp +@author: Nick Leppänen Larsson +@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': '', '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': '', '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: 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 = "" + 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{{ : }} + @type ctParameters: dict + """ + self.headers = {} + self._filename = None + # self._data = data + # self._data = self._part + "\0" + self._data = data + if contentType == "application/smil": + #self.headers['Content-Type'] = (contentType, {'Charset': 'utf-8', 'Name': 'smil.smil'}) + self.headers['Content-Type'] = (contentType, {}) + else: + self.headers['Content-Type'] = (contentType, ctParameters) + self.headers['Content-ID'] = (self._part) + + def setText(self, text): + """ Convenience wrapper method for setData() + + This method sets the DataPart object to hold the specified text + string, with MIME content type "text/plain". + + @param text: The text to hold + @type text: str + """ + self.setData(text, 'text/plain', {'Charset': 'utf-8', 'Name': 'text.txt'}) + + def __len__(self): + """ Provides the length of the data encapsulated by this object """ + if self._filename != None: + return int(os.stat(self._filename)[6]) + else: + return len(self.data) + + @property + def data(self): + """ @return: the data of this part + @rtype: str + """ + if self._data != None: + if type(self._data) == array.array: + self._data = self._data.tostring() + return self._data + elif self._filename != None: + f = open(self._filename, 'r') + self._data = f.read() + f.close() + return self._data + else: + return '' diff --git a/src/mms/mms_pdu.py b/src/mms/mms_pdu.py new file mode 100644 index 0000000..5fc1797 --- /dev/null +++ b/src/mms/mms_pdu.py @@ -0,0 +1,1145 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# This library is free software, distributed under the terms of +# the GNU Lesser General Public License Version 2. +# See the COPYING.LESSER file included in this archive +# +# The docstrings in this module contain epytext markup; API documentation +# may be created by processing this file with epydoc: http://epydoc.sf.net + +""" MMS Data Unit structure encoding and decoding classes +Original by Francois Aucamp +Modified by Nick Leppänen Larsson for use in Maemo5/Fremantle on the Nokia N900. + +@author: Francois Aucamp +@author: Nick Leppänen Larsson +@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: + # , + # , + # , + # + 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 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: + # , + # , + # , + # + 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 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: + (, ) + @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: + (, ) + @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 = + No = + + A lot of other yes/no fields use this encoding (read-reply, + report-allowed, etc) + + @raise wsp_pdu.DecodeError: The boolean value could not be parsed. + 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 = + Insert-address-token = + + @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 = '' + 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 = + Advertisement = + Informational = + Auto = + 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 '' + @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 '' + + @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 = + Show = + + @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 = + Relative-token = + + @raise wsp_pdu.DecodeError: The Expiry-value could not be decoded + + @return: The decoded Expiry-value, either as a date, or as a delta-seconds value + @rtype: str or int + """ + 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 ( + - length of + ) octets the part's headers + Data 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 + + 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: + # , + # , + # , + # . + + # 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: + (, ) + @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 = + Insert-address-token = + + @param fromValue: The "originator" of the MMS message. This may be an + empty string, in which case a token will be encoded + informing the MMSC to insert the address of the + device that sent this message (default). + @type fromValue: str + + @return: The encoded "From" address value, as a sequence of bytes + @rtype: list + """ + encodedFromValue = [] + if len(fromValue) == 0: + valueLength = wsp_pdu.Encoder.encodeValueLength(1) + encodedFromValue.extend(valueLength) + encodedFromValue.append(129) # Insert-address-token + else: + encodedAddress = MMSEncoder.encodeEncodedStringValue(fromValue) + length = len(encodedAddress) + 1 # the "+1" is for the Address-present-token + valueLength = wsp_pdu.Encoder.encodeValueLength(length) + encodedFromValue.extend(valueLength) + encodedFromValue.append(128) # Address-present-token + encodedFromValue.extend(encodedAddress) + return encodedFromValue + + @staticmethod + def encodeEncodedStringValue(stringValue): + """ From [4], section 7.2.9: + C{Encoded-string-value = Text-string | Value-length Char-set Text-string} + The Char-set values are registered by IANA as MIBEnum value. + + @param stringValue: The text string to encode + @type stringValue: str + + @note: This function is currently a simple wrappper to + C{encodeTextString()} + + @return: The encoded string value, as a sequence of bytes + @rtype: list + """ + return wsp_pdu.Encoder.encodeTextString(stringValue) + + @staticmethod + def encodeMessageTypeValue(messageType): + """ Defined in [4], section 7.2.14. + + @note: Unknown message types are discarded; thus they will be encoded + as 0x80 ("m-send-req") by this function + + @param messageType: The MMS message type to encode + @type messageType: str + + @return: The encoded message type, as a sequence of bytes + @rtype: list + """ + messageTypes = {'m-send-req' : 0x80, + 'm-send-conf' : 0x81, + 'm-notification-ind' : 0x81, + 'm-notifyresp-ind' : 0x83, + 'm-retrieve-conf' : 0x84, + 'm-acknowledge-ind' : 0x85, + 'm-delivery-ind' : 0x86} + if messageType in messageTypes: + return [messageTypes[messageType]] + else: + return [0x80] + + @staticmethod + def encodeStatusValue(statusValue): + """ Defined in [4], section 7.2.# + + Used to encode the "Status" MMS header. + + @return: The encoded Status-value, or 0x84 ('Unrecognised') if none + @rtype: str + """ + + statusValues = {'Expired' : 0x80, + 'Retrieved' : 0x81, + 'Rejected' : 0x82, + 'Deferred' : 0x83, + 'Unrecognised' : 0x84} + + if statusValue in statusValues: + return [statusValues[statusValue]] + else: + return [0x84] \ No newline at end of file diff --git a/src/mms/wsp_pdu.py b/src/mms/wsp_pdu.py new file mode 100644 index 0000000..6ae098a --- /dev/null +++ b/src/mms/wsp_pdu.py @@ -0,0 +1,1966 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# This library is free software, distributed under the terms of +# the GNU Lesser General Public License Version 2. +# See the COPYING file included in this archive +# +# The docstrings in this module contain epytext markup; API documentation +# may be created by processing this file with epydoc: http://epydoc.sf.net +""" +Original by Francois Aucamp +Modified by Nick Leppänen Larsson for use in Maemo5/Fremantle on the Nokia N900. + +@author: Francois Aucamp C{} +@author: Nick Leppänen Larsson +@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{{assigned number: (name, 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 { : } + # 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{{ : (, )}} + @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 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 = *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 ' + 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 = + + @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 + 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{{: }}. + The final returned tuple is in the format: + (, ) + @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{{: }}. + The final returned tuple is in the format: + (, ) + @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 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: + (, ) + @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: + (, ) + @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: + (, ) + @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 (, ) + @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: "." + @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 = + + @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 = + + @raise DecodeError: The decoding failed. C{byteIter} will not be + modified in this case. + @return: the decoded Pragma-value, in the format: + (, ) + @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 = } + 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 = } + + @todo: Currently, "Wap-value" is decoded as a Text-string in most cases + + @return: The header name, and its value, in the format: + (, ) + @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: + (, ) + @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: + (, ) + @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 = + + @raise EncodeError: is not of type "int" + + @param integer: The integer value to encode + @type integer: int + + @return: The encoded Long-integer, as a sequence of byte values + @rtype: list + """ + if type(integer) != int: + raise EncodeError, ' 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 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, ' 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: 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 + 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 = + + @raise EmcodeError: The specified cannot be encoded as a + short-length value; it is not in octet range 0-30. + + @return The encoded short-length, as a sequence of bytes + @rtype: list + """ + if length < 0 or length > 30: + raise EncodeError, 'Cannot encode short-length; length should be in 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 = + + @note: This implementation does not currently support encoding of + "Accept-parameters". + + @param acceptValue: The Accept-value to encode (media/content type) + @type acceptValue: str + + @raise EncodeError: The encoding failed. + + @return The encoded Accept-value, as a sequence of bytes + @rtype: list + """ + encodedAcceptValue = [] + # Try to use Constrained-media encoding + try: + encodedAcceptValue = Encoder.encodeConstrainedMedia(acceptValue) + except EncodeError: + # ...now try Accept-general-form + try: + encodedMediaRange = Encoder.encodeMediaType(acceptValue) + except EncodeError, msg: + raise EncodeError, 'Cannot encode Accept-value: %s' % msg + valueLength = Encoder.encodeValueLength(len(encodedMediaRange)) + encodedAcceptValue = valueLength + encodedAcceptValue.extend(encodedMediaRange) + return encodedAcceptValue diff --git a/src/wappushhandler.py b/src/wappushhandler.py new file mode 100644 index 0000000..2af6985 --- /dev/null +++ b/src/wappushhandler.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" Class for handling wap push messages and creating MMS messages + +@author: Nick Leppänen Larsson +@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 -- 1.7.9.5