commit 9fa2df5563f5a30e2f5b3fa6754842ce34e7bcad Author: dax Date: Sun Nov 27 12:03:00 2005 +0100 first import first import between version 0.1.3 and 0.2 darcs-hash:20051127110300-684f5-0ed50cd0e86df9195cec2c1df070fdf24a6faeb5.gz diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..5b6e7c6 --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 Library 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 Library General +Public License instead of this License. diff --git a/README b/README new file mode 100644 index 0000000..db8cc36 --- /dev/null +++ b/README @@ -0,0 +1,25 @@ +Installation : + +- Get pyxmpp from http://pyxmpp.jabberstudio.org/ (work with snapshot pyxmpp-0.5.s20050506) + +- edit jmc.xml to match jabber server configuration : + - for jabberd2, port must match router.xml port + - for ejabberd, add new listening port : + {5347, ejabberd_service, [{access, all}, + {host, "jmc.localhost", + [{password, "secret"}]}]} + +- then run python jmc.py -c jmc.xml (-D to get debug) + +Usage : + +- Now you can register new mail server connection with your favorite +jabber client. +- all connection are then listed under your service browser and you +can edit mail server parameters by registering on listed connection. + +Feedback : + +Send me feedback and comments to dax at happycoders dot org + + diff --git a/TODO b/TODO new file mode 100644 index 0000000..61b012c --- /dev/null +++ b/TODO @@ -0,0 +1,44 @@ +* Make real documentation + +* i18n support in the same way as ejabberd and + PyMSNt, PyICQ-t and PyAIM-t: using xml:lang, but have an option in + the config to set the default language to show, when the client do + not support xml:lang.i18n support (use xml:lang + default) + +* Database backend (default SQLLite) + +* Checkbox and radio button options in the forms to set when and how + you like to get the messages (in offline, online, free for chat, + away,... presence). + So: +Online: +Away: +Free for Chat: +Extended Away: +Offline: + +* Dropdown menu in the forms with configurable time-intervals. E.g., + users can choose betwoon 1 minute, 2 minutes, 5 minutes, 15 + minutes, 30 minutes, 60 minutes, 1 day. The admin can configure in + the configuration file of jmc which options are available. + +* Support for Ad Hoc Commands (see PyMSNt cvs, avatar + branch). Interesting for statistics. + +* Support for attachements with size limit and file format limit + (e.g. only png, jpeg,... but no exe, bat,...). + +* Support for epoll, kpoll and kqueu (see PyMSNt cvs, avatar branch + code). + +* make JMC run on windows + + diff --git a/coverage.py b/coverage.py new file mode 100755 index 0000000..abd95da --- /dev/null +++ b/coverage.py @@ -0,0 +1,596 @@ +#!/usr/bin/python +# +# Perforce Defect Tracking Integration Project +# +# +# COVERAGE.PY -- COVERAGE TESTING +# +# Gareth Rees, Ravenbrook Limited, 2001-12-04 +# +# +# 1. INTRODUCTION +# +# This module provides coverage testing for Python code. +# +# The intended readership is all Python developers. +# +# This document is not confidential. +# +# See [GDR 2001-12-04a] for the command-line interface, programmatic +# interface and limitations. See [GDR 2001-12-04b] for requirements and +# design. + +"""Usage: + +coverage.py -x MODULE.py [ARG1 ARG2 ...] + Execute module, passing the given command-line arguments, collecting + coverage data. + +coverage.py -e + Erase collected coverage data. + +coverage.py -r [-m] FILE1 FILE2 ... + Report on the statement coverage for the given files. With the -m + option, show line numbers of the statements that weren't executed. + +coverage.py -a [-d dir] FILE1 FILE2 ... + Make annotated copies of the given files, marking statements that + are executed with > and statements that are missed with !. With + the -d option, make the copies in that directory. Without the -d + option, make each copy in the same directory as the original. + +Coverage data is saved in the file .coverage by default. Set the +COVERAGE_FILE environment variable to save it somewhere else.""" + +import os +import re +import string +import sys +import types + + +# 2. IMPLEMENTATION +# +# This uses the "singleton" pattern. +# +# The word "morf" means a module object (from which the source file can +# be deduced by suitable manipulation of the __file__ attribute) or a +# filename. +# +# When we generate a coverage report we have to canonicalize every +# filename in the coverage dictionary just in case it refers to the +# module we are reporting on. It seems a shame to throw away this +# information so the data in the coverage dictionary is transferred to +# the 'cexecuted' dictionary under the canonical filenames. +# +# The coverage dictionary is called "c" and the trace function "t". The +# reason for these short names is that Python looks up variables by name +# at runtime and so execution time depends on the length of variables! +# In the bottleneck of this application it's appropriate to abbreviate +# names to increase speed. + +# A dictionary with an entry for (Python source file name, line number +# in that file) if that line has been executed. +c = {} + +# t(f, x, y). This method is passed to sys.settrace as a trace +# function. See [van Rossum 2001-07-20b, 9.2] for an explanation of +# sys.settrace and the arguments and return value of the trace function. +# See [van Rossum 2001-07-20a, 3.2] for a description of frame and code +# objects. + +def t(f, x, y): + c[(f.f_code.co_filename, f.f_lineno)] = 1 + return t + +the_coverage = None + +class coverage: + error = "coverage error" + + # Name of the cache file (unless environment variable is set). + cache_default = ".coverage" + + # Environment variable naming the cache file. + cache_env = "COVERAGE_FILE" + + # A map from canonical Python source file name to a dictionary in + # which there's an entry for each line number that has been + # executed. + cexecuted = {} + + # Cache of results of calling the analysis() method, so that you can + # specify both -r and -a without doing double work. + analysis_cache = {} + + # Cache of results of calling the canonical_filename() method, to + # avoid duplicating work. + canonical_filename_cache = {} + + def __init__(self): + global the_coverage + if the_coverage: + raise self.error, "Only one coverage object allowed." + self.cache = os.environ.get(self.cache_env, self.cache_default) + self.restore() + self.analysis_cache = {} + + def help(self, error=None): + if error: + print error + print + print __doc__ + sys.exit(1) + + def command_line(self): + import getopt + settings = {} + optmap = { + '-a': 'annotate', + '-d:': 'directory=', + '-e': 'erase', + '-h': 'help', + '-i': 'ignore-errors', + '-m': 'show-missing', + '-r': 'report', + '-x': 'execute', + } + short_opts = string.join(map(lambda o: o[1:], optmap.keys()), '') + long_opts = optmap.values() + options, args = getopt.getopt(sys.argv[1:], short_opts, + long_opts) + for o, a in options: + if optmap.has_key(o): + settings[optmap[o]] = 1 + elif optmap.has_key(o + ':'): + settings[optmap[o + ':']] = a + elif o[2:] in long_opts: + settings[o[2:]] = 1 + elif o[2:] + '=' in long_opts: + settings[o[2:]] = a + else: + self.help("Unknown option: '%s'." % o) + if settings.get('help'): + self.help() + for i in ['erase', 'execute']: + for j in ['annotate', 'report']: + if settings.get(i) and settings.get(j): + self.help("You can't specify the '%s' and '%s' " + "options at the same time." % (i, j)) + args_needed = (settings.get('execute') + or settings.get('annotate') + or settings.get('report')) + action = settings.get('erase') or args_needed + if not action: + self.help("You must specify at least one of -e, -x, -r, " + "or -a.") + if not args_needed and args: + self.help("Unexpected arguments %s." % args) + if settings.get('erase'): + self.erase() + if settings.get('execute'): + if not args: + self.help("Nothing to do.") + sys.argv = args + self.start() + import __main__ + sys.path[0] = os.path.dirname(sys.argv[0]) + execfile(sys.argv[0], __main__.__dict__) + if not args: + args = self.cexecuted.keys() + ignore_errors = settings.get('ignore-errors') + show_missing = settings.get('show-missing') + directory = settings.get('directory=') + if settings.get('report'): + self.report(args, show_missing, ignore_errors) + if settings.get('annotate'): + self.annotate(args, directory, ignore_errors) + + def start(self): + sys.settrace(t) + + def stop(self): + sys.settrace(None) + + def erase(self): + global c + c = {} + self.analysis_cache = {} + self.cexecuted = {} + if os.path.exists(self.cache): + os.remove(self.cache) + + # save(). Save coverage data to the coverage cache. + + def save(self): + self.canonicalize_filenames() + cache = open(self.cache, 'wb') + import marshal + marshal.dump(self.cexecuted, cache) + cache.close() + + # restore(). Restore coverage data from the coverage cache (if it + # exists). + + def restore(self): + global c + c = {} + self.cexecuted = {} + if not os.path.exists(self.cache): + return + try: + cache = open(self.cache, 'rb') + import marshal + cexecuted = marshal.load(cache) + cache.close() + if isinstance(cexecuted, types.DictType): + self.cexecuted = cexecuted + except: + pass + + # canonical_filename(filename). Return a canonical filename for the + # file (that is, an absolute path with no redundant components and + # normalized case). See [GDR 2001-12-04b, 3.3]. + + def canonical_filename(self, filename): + if not self.canonical_filename_cache.has_key(filename): + f = filename + if os.path.isabs(f) and not os.path.exists(f): + f = os.path.basename(f) + if not os.path.isabs(f): + for path in [os.curdir] + sys.path: + g = os.path.join(path, f) + if os.path.exists(g): + f = g + break + cf = os.path.normcase(os.path.abspath(f)) + self.canonical_filename_cache[filename] = cf + return self.canonical_filename_cache[filename] + + # canonicalize_filenames(). Copy results from "executed" to + # "cexecuted", canonicalizing filenames on the way. Clear the + # "executed" map. + + def canonicalize_filenames(self): + global c + for filename, lineno in c.keys(): + f = self.canonical_filename(filename) + if not self.cexecuted.has_key(f): + self.cexecuted[f] = {} + self.cexecuted[f][lineno] = 1 + c = {} + + # morf_filename(morf). Return the filename for a module or file. + + def morf_filename(self, morf): + if isinstance(morf, types.ModuleType): + if not hasattr(morf, '__file__'): + raise self.error, "Module has no __file__ attribute." + file = morf.__file__ + else: + file = morf + return self.canonical_filename(file) + + # analyze_morf(morf). Analyze the module or filename passed as + # the argument. If the source code can't be found, raise an error. + # Otherwise, return a pair of (1) the canonical filename of the + # source code for the module, and (2) a list of lines of statements + # in the source code. + + def analyze_morf(self, morf): + if self.analysis_cache.has_key(morf): + return self.analysis_cache[morf] + filename = self.morf_filename(morf) + ext = os.path.splitext(filename)[1] + if ext == '.pyc': + if not os.path.exists(filename[0:-1]): + raise self.error, ("No source for compiled code '%s'." + % filename) + filename = filename[0:-1] + elif ext != '.py': + raise self.error, "File '%s' not Python source." % filename + source = open(filename, 'r') + import parser + tree = parser.suite(source.read()).totuple(1) + source.close() + statements = {} + self.find_statements(tree, statements) + lines = statements.keys() + lines.sort() + result = filename, lines + self.analysis_cache[morf] = result + return result + + # find_statements(tree, dict). Find each statement in the parse + # tree and record the line on which the statement starts in the + # dictionary (by assigning it to 1). + # + # It works by walking the whole tree depth-first. Every time it + # comes across a statement (symbol.stmt -- this includes compound + # statements like 'if' and 'while') it calls find_statement, which + # descends the tree below the statement to find the first terminal + # token in that statement and record the lines on which that token + # was found. + # + # This algorithm may find some lines several times (because of the + # grammar production statement -> compound statement -> statement), + # but that doesn't matter because we record lines as the keys of the + # dictionary. + # + # See also [GDR 2001-12-04b, 3.2]. + + def find_statements(self, tree, dict): + import symbol, token + if token.ISNONTERMINAL(tree[0]): + for t in tree[1:]: + self.find_statements(t, dict) + if tree[0] == symbol.stmt: + self.find_statement(tree[1], dict) + elif (tree[0] == token.NAME + and tree[1] in ['elif', 'except', 'finally']): + dict[tree[2]] = 1 + + def find_statement(self, tree, dict): + import token + while token.ISNONTERMINAL(tree[0]): + tree = tree[1] + dict[tree[2]] = 1 + + # format_lines(statements, lines). Format a list of line numbers + # for printing by coalescing groups of lines as long as the lines + # represent consecutive statements. This will coalesce even if + # there are gaps between statements, so if statements = + # [1,2,3,4,5,10,11,12,13,14] and lines = [1,2,5,10,11,13,14] then + # format_lines will return "1-2, 5-11, 13-14". + + def format_lines(self, statements, lines): + pairs = [] + i = 0 + j = 0 + start = None + pairs = [] + while i < len(statements) and j < len(lines): + if statements[i] == lines[j]: + if start == None: + start = lines[j] + end = lines[j] + j = j + 1 + elif start: + pairs.append((start, end)) + start = None + i = i + 1 + if start: + pairs.append((start, end)) + def stringify(pair): + start, end = pair + if start == end: + return "%d" % start + else: + return "%d-%d" % (start, end) + import string + return string.join(map(stringify, pairs), ", ") + + def analysis(self, morf): + filename, statements = self.analyze_morf(morf) + self.canonicalize_filenames() + if not self.cexecuted.has_key(filename): + self.cexecuted[filename] = {} + missing = [] + for line in statements: + if not self.cexecuted[filename].has_key(line): + missing.append(line) + return (filename, statements, missing, + self.format_lines(statements, missing)) + + def morf_name(self, morf): + if isinstance(morf, types.ModuleType): + return morf.__name__ + else: + return os.path.splitext(os.path.basename(morf))[0] + + def report(self, morfs, show_missing=1, ignore_errors=0): + if not isinstance(morfs, types.ListType): + morfs = [morfs] + max_name = max([5,] + map(len, map(self.morf_name, morfs))) + fmt_name = "%%- %ds " % max_name + fmt_err = fmt_name + "%s: %s" + header = fmt_name % "Name" + " Stmts Exec Cover" + fmt_coverage = fmt_name + "% 6d % 6d % 5d%%" + if show_missing: + header = header + " Missing" + fmt_coverage = fmt_coverage + " %s" + print header + print "-" * len(header) + total_statements = 0 + total_executed = 0 + for morf in morfs: + name = self.morf_name(morf) + try: + _, statements, missing, readable = self.analysis(morf) + n = len(statements) + m = n - len(missing) + if n > 0: + pc = 100.0 * m / n + else: + pc = 100.0 + args = (name, n, m, pc) + if show_missing: + args = args + (readable,) + print fmt_coverage % args + total_statements = total_statements + n + total_executed = total_executed + m + except KeyboardInterrupt: + raise + except: + if not ignore_errors: + type, msg = sys.exc_info()[0:2] + print fmt_err % (name, type, msg) + if len(morfs) > 1: + print "-" * len(header) + if total_statements > 0: + pc = 100.0 * total_executed / total_statements + else: + pc = 100.0 + args = ("TOTAL", total_statements, total_executed, pc) + if show_missing: + args = args + ("",) + print fmt_coverage % args + + # annotate(morfs, ignore_errors). + + blank_re = re.compile("\\s*(#|$)") + else_re = re.compile("\\s*else\\s*:\\s*(#|$)") + + def annotate(self, morfs, directory=None, ignore_errors=0): + for morf in morfs: + try: + filename, statements, missing, _ = self.analysis(morf) + source = open(filename, 'r') + if directory: + dest_file = os.path.join(directory, + os.path.basename(filename) + + ',cover') + else: + dest_file = filename + ',cover' + dest = open(dest_file, 'w') + lineno = 0 + i = 0 + j = 0 + covered = 1 + while 1: + line = source.readline() + if line == '': + break + lineno = lineno + 1 + while i < len(statements) and statements[i] < lineno: + i = i + 1 + while j < len(missing) and missing[j] < lineno: + j = j + 1 + if i < len(statements) and statements[i] == lineno: + covered = j >= len(missing) or missing[j] > lineno + if self.blank_re.match(line): + dest.write(' ') + elif self.else_re.match(line): + # Special logic for lines containing only + # 'else:'. See [GDR 2001-12-04b, 3.2]. + if i >= len(statements) and j >= len(missing): + dest.write('! ') + elif i >= len(statements) or j >= len(missing): + dest.write('> ') + elif statements[i] == missing[j]: + dest.write('! ') + else: + dest.write('> ') + elif covered: + dest.write('> ') + else: + dest.write('! ') + dest.write(line) + source.close() + dest.close() + except KeyboardInterrupt: + raise + except: + if not ignore_errors: + raise + + +# Singleton object. +the_coverage = coverage() + +# Module functions call methods in the singleton object. +def start(*args): return apply(the_coverage.start, args) +def stop(*args): return apply(the_coverage.stop, args) +def erase(*args): return apply(the_coverage.erase, args) +def analysis(*args): return apply(the_coverage.analysis, args) +def report(*args): return apply(the_coverage.report, args) + +# Save coverage data when Python exits. (The atexit module wasn't +# introduced until Python 2.0, so use sys.exitfunc when it's not +# available.) +try: + import atexit + atexit.register(the_coverage.save) +except ImportError: + sys.exitfunc = the_coverage.save + +# Command-line interface. +if __name__ == '__main__': + the_coverage.command_line() + + +# A. REFERENCES +# +# [GDR 2001-12-04a] "Statement coverage for Python"; Gareth Rees; +# Ravenbrook Limited; 2001-12-04; +# . +# +# [GDR 2001-12-04b] "Statement coverage for Python: design and +# analysis"; Gareth Rees; Ravenbrook Limited; 2001-12-04; +# . +# +# [van Rossum 2001-07-20a] "Python Reference Manual (releae 2.1.1)"; +# Guide van Rossum; 2001-07-20; +# . +# +# [van Rossum 2001-07-20b] "Python Library Reference"; Guido van Rossum; +# 2001-07-20; . +# +# +# B. DOCUMENT HISTORY +# +# 2001-12-04 GDR Created. +# +# 2001-12-06 GDR Added command-line interface and source code +# annotation. +# +# 2001-12-09 GDR Moved design and interface to separate documents. +# +# 2001-12-10 GDR Open cache file as binary on Windows. Allow +# simultaneous -e and -x, or -a and -r. +# +# 2001-12-12 GDR Added command-line help. Cache analysis so that it +# only needs to be done once when you specify -a and -r. +# +# 2001-12-13 GDR Improved speed while recording. Portable between +# Python 1.5.2 and 2.1.1. +# +# 2002-01-03 GDR Module-level functions work correctly. +# +# 2002-01-07 GDR Update sys.path when running a file with the -x option, +# so that it matches the value the program would get if it were run on +# its own. +# +# +# C. COPYRIGHT AND LICENCE +# +# Copyright 2001 Gareth Rees. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. +# +# +# +# $Id: coverage.py,v 1.1 2005/07/11 20:39:31 dax Exp $ diff --git a/jabber/__init__.py b/jabber/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jabber/component.py b/jabber/component.py new file mode 100644 index 0000000..c402a9a --- /dev/null +++ b/jabber/component.py @@ -0,0 +1,868 @@ +## +## component.py +## Login : David Rousselie +## Started on Fri Jan 7 11:06:42 2005 +## $Id: component.py,v 1.12 2005/09/18 20:24:07 dax Exp $ +## +## Copyright (C) 2005 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import re +import signal +import threading +import logging +import sys +import anydbm +import os + +import mailconnection +from mailconnection import * +from x import * +from storage import * +import mailconnection_factory +import pyxmpp.jabberd +from pyxmpp.presence import Presence +from pyxmpp.message import Message +from pyxmpp.streambase import StreamError, FatalStreamError +from pyxmpp.jid import JID +from pyxmpp.jabber.disco import DiscoItems, DiscoItem, DiscoInfo, DiscoIdentity +from pyxmpp.jabberd.component import Component + +class ComponentFatalError(RuntimeError): + pass + +class MailComponent(Component): + def __init__(self, config): + Component.__init__(self, \ + JID(config.get_content("config/jabber/service")), \ + config.get_content("config/jabber/secret"), \ + config.get_content("config/jabber/server"), \ + int(config.get_content("config/jabber/port")), \ + disco_category = "gateway", \ + disco_type = "headline") + self.__logger = logging.getLogger("jabber.Component") + self.__shutdown = 0 + + # TODO : delete signals not known by Windows + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGPIPE, self.signal_handler) + signal.signal(signal.SIGHUP, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + signal.signal(signal.SIGALRM, self.time_handler) + + self.__interval = int(config.get_content("config/check_interval")) + self.__config = config +# self.__registered = {} + try: + self.__storage = globals()[config.get_content("config/storage") + "Storage"]() + except: + print >>sys.stderr, "Cannot find " \ + + config.get_content("config/storage") + "Storage class" + exit(1) + self.__spool_dir = config.get_content("config/spooldir") + "/" + \ + config.get_content("config/jabber/service") + self.__storage.spool_dir = self.__spool_dir + self.__storage.nb_pk_fields = 2 + self.__reg_form = None + self.__reg_form_init = None + # dump registered accounts (save) at least every hour + self.__count = 60 / self.__interval + + def __del__(self): + logging.shutdown() + + """ Register Form creator """ + def get_reg_form(self): + if self.__reg_form == None: + self.__reg_form = X() + self.__reg_form.xmlns = "jabber:x:data" + self.__reg_form.title = "Jabber Mail connection registration" + self.__reg_form.instructions = "Enter anything below" + self.__reg_form.type = "form" + + self.__reg_form.add_field(type = "text-single", \ + label = "Connection name", \ + var = "name") + + self.__reg_form.add_field(type = "text-single", \ + label = "Login", \ + var = "login") + + self.__reg_form.add_field(type = "text-private", \ + label = "Password", \ + var = "password") + + self.__reg_form.add_field(type = "text-single", \ + label = "Host", \ + var = "host") + + self.__reg_form.add_field(type = "text-single", \ + label = "Port", \ + var = "port") + + field = self.__reg_form.add_field(type = "list-single", \ + label = "Mailbox type", \ + var = "type") + field.add_option(label = "POP3", \ + value = "pop3") + field.add_option(label = "POP3S", \ + value = "pop3s") + field.add_option(label = "IMAP", \ + value = "imap") + field.add_option(label = "IMAPS", \ + value = "imaps") + + self.__reg_form.add_field(type = "text-single", \ + label = "Mailbox (IMAP)", \ + var = "mailbox", \ + value = "INBOX") + + field = self.__reg_form.add_field(type = "list-single", \ + label = "Action when state is 'Free For Chat'", \ + var = "ffc_action", \ + value = str(mailconnection.RETRIEVE)) + field.add_option(label = "Do nothing", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Send mail digest", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Retrieve mail", \ + value = str(mailconnection.RETRIEVE)) + + field = self.__reg_form.add_field(type = "list-single", \ + label = "Action when state is 'Online'", \ + var = "online_action", \ + value = str(mailconnection.RETRIEVE)) + field.add_option(label = "Do nothing", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Send mail digest", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Retrieve mail", \ + value = str(mailconnection.RETRIEVE)) + + field = self.__reg_form.add_field(type = "list-single", \ + label = "Action when state is 'Away'", \ + var = "away_action", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Do nothing", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Send mail digest", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Retrieve mail", \ + value = str(mailconnection.RETRIEVE)) + + field = self.__reg_form.add_field(type = "list-single", \ + label = "Action when state is 'Extended Away'", \ + var = "ea_action", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Do nothing", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Send mail digest", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Retrieve mail", \ + value = str(mailconnection.RETRIEVE)) + + field = self.__reg_form.add_field(type = "list-single", \ + label = "Action when state is 'Offline'", \ + var = "offline_action", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Do nothing", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Send mail digest", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Retrieve mail", \ + value = str(mailconnection.RETRIEVE)) + + # default interval in config file + self.__reg_form.add_field(type = "text-single", \ + label = "Mail check interval (in minutes)", \ + var = "interval", \ + value = "5") + + return self.__reg_form + + """ Register Form modifier for existing accounts """ + def get_reg_form_init(self, jid, name): + if not self.__storage.has_key((jid, name)): + return None + account = self.__storage[(jid, name)] + if self.__reg_form_init == None: + self.__reg_form_init = X() + self.__reg_form_init.xmlns = "jabber:x:data" + self.__reg_form_init.title = "Jabber mail connection Modifier" + self.__reg_form_init.instructions = "Modifier for connection " + \ + name + self.__reg_form_init.type = "form" + + self.__reg_form_init.add_field(type = "fixed", \ + label = "Connection name", \ + var = "name", \ + value = name) + + self.__reg_form_init.add_field(type = "text-single", \ + label = "Login", \ + var = "login", \ + value = account.login) + + self.__reg_form_init.add_field(type = "text-private", \ + label = "Password", \ + var = "password", \ + value = account.password) + + self.__reg_form_init.add_field(type = "text-single", \ + label = "Host", \ + var = "host", \ + value = account.host) + + self.__reg_form_init.add_field(type = "text-single", \ + label = "Port", \ + var = "port", \ + value = str(account.port)) + + field = self.__reg_form_init.add_field(type = "list-single", \ + label = "Mailbox type", \ + var = "type", \ + value = account.get_type()) + field.add_option(label = "POP3", \ + value = "pop3") + field.add_option(label = "POP3S", \ + value = "pop3s") + field.add_option(label = "IMAP", \ + value = "imap") + field.add_option(label = "IMAPS", \ + value = "imaps") + + self.__reg_form_init.add_field(type = "text-single", \ + label = "Mailbox (IMAP)", \ + var = "mailbox") + + field = self.__reg_form_init.add_field(type = "list-single", \ + label = "Action when state is 'Free For Chat'", \ + var = "ffc_action", \ + value = str(account.ffc_action)) + field.add_option(label = "Do nothing", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Send mail digest", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Retrieve mail", \ + value = str(mailconnection.RETRIEVE)) + + field = self.__reg_form_init.add_field(type = "list-single", \ + label = "Action when state is 'Online'", \ + var = "online_action", \ + value = str(account.online_action)) + field.add_option(label = "Do nothing", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Send mail digest", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Retrieve mail", \ + value = str(mailconnection.RETRIEVE)) + + field = self.__reg_form_init.add_field(type = "list-single", \ + label = "Action when state is 'Away'", \ + var = "away_action", \ + value = str(account.away_action)) + field.add_option(label = "Do nothing", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Send mail digest", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Retrieve mail", \ + value = str(mailconnection.RETRIEVE)) + + field = self.__reg_form_init.add_field(type = "list-single", \ + label = "Action when state is 'Extended Away'", \ + var = "ea_action", \ + value = str(account.ea_action)) + field.add_option(label = "Do nothing", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Send mail digest", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Retrieve mail", \ + value = str(mailconnection.RETRIEVE)) + + field = self.__reg_form_init.add_field(type = "list-single", \ + label = "Action when state is 'Offline'", \ + var = "offline_action", \ + value = str(account.offline_action)) + field.add_option(label = "Do nothing", \ + value = str(mailconnection.DO_NOTHING)) + field.add_option(label = "Send mail digest", \ + value = str(mailconnection.DIGEST)) + field.add_option(label = "Retrieve mail", \ + value = str(mailconnection.RETRIEVE)) + + self.__reg_form_init.add_field(type = "text-single", \ + label = "Mail check interval (in minutes)", \ + var = "interval", \ + value = str(account.interval)) + else: + self.__reg_form_init.fields["login"].value = account.login + self.__reg_form_init.fields["password"].value = account.password + self.__reg_form_init.fields["host"].value = account.host + self.__reg_form_init.fields["port"].value = str(account.port) + self.__reg_form_init.fields["type"].value = account.get_type() + self.__reg_form_init.fields["ffc_action"].value = str(account.ffc_action) + self.__reg_form_init.fields["online_action"].value = str(account.online_action) + self.__reg_form_init.fields["away_action"].value = str(account.away_action) + self.__reg_form_init.fields["ea_action"].value = str(account.ea_action) + self.__reg_form_init.fields["offline_action"].value = str(account.offline_action) + self.__reg_form_init.fields["interval"].value = str(account.interval) + + if account.get_type()[0:4] == "imap": + self.__reg_form_init.fields["mailbox"].value = account.mailbox + else: + self.__reg_form_init.fields["mailbox"].value = "INBOX" + + return self.__reg_form_init + + """ Looping method """ + def run(self, timeout): + self.connect() + # Set check mail timer + threading.Timer(self.__interval * 60, self.time_handler) + try: + while (not self.__shutdown and self.stream + and not self.stream.eof and self.stream.socket is not None): + try: + self.stream.loop_iter(timeout) + except (KeyboardInterrupt, SystemExit, FatalStreamError, \ + StreamError): + raise + except: + self.__logger.exception("Exception cought:") + finally: + ## TODO : for jid in self.__storage.keys(()) + ## for name in self.__storage.keys((jid,)) +# for jid in self.__storage.keys(()): +# p = Presence(from_jid = str(self.jid), to_jid = jid, \ +# stanza_type = "unavailable") +# self.stream.send(p) +# for name in self.__registered[jid].keys(): +# if self.__storage[(jid, name)].status != "offline": +# p = Presence(from_jid = name + "@" + str(self.jid), \ +# to_jid = jid, \ +# stanza_type = "unavailable") +# self.stream.send(p) + threads = threading.enumerate() + for th in threads: + try: + th.join(10 * timeout) + except: + pass + for th in threads: + try: + th.join(timeout) + except: + pass + self.disconnect() + del self.__storage + self.__logger.debug("Exitting normally") + + """ Stop method handler """ + def signal_handler(self, signum, frame): + self.__logger.debug("Signal %i received, shutting down..." % (signum,)) + self.__shutdown = 1 + + """ SIGALRM signal handler """ + def time_handler(self, signum, frame): + self.__logger.debug("Signal %i received, checking mail..." % (signum,)) + self.check_all_mail() + self.__logger.debug("Resetting alarm signal") + threading.Timer(self.__interval * 60, self.time_handler) + if self.__count == 0: + self.__logger.debug("Dumping registered accounts Database") + self.__storage.sync() + self.__count = 60 / self.__interval + else: + self.__count -= 1 + + """ Component authentication handler """ + def authenticated(self): + self.__logger.debug("AUTHENTICATED") + Component.authenticated(self) +# for jid in self.__registered.keys(): +# p = Presence(from_jid = str(self.jid), \ +# to_jid = jid, stanza_type = "probe") +# self.stream.send(p) + + self.stream.set_iq_get_handler("query", "jabber:iq:version", \ + self.get_version) + self.stream.set_iq_get_handler("query", "jabber:iq:register", \ + self.get_register) + self.stream.set_iq_set_handler("query", "jabber:iq:register", \ + self.set_register) + + self.stream.set_presence_handler("available", \ + self.presence_available) + + self.stream.set_presence_handler("probe", \ + self.presence_available) + + self.stream.set_presence_handler("unavailable", \ + self.presence_unavailable) + + self.stream.set_presence_handler("unsubscribe", \ + self.presence_unsubscribe) + self.stream.set_presence_handler("unsubscribed", \ + self.presence_unsubscribed) + self.stream.set_presence_handler("subscribe", \ + self.presence_subscribe) + self.stream.set_presence_handler("subscribed", \ + self.presence_subscribed) + + self.stream.set_message_handler("normal", \ + self.message) + + def stream_state_changed(self,state,arg): + print "*** State changed: %s %r ***" % (state,arg) + + """ Discovery get info handler """ + def disco_get_info(self, node, iq): + self.__logger.debug("DISCO_GET_INFO") + di = DiscoInfo() + if node is None: + di.add_feature("jabber:iq:version") + di.add_feature("jabber:iq:register") + DiscoIdentity(di, "Jabber Mail Component", "headline", "mail") + else: + di.add_feature("jabber:iq:register") + return di + + """ Discovery get nested nodes handler """ + def disco_get_items(self, node, iq): + self.__logger.debug("DISCO_GET_ITEMS") + base_from_jid = str(iq.get_from().bare()) + di = DiscoItems() + if not node and self.__registered.has_key(base_from_jid): + for name in self.__registered[base_from_jid].keys(): + account = self.__registered[base_from_jid][name] + str_name = account.get_type() + " connection " + name + if account.get_type()[0:4] == "imap": + str_name += " (" + account.mailbox + ")" + DiscoItem(di, JID(name + "@" + str(self.jid)), \ + name, str_name) + return di + + """ Get Version handler """ + def get_version(self, iq): + self.__logger.debug("GET_VERSION") + iq = iq.make_result_response() + q = iq.new_query("jabber:iq:version") + q.newTextChild(q.ns(), "name", "Jabber Mail Component") + q.newTextChild(q.ns(), "version", "0.1") + self.stream.send(iq) + return 1 + + """ Send back register form to user """ + def get_register(self, iq): + self.__logger.debug("GET_REGISTER") + base_from_jid = str(iq.get_from().bare()) + to = iq.get_to() + iq = iq.make_result_response() + q = iq.new_query("jabber:iq:register") + if to and to != self.jid: + self.get_reg_form_init(base_from_jid, to.node).attach_xml(q) + else: + self.get_reg_form().attach_xml(q) + self.stream.send(iq) + return 1 + + """ Handle user registration response """ + def set_register(self, iq): + self.__logger.debug("SET_REGISTER") + to = iq.get_to() + from_jid = iq.get_from() + base_from_jid = str(from_jid.bare()) + remove = iq.xpath_eval("r:query/r:remove", \ + {"r" : "jabber:iq:register"}) + if remove: + if self.__registered.has_key(base_from_jid): + for name in self.__registered[base_from_jid].keys(): + p = Presence(from_jid = name + "@" + str(self.jid), \ + to_jid = from_jid, \ + stanza_type = "unsubscribe") + self.stream.send(p) + p = Presence(from_jid = name + "@" + str(self.jid), \ + to_jid = from_jid, \ + stanza_type = "unsubscribed") + self.stream.send(p) + del self.__registered[base_from_jid] +# self.__storage. + p = Presence(from_jid = self.jid, to_jid = from_jid, \ + stanza_type = "unsubscribe") + self.stream.send(p) + p = Presence(from_jid = self.jid, to_jid = from_jid, \ + stanza_type = "unsubscribed") + self.stream.send(p) + return 1 + + query = iq.get_query() + x = X() + x.from_xml(query.children) + + if x.fields.has_key("name"): + name = x.fields["name"].value.lower() + else: + name = u"" + + if x.fields.has_key("login"): + login = x.fields["login"].value + else: + login = u"" + + if x.fields.has_key("password"): + password = x.fields["password"].value + else: + password = u"" + + if x.fields.has_key("host"): + host = x.fields["host"].value + else: + host = u"" + + if x.fields.has_key("mailbox"): + mailbox = x.fields["mailbox"].value + else: + mailbox = u"" + + if x.fields.has_key("type"): + type = x.fields["type"].value + else: + type = u"pop3" + + if x.fields.has_key("port") and x.fields["port"].value != "": + port = int(x.fields["port"].value) + else: + port = None + + if x.fields.has_key("ffc_action") and x.fields["ffc_action"].value != "": + ffc_action = int(x.fields["ffc_action"].value) + else: + ffc_action = mailconnection.DO_NOTHING + + if x.fields.has_key("online_action") and x.fields["online_action"].value != "": + online_action = int(x.fields["online_action"].value) + else: + online_action = mailconnection.DO_NOTHING + + if x.fields.has_key("away_action") and x.fields["away_action"].value != "": + away_action = int(x.fields["away_action"].value) + else: + away_action = mailconnection.DO_NOTHING + + if x.fields.has_key("ea_action") and x.fields["ea_action"].value != "": + ea_action = int(x.fields["ea_action"].value) + else: + ea_action = mailconnection.DO_NOTHING + + if x.fields.has_key("offline_action") and x.fields["offline_action"].value != "": + offline_action = int(x.fields["offline_action"].value) + else: + offline_action = mailconnection.DO_NOTHING + + if x.fields.has_key("interval") and x.fields["interval"].value != "": + interval = int(x.fields["interval"].value) + else: + interval = None + + self.__logger.debug(u"New Account: %s, %s, %s, %s, %s, %s, %s %i %i %i %i %i %i" \ + % (name, login, password, host, str(port), \ + mailbox, type, ffc_action, online_action, away_action, \ + ea_action, offline_action, interval)) + + iq = iq.make_result_response() + self.stream.send(iq) + + if not self.__registered.has_key(base_from_jid): + self.__registered[base_from_jid] = {} + p = Presence(from_jid = self.jid, to_jid = from_jid.bare(), \ + stanza_type="subscribe") + self.stream.send(p) + + ## Update account + if port != None: + socket = host + ":" + str(port) + else: + socket = host + if self.__registered[base_from_jid].has_key(name): + m = Message(from_jid = self.jid, to_jid = from_jid, \ + stanza_type = "message", \ + body = u"Updated %s connection '%s': Registered with "\ + "username '%s' and password '%s' on '%s'" \ + % (type, name, login, password, socket)) + self.stream.send(m) + else: + m = Message(from_jid = self.jid, to_jid = from_jid, \ + stanza_type = "message", \ + body = u"New %s connection '%s': Registered with " \ + "username '%s' and password '%s' on '%s'" \ + % (type, name, login, password, socket)) + self.stream.send(m) + p = Presence(from_jid = name + "@" + str(self.jid), \ + to_jid = from_jid.bare(), \ + stanza_type="subscribe") + self.stream.send(p) + + self.__registered[base_from_jid][name] = \ + mailconnection_factory.get_new_mail_connection(type) + self.__registered[base_from_jid][name].login = login + self.__registered[base_from_jid][name].password = password + self.__registered[base_from_jid][name].host = host + self.__registered[base_from_jid][name].ffc_action = ffc_action + self.__registered[base_from_jid][name].online_action = online_action + self.__registered[base_from_jid][name].away_action = away_action + self.__registered[base_from_jid][name].ea_action = ea_action + self.__registered[base_from_jid][name].offline_action = offline_action + + if port: + self.__registered[base_from_jid][name].port = port + + if interval: + self.__registered[base_from_jid][name].interval = interval + + if type[0:4] == "imap": + self.__registered[base_from_jid][name].mailbox = mailbox + + self.__storage.add([base_from_jid, name], self.__registered[base_from_jid][name]) + return 1 + + """ Handle presence availability """ + def presence_available(self, stanza): + self.__logger.debug("PRESENCE_AVAILABLE") + from_jid = stanza.get_from() + base_from_jid = str(from_jid.bare()) + name = stanza.get_to().node + show = stanza.get_show() + self.__logger.debug("SHOW : " + str(show)) + if self.__registered.has_key(base_from_jid): + if not name: + p = Presence(from_jid = self.jid, \ + to_jid = from_jid, \ + status = \ + str(len(self.__registered[base_from_jid])) \ + + " accounts registered.", \ + show = show, \ + stanza_type = "available") + self.stream.send(p) + for name in self.__registered[base_from_jid].keys(): + account = self.__registered[base_from_jid][name] + # Make available to receive mail only when online + account.status = "offline" # TODO get real status available = (not show) + p = Presence(from_jid = name + "@" + \ + str(self.jid), \ + to_jid = from_jid, \ + status = account.get_status(), \ + show = show, \ + stanza_type = "available") + self.stream.send(p) + elif self.__registered[base_from_jid].has_key(name): + account = self.__registered[base_from_jid][name] + # Make available to receive mail only when online + account.status = "offline" # TODO get real status = (not show) + p = Presence(from_jid = name + "@" + \ + str(self.jid), \ + to_jid = from_jid, \ + status = account.get_status(), \ + show = show, \ + stanza_type = "available") + self.stream.send(p) + return 1 + + """ handle presence unavailability """ + def presence_unavailable(self, stanza): + self.__logger.debug("PRESENCE_UNAVAILABLE") + from_jid = stanza.get_from() + base_from_jid = str(from_jid.bare()) + if stanza.get_to() == str(self.jid) \ + and self.__registered.has_key(base_from_jid): + for name in self.__registered[base_from_jid].keys(): + self.__registered[base_from_jid][name].status = "offline" # TODO get real status + p = Presence(from_jid = name + "@" + str(self.jid), \ + to_jid = from_jid, \ + stanza_type = "unavailable") + self.stream.send(p) + + p = Presence(from_jid = stanza.get_to(), to_jid = from_jid, \ + stanza_type = "unavailable") + self.stream.send(p) + return 1 + + """ handle subscribe presence from user """ + def presence_subscribe(self, stanza): + self.__logger.debug("PRESENCE_SUBSCRIBE") + p = stanza.make_accept_response() + self.stream.send(p) + return 1 + + """ handle subscribed presence from user """ + def presence_subscribed(self, stanza): + self.__logger.debug("PRESENCE_SUBSCRIBED") + name = stanza.get_to().node + from_jid = stanza.get_from() + base_from_jid = str(from_jid.bare()) + if self.__registered.has_key(base_from_jid) \ + and self.__registered[base_from_jid].has_key(name): + account = self.__registered[base_from_jid][name] + account.status = "online" # TODO retrieve real status + p = Presence(from_jid = stanza.get_to(), to_jid = from_jid, \ + status = account.get_status(), \ + stanza_type = "available") + self.stream.send(p) + return 1 + + """ handle unsubscribe presence from user """ + def presence_unsubscribe(self, stanza): + self.__logger.debug("PRESENCE_UNSUBSCRIBE") + name = stanza.get_to().node + from_jid = stanza.get_from() + base_from_jid = str(from_jid.bare()) + if self.__registered.has_key(base_from_jid) \ + and self.__registered[base_from_jid].has_key(name): + del self.__registered[base_from_jid][name] + p = Presence(from_jid = stanza.get_to(), to_jid = from_jid, \ + stanza_type = "unsubscribe") + self.stream.send(p) + p = stanza.make_accept_response() + self.stream.send(p) + return 1 + + """ handle unsubscribed presence from user """ + def presence_unsubscribed(self, stanza): + self.__logger.debug("PRESENCE_UNSUBSCRIBED") + p = Presence(from_jid = stanza.get_to(), to_jid = stanza.get_from(), \ + stanza_type = "unavailable") + self.stream.send(p) + self.__storage.sync() + return 1 + + """ Handle new message """ + def message(self, message): + self.__logger.debug("MESSAGE: " + message.get_body()) + name = message.get_to().node + base_from_jid = str(message.get_from().bare()) + if name and self.__registered.has_key(base_from_jid): + body = message.get_body() + cmd = body.split(' ') + if cmd[0] == "check": + self.check_mail(base_from_jid, name) + elif cmd[0] == "dump": + body = "" + for jid in self.__registered.keys(): + for name in self.__registered[jid].keys(): + body += name + " for user " + jid + msg = Message(from_jid = self.jid, to_jid = base_from_jid, \ + stanza_type = "message", \ + body = body) + self.stream.send(msg) + return 1 + +# """ Store registered sessions """ +# def store_registered(self): +# self.__logger.debug("STORE_REGISTERED") +# try: +# if not os.path.isdir(self.__spool_dir): +# os.makedirs(self.__spool_dir) +# str_registered = anydbm.open(self.__spool_dir + "/registered.db", \ +# 'n') +# self.__logger.debug("Saving registered sessions") +# for jid in self.__registered.keys(): +# for name in self.__registered[jid].keys(): +# self.__logger.debug("\t" + jid + ", _" + name + "_") +# str_registered[jid + '#' + name] = \ +# str(self.__storage[(jid, name)]) +# str_registered.close() +# except Exception, e: +# print >>sys.stderr, "Cannot save to registered.db : " +# print >>sys.stderr, e + +# """ Load previously registered sessions """ +# def load_registered(self): +# self.__logger.debug("LOAD_REGISTERED") +# try: +# if not os.path.isdir(self.__spool_dir): +# os.makedirs(self.__spool_dir) +# str_registered = anydbm.open(self.__spool_dir + "/registered.db", \ +# 'c') +# self.__logger.debug("Loading previously registered sessions") +# for key in str_registered.keys(): +# jid, name = key.split('#') +# if not self.__registered.has_key(jid): +# self.__registered[jid] = {} +# self.__storage[(jid, name)] = \ +# mailconnection_factory.str_to_mail_connection(str_registered[key]) +# # self.__storage[(jid, name)].name = name +# self.__logger.debug("Loaded data for %s on %s :" % (jid, name)) +# self.__logger.debug("\t %s" % (self.__storage[(jid, name)])) +# str_registered.close() +# except Exception, e: +# print >>sys.stderr, "Cannot load registered.db : " +# print >>sys.stderr, e + + """ Check mail account """ + def check_mail(self, jid, name): + self.__logger.debug("CHECK_MAIL") + account = self.__storage[(jid, name)] + action = account.action + if action != "nothing": + try: + self.__logger.debug("Checking " \ + + name) + self.__logger.debug("\t" + account.login \ + + "@" + account.host) + account.connect() + mail_list = account.get_mail_list() + if not mail_list or mail_list[0] == '': + num = 0 + else: + num = len(mail_list) + # unseen mails checked by external client + if num < account.lastcheck: + account.lastcheck = 0 + if action == "retrieve": + while account.lastcheck < num: + body = account.get_mail(int(mail_list[account.lastcheck])) + mesg = Message(from_jid = name + "@" + \ + str(self.jid), \ + to_jid = jid, \ + stanza_type = "message", \ + body = body) + self.stream.send(mesg) + account.lastcheck += 1 + else: + body = "" + while account.lastcheck < num: + body += \ + account.get_mail_summary(int(mail_list[account.lastcheck])) \ + + "\n----------------------------------\n" + account.lastcheck += 1 + if body != "": + mesg = Message(from_jid = name + "@" + \ + str(self.jid), \ + to_jid = jid, \ + stanza_type = "headline", \ + body = body) + self.stream.send(mesg) + + account.disconnect() + except Exception,e: + self.__logger.debug("Error while checking mail : %s" \ + % (e)) + + """ check mail handler """ + def check_all_mail(self): + self.__logger.debug("CHECK_ALL_MAIL") + for jid in self.__registered.keys(): + for name in self.__registered[jid].keys(): + self.check_mail(jid, name) diff --git a/jabber/config.py b/jabber/config.py new file mode 100644 index 0000000..9a9b5e6 --- /dev/null +++ b/jabber/config.py @@ -0,0 +1,47 @@ +## +## config.py +## Login : David Rousselie +## Started on Fri Jan 7 11:06:42 2005 +## $Id: config.py,v 1.2 2005/03/13 11:39:36 dax Exp $ +## +## Copyright (C) 2005 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import libxml2 +import os + +from pyxmpp.jid import JID +from component import ComponentFatalError + +class Config: + def __init__(self, config_file): + self.doc = None + self.config_file = config_file +# libxml2.initializeCatalog() +# libxml2.loadCatalog(os.path.join(data_dir, "catalog.xml")) + parser = libxml2.createFileParserCtxt(config_file) +# parser.validate(1) + parser.parseDocument() + if not parser.isValid(): + raise ComponentFatalError, "Invalid configuration" + self.doc = parser.doc() + + def get_content(self, xpath): + return self.doc.xpathEval(xpath)[0].getContent() + + def __del__(self): + if self.doc: + self.doc.freeDoc() diff --git a/jabber/mailconnection.py b/jabber/mailconnection.py new file mode 100644 index 0000000..7f82925 --- /dev/null +++ b/jabber/mailconnection.py @@ -0,0 +1,373 @@ +## +## mailconnection.py +## Login : David Rousselie +## Started on Fri Jan 7 11:06:42 2005 +## $Id: mailconnection.py,v 1.11 2005/09/18 20:24:07 dax Exp $ +## +## Copyright (C) 2005 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + + +import sys +import email +import email.Header + +import poplib +import imaplib +import socket + +IMAP4_TIMEOUT = 10 +POP3_TIMEOUT = 10 + +DO_NOTHING = 0 +DIGEST = 1 +RETRIEVE = 2 +## All MY* classes are implemented to add a timeout (settimeout) +## while connecting +class MYIMAP4(imaplib.IMAP4): + def open(self, host = '', port = imaplib.IMAP4_PORT): + """Setup connection to remote server on "host:port" + (default: localhost:standard IMAP4 port). + This connection will be used by the routines: + read, readline, send, shutdown. + """ + self.host = host + self.port = port + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.settimeout(IMAP4_TIMEOUT) + self.sock.connect((host, port)) + self.sock.settimeout(None) + self.file = self.sock.makefile('rb') + +class MYIMAP4_SSL(imaplib.IMAP4_SSL): + def open(self, host = '', port = imaplib.IMAP4_SSL_PORT): + """Setup connection to remote server on "host:port". + (default: localhost:standard IMAP4 SSL port). + This connection will be used by the routines: + read, readline, send, shutdown. + """ + self.host = host + self.port = port + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.settimeout(IMAP4_TIMEOUT) + self.sock.connect((host, port)) + self.sock.settimeout(None) + self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile) + +class MYPOP3(poplib.POP3): + def __init__(self, host, port = poplib.POP3_PORT): + self.host = host + self.port = port + msg = "getaddrinfo returns an empty list" + self.sock = None + for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + try: + self.sock = socket.socket(af, socktype, proto) + self.sock.settimeout(POP3_TIMEOUT) + self.sock.connect(sa) + self.sock.settimeout(None) + except socket.error, msg: + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + self.file = self.sock.makefile('rb') + self._debugging = 0 + self.welcome = self._getresp() + +class MYPOP3_SSL(poplib.POP3_SSL): + def __init__(self, host, port = poplib.POP3_SSL_PORT, keyfile = None, certfile = None): + self.host = host + self.port = port + self.keyfile = keyfile + self.certfile = certfile + self.buffer = "" + msg = "getaddrinfo returns an empty list" + self.sock = None + for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + try: + self.sock = socket.socket(af, socktype, proto) + self.sock.settimeout(POP3_TIMEOUT) + self.sock.connect(sa) + self.sock.settimeout(None) + except socket.error, msg: + if self.sock: + self.sock.close() + self.sock = None + continue + break + if not self.sock: + raise socket.error, msg + self.file = self.sock.makefile('rb') + self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile) + self._debugging = 0 + self.welcome = self._getresp() + +class MailConnection(object): + """ Wrapper to mail connection and action. + Abstract class, do not represent real mail connection type""" + + def __init__(self, login = "", password = "", host = "", \ + port = 110, ssl = False): + """ Initialize MailConnection object for common parameters of all + connections types + + :Parameters: + - 'login': login used to connect mail server + - 'password': password associated with 'login' to connect mail server + - 'host': mail server hostname + - 'port': mail server port + - 'ssl': activate ssl to connect server + + :Types: + - 'login': string + - 'password': string + - 'host': string + - 'port': int + - 'ssl': boolean""" + self.login = login + self.password = password + self.host = host + self.port = port + self.ssl = ssl + self.lastcheck = 0 + self.status = "offline" + self.connection = None + self.ffc_action = RETRIEVE + self.online_action = RETRIEVE + self.away_action = RETRIEVE + self.ea_action = RETRIEVE + self.offline_action = DO_NOTHING + + def __eq__(self, other): + return self.get_type() == other.get_type() \ + and self.login == other.login \ + and self.password == other.password \ + and self.host == other.host \ + and self.port == other.port \ + and self.ssl == other.ssl \ + and self.ffc_action == other.ffc_action \ + and self.online_action == other.online_action \ + and self.away_action == other.away_action \ + and self.ea_action == other.ea_action \ + and self.offline_action == other.offline_action + + def __str__(self): + return self.get_type() + "#" + self.login + "#" + self.password + "#" \ + + self.host + "#" + str(self.port) + "#" + str(self.ffc_action) + "#" \ + + str(self.online_action) + "#" + str(self.away_action) + "#" + \ + str(self.ea_action) + "#" + str(self.offline_action) + + def get_decoded_part(self, part): + content_charset = part.get_content_charset() + if content_charset: + return part.get_payload(decode=True).decode(content_charset) + else: + return part.get_payload(decode=True) + + def format_message(self, email_msg, include_body = True): + from_decoded = email.Header.decode_header(email_msg["From"]) + result = u"From : " + for i in range(len(from_decoded)): + if from_decoded[i][1]: + result += from_decoded[i][0].decode(from_decoded[i][1]) + else: + result += from_decoded[i][0] + result += "\n" + + subject_decoded = email.Header.decode_header(email_msg["Subject"]) + result += u"Subject : " + for i in range(len(subject_decoded)): + if subject_decoded[i][1]: + result += subject_decoded[i][0].decode(subject_decoded[i][1]) + else: + result += subject_decoded[i][0] + result += "\n\n" + + if include_body: + action = { + "text/plain" : lambda part: self.get_decoded_part(part), + "text/html" : lambda part: "\n<<>>\n" + } + for part in email_msg.walk(): + content_type = part.get_content_type() + if action.has_key(content_type): + result += action[content_type](part) + '\n' + return result + + def format_message_summary(self, email_msg): + return self.format_message(email_msg, False) + + def get_type(self): + return "UNKNOWN" + + def get_status(self): + return self.get_type() + "://" + self.login + "@" + self.host + ":" + \ + str(self.port) + + def connect(self): + pass + + def disconnect(self): + pass + + def get_mail_list(self): + return 0 + + def get_mail(self, index): + return None + + def get_mail_summary(self, index): + return None + + def get_action(self): + mapping = {"online": self.online_action, + "ffc": self.ffc_action, + "away": self.away_action, + "ea": self.ea_action, + "offline": self.offline_action} + if mapping.has_key(self.status): + return mapping[self.status] + return "nothing" + + action = property(get_action) + +class IMAPConnection(MailConnection): + def __init__(self, login = "", password = "", host = "", \ + port = None, ssl = False, mailbox = "INBOX"): + if not port: + if ssl: + port = 993 + else: + port = 143 + MailConnection.__init__(self, login, password, host, port, ssl) + self.mailbox = mailbox + + def __eq__(self, other): + return MailConnection.__eq__(self, other) \ + and self.mailbox == other.mailbox + + def __str__(self): + return MailConnection.__str__(self) + "#" + self.mailbox + + def get_type(self): + if self.ssl: + return "imaps" + return "imap" + + def get_status(self): + return MailConnection.get_status(self) + "/" + self.mailbox + + def connect(self): + print >>sys.stderr, "Connecting to IMAP server " \ + + self.login + "@" + self.host + ":" + str(self.port) \ + + " (" + self.mailbox + "). SSL=" \ + + str(self.ssl) + if self.ssl: + self.connection = MYIMAP4_SSL(self.host, self.port) + else: + self.connection = MYIMAP4(self.host, self.port) + self.connection.login(self.login, self.password) + + def disconnect(self): + print >>sys.stderr, "Disconnecting from IMAP server " \ + + self.host + self.connection.logout() + + def get_mail_list(self): + print >>sys.stderr, "Getting mail list" + typ, data = self.connection.select(self.mailbox) + typ, data = self.connection.search(None, 'UNSEEN') + if typ == 'OK': + return data[0].split(' ') + return None + + def get_mail(self, index): + print >>sys.stderr, "Getting mail " + str(index) + typ, data = self.connection.select(self.mailbox) + typ, data = self.connection.fetch(index, '(RFC822)') + self.connection.store(index, "FLAGS", "UNSEEN") + if typ == 'OK': + return self.format_message(email.message_from_string(data[0][1])) + return u"Error while fetching mail " + str(index) + + def get_mail_summary(self, index): + print >>sys.stderr, "Getting mail summary " + str(index) + typ, data = self.connection.select(self.mailbox) + typ, data = self.connection.fetch(index, '(RFC822)') + self.connection.store(index, "FLAGS", "UNSEEN") + if typ == 'OK': + return self.format_message_summary(email.message_from_string(data[0][1])) + return u"Error while fetching mail " + str(index) + +class POP3Connection(MailConnection): + def __init__(self, login = "", password = "", host = "", \ + port = None, ssl = False): + if not port: + if ssl: + port = 995 + else: + port = 110 + MailConnection.__init__(self, login, password, host, port, ssl) + + def get_type(self): + if self.ssl: + return "pop3s" + return "pop3" + + def connect(self): + print >>sys.stderr, "Connecting to POP3 server " \ + + self.login + "@" + self.host + ":" + str(self.port)\ + + ". SSL=" + str(self.ssl) + if self.ssl: + self.connection = MYPOP3_SSL(self.host, self.port) + else: + self.connection = MYPOP3(self.host, self.port) + try: + self.connection.apop(self.login, self.password) + except: + self.connection.user(self.login) + self.connection.pass_(self.password) + + + def disconnect(self): + print >>sys.stderr, "Disconnecting from POP3 server " \ + + self.host + self.connection.quit() + + def get_mail_list(self): + print >>sys.stderr, "Getting mail list" + count, size = self.connection.stat() + return [str(i) for i in range(1, count + 1)] + + def get_mail(self, index): + print >>sys.stderr, "Getting mail " + str(index) + ret, data, size = self.connection.retr(index) + if ret[0:3] == '+OK': + return self.format_message(email.message_from_string('\n'.join(data))) + return u"Error while fetching mail " + str(index) + + def get_mail_summary(self, index): + print >>sys.stderr, "Getting mail summary " + str(index) + ret, data, size = self.connection.retr(index) + if ret[0:3] == '+OK': + return self.format_message_summary(email.message_from_string('\n'.join(data))) + return u"Error while fetching mail " + str(index) diff --git a/jabber/mailconnection_factory.py b/jabber/mailconnection_factory.py new file mode 100644 index 0000000..f3e4169 --- /dev/null +++ b/jabber/mailconnection_factory.py @@ -0,0 +1,108 @@ +## +## mailconnection_factory.py +## Login : +## Started on Fri May 20 10:41:46 2005 adro8400 +## $Id: mailconnection_factory.py,v 1.2 2005/09/18 20:24:07 dax Exp $ +## +## Copyright (C) 2005 adro8400 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +from mailconnection import IMAPConnection, POP3Connection + +""" Static method to return an empty MailConnection object of given type +:Parameters: + - 'type': type of connection to return : 'imap', 'imaps', 'pop3', 'pop3s' + +:return: MailConnection of given type in parameter, None if unknown type + +:returntype: 'MailConnection'""" +def get_new_mail_connection(type): + if type == "imap": + return IMAPConnection() + elif type == "imaps": + return IMAPConnection(ssl = True) + elif type == "pop3": + return POP3Connection() + elif type == "pop3s": + return POP3Connection(ssl = True) + raise Exception, "Connection type \"" + type + "\" unknown" + +""" Static methode to create a MailConnection object filled from string + +:Parameters: +- 'connection_string': string containing MailConnection parameters separated +by '#'. ex: 'pop3#login#password#host#110#True' + +:Types: +- 'connection_string': string + +:return: MailConnection of given type found in string parameter + +:returntype: 'MailConnection'""" +def str_to_mail_connection(connection_string): + arg_list = connection_string.split("#") + # optionals values must be the at the beginning of the list to pop them + # last + arg_list.reverse() + type = arg_list.pop() + login = arg_list.pop() + password = arg_list.pop() + host = arg_list.pop() + port = int(arg_list.pop()) + ffc_action = int(arg_list.pop()) + online_action = int(arg_list.pop()) + away_action = int(arg_list.pop()) + ea_action = int(arg_list.pop()) + offline_action = int(arg_list.pop()) + result = None + if type == "imap": + mailbox = arg_list.pop() + result = IMAPConnection(login = login, \ + password = password, \ + host = host, \ + ssl = False, \ + port = port, \ + mailbox = mailbox) + elif type == "imaps": + mailbox = arg_list.pop() + result = IMAPConnection(login = login, \ + password = password, \ + host = host, \ + port = port, \ + ssl = True, \ + mailbox = mailbox) + elif type == "pop3": + result = POP3Connection(login = login, \ + password = password, \ + host = host, \ + port = port, \ + ssl = False) + elif type == "pop3s": + result = POP3Connection(login = login, \ + password = password, \ + host = host, \ + port = port, \ + ssl = True) + if result is None: + raise Exception, "Connection type \"" + type + "\" unknown" + result.ffc_action = ffc_action + result.online_action = online_action + result.away_action = away_action + result.ea_action = ea_action + result.offline_action = offline_action + return result + + diff --git a/jabber/storage.py b/jabber/storage.py new file mode 100644 index 0000000..9653e7f --- /dev/null +++ b/jabber/storage.py @@ -0,0 +1,180 @@ +## +## storage.py +## Login : +## Started on Wed Jul 20 20:26:53 2005 dax +## $Id: storage.py,v 1.1 2005/09/18 20:24:07 dax Exp $ +## +## Copyright (C) 2005 dax +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import os +import re +import os.path +import sys +import anydbm +import mailconnection_factory +from UserDict import UserDict + +class Storage(UserDict): + def __init__(self, nb_pk_fields = 1, spool_dir = "."): + UserDict.__init__(self) + self._spool_dir = "" + self.set_spool_dir(spool_dir) + self.nb_pk_fields = nb_pk_fields + self.file = self._spool_dir + "/registered.db" + + # return hash of hash (jid and name) + def load(self): + pass + + def sync(self): + pass + + def store(self, nb_pk_fields, registered, pk): + pass + + def add(self, key_list, obj): + pass + + def remove(self, key_list): + pass + + def get_spool_dir(self): + return self._spool_dir + + def set_spool_dir(self, spool_dir): + self._spool_dir = spool_dir + if not os.path.isdir(self._spool_dir): + os.makedirs(self._spool_dir) + + spool_dir = property(get_spool_dir, set_spool_dir) + +class DBMStorage(Storage): + def __init__(self, nb_pk_fields = 1, spool_dir = "."): +# print "DBM INIT" + Storage.__init__(self, nb_pk_fields, spool_dir) + self.__str_registered = anydbm.open(self.file, \ + 'c') + + def __del__(self): +# print "DBM STOP" + self.__str_registered.close() + + def load(self): +# print "DBM LOAD" + result = {} + try: + for pk in self.__str_registered.keys(): + pk_list = pk.split('#') + obj = result + key = None + while pk_list: + key = pk_list.pop(0) + if pk_list: + if not obj.has_key(key): + obj[key] = {} + obj = obj[key] + obj[key] = mailconnection_factory.str_to_mail_connection(self.__str_registered[pk]) + except Exception, e: + print >>sys.stderr, "Cannot load registered.db : " + print >>sys.stderr, e + return result + + def sync(self): +# print "DBM SYNC" + self.__str_registered.close() + self.__str_registered = anydbm.open(self.file, \ + 'c') + + def __store(self, nb_pk_fields, registered, pk): + if nb_pk_fields > 0: + for key in registered.keys(): + if pk: + self.__store(nb_pk_fields - 1, \ + registered[key], pk + "#" + key) + else: + self.__store(nb_pk_fields - 1, \ + registered[key], key) + else: + self.__str_registered[pk] = str(registered) + print "STORING : " + pk + " = " + str(registered) + + def store(self, registered): +# print "DBM STORE" + try: + self.__store(self.nb_pk_fields, registered, "") + # Force file synchronisation + self.sync() + except Exception, e: + print >>sys.stderr, "Cannot save to registered.db : " + print >>sys.stderr, e + + def __setitem__(self, pk_tuple, obj): + print "Adding " + "#".join(pk_tuple) + " = " + str(obj) + self.__str_registered["#".join(pk_tuple)] = str(obj) + self.sync() + + def __getitem__(self, pk_tuple): + print "Getting " + "#".join(pk_tuple) + if len(pk_tuple) == self.nb_pk_fields: + return mailconnection_factory.str_to_mail_connection(self.__str_registered["#".join(pk_tuple)]) + else: + partial_key = "#".join(pk_tuple) + regexp = re.compile(partial_key) + return [mailconnection_factory.str_to_mail_connection(self.__str_registered[key]) + for key in self.__str_registered.keys() + if regexp.search(key)] + + def __delitem__(self, pk_tuple): + print "Deleting " + "#".join(pk_tuple) + del self.__str_registered["#".join(pk_tuple)] + self.sync() + + def has_key(self, pk_tuple): + return self.__str_registered.has_key("#".join(pk_tuple)) + + def keys(self, pk_tuple = None): + if pk_tuple is None: + return [tuple(key.split("#")) for key in self.__str_registered.keys()] + else: + level = len(pk_tuple) + partial_key = "#".join(pk_tuple) + regexp = re.compile("^" + partial_key) + result = {} + for key in self.__str_registered.keys(): + if regexp.search(key): + result[key.split("#")[level]] = None + return result.keys() + +class SQLiteStorage(Storage): + def __init__(self, nb_pk_fields = 1, spool_dir = "."): + pass + + def load(self): + pass + + def sync(self): + pass + + def store(self, nb_pk_fields, registered, pk): + pass + + def add(self, key_list, obj): + pass + + def remove(self, key_list): + pass + diff --git a/jabber/x.py b/jabber/x.py new file mode 100644 index 0000000..a9e41d3 --- /dev/null +++ b/jabber/x.py @@ -0,0 +1,129 @@ +## +## x.py +## Login : David Rousselie +## Started on Fri Jan 7 11:06:42 2005 +## $Id: x.py,v 1.3 2005/09/18 20:24:07 dax Exp $ +## +## Copyright (C) 2005 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import sys +from pyxmpp.stanza import common_doc + +class Option(object): + def __init__(self, label, value): + self.__label = label + self.__value = value + + def get_xml(self, parent): + if parent is None: + option = common_doc.newChild(None, "option", None) + else: + option = parent.newChild(None, "option", None) + option.setProp("label", self.__label) + option.newChild(None, "value", self.__value) + return option + +class Field(object): + def __init__(self, type, label, var, value): + self.__type = type + self.__label = label + self.__var = var + self.value = value + self.__options = [] + + def add_option(self, label, value): + option = Option(label, value) + self.__options.append(option) + return option + + def get_xml(self, parent): + if parent is None: + raise Exception, "parent field should not be None" + else: + field = parent.newChild(None, "field", None) + field.setProp("type", self.__type) + if not self.__label is None: + field.setProp("label", self.__label) + if not self.__var is None: + field.setProp("var", self.__var) + if self.value: + field.newChild(None, "value", self.value) + for option in self.__options: + option.get_xml(field) + return field + +class X(object): + def __init__(self): + self.fields = {} + self.fields_tab = [] + self.title = None + self.instructions = None + self.type = None + self.xmlns = None + + def add_field(self, type = "fixed", label = None, var = None, value = ""): + field = Field(type, label, var, value) + self.fields[var] = field + # fields_tab exist to keep added fields order + self.fields_tab.append(field) + return field + + def attach_xml(self, iq): + node = iq.newChild(None, "x", None) + _ns = node.newNs(self.xmlns, None) + node.setNs(_ns) + if not self.title is None: + node.newTextChild(None, "title", self.title) + if not self.instructions is None: + node.newTextChild(None, "instructions", self.instructions) + for field in self.fields_tab: + field.get_xml(node) + return node + + def from_xml(self, node): + ## TODO : test node type and ns and clean that loop !!!! + while node and node.type != "element": + node = node.next + child = node.children + while child: + ## TODO : test child type (element) and ns (jabber:x:data) + if child.type == "element" and child.name == "field": + if child.hasProp("type"): + type = child.prop("type") + else: + type = "" + + if child.hasProp("label"): + label = child.prop("label") + else: + label = "" + + if child.hasProp("var"): + var = child.prop("var") + else: + var = "" + + xval = child.children + while xval and xval.name != "value": + xval = xval.next + if xval: + value = xval.getContent() + else: + value = "" + field = Field(type, label, var, value) + self.fields[var] = field + child = child.next diff --git a/jmc.py b/jmc.py new file mode 100755 index 0000000..9cde31b --- /dev/null +++ b/jmc.py @@ -0,0 +1,74 @@ +#!/usr/bin/python -u +## +## Jabber Mail Component +## jmc.py +## Login : David Rousselie +## Started on Fri Jan 7 11:06:42 2005 +## $Id: jmc.py,v 1.3 2005/07/11 20:39:31 dax Exp $ +## +## Copyright (C) 2005 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import sys +import os.path +import logging + +from jabber.component import MailComponent, ComponentFatalError +from jabber.config import Config + +def main(config_file = "jmc.xml", isDebug = 0): + try: + logger = logging.getLogger() + logger.addHandler(logging.StreamHandler()) + if isDebug > 0: + logger.setLevel(logging.DEBUG) + try: + logger.debug("Loading config file " + config_file) + config = Config(config_file) + except: + print >>sys.stderr, "Couldn't load config file:", \ + str(sys.exc_value) + sys.exit(1) + + print "creating component..." + mailcomp = MailComponent(config) + + print "starting..." + mailcomp.run(1) + except ComponentFatalError,e: + print e + print "Aborting." + sys.exit(1) + +if __name__ == "__main__": + var_option = 0 + file_num = 0 + index = 0 + debug_level = 0 + for opt in sys.argv: + if var_option == 0 and len(opt) == 2 and opt == "-c": + var_option += 1 + elif (var_option & 1) == 1 and len(opt) > 0: + var_option += 1 + file_num = index + if len(opt) == 2 and opt == "-D": + debug_level = 1 + index += 1 + if (var_option & 2) == 2: + main(sys.argv[file_num], debug_level) + else: + main("jmc.xml", debug_level) + diff --git a/jmc.xml b/jmc.xml new file mode 100644 index 0000000..82961b6 --- /dev/null +++ b/jmc.xml @@ -0,0 +1,18 @@ + + + 127.0.0.1 + 5347 + secret + jmc.localhost + 5 + fr + + Jabber Mail Component + A Jabber mail server component + http://people.happycoders.org/dax/jabber/jmc/ + + + DBM + /var/spool/jabber + 1 + diff --git a/run_test.py b/run_test.py new file mode 100644 index 0000000..4b97f72 --- /dev/null +++ b/run_test.py @@ -0,0 +1,70 @@ +## +## test.py +## Login : +## Started on Wed May 18 13:33:03 2005 adro8400 +## $Id: run_test.py,v 1.2 2005/09/18 20:24:07 dax Exp $ +## +## Copyright (C) 2005 adro8400 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import coverage +coverage.erase() +coverage.start() +import unittest +import sys +import tests +from tests.test_mailconnection import * +from tests.test_mailconnection_factory import * +from tests.test_component import * +from tests.test_storage import * +from test import test_support +import jabber + +if __name__ == '__main__': + mail_connection_suite = unittest.makeSuite(MailConnection_TestCase, \ + "test") + pop3_connection_suite = unittest.makeSuite(POP3Connection_TestCase, \ + "test") + imap_connection_suite = unittest.makeSuite(IMAPConnection_TestCase, \ + "test") + mc_factory_suite = unittest.makeSuite(MailConnectionFactory_TestCase, \ + "test") + component_suite = unittest.makeSuite(MailComponent_TestCase_Basic, \ + "test") + component2_suite = unittest.makeSuite(MailComponent_TestCase_NoReg, \ + "test") + storage_suite = unittest.makeSuite(Storage_TestCase, \ + "test") + dbmstorage_suite = unittest.makeSuite(DBMStorage_TestCase, \ + "test") + + jmc_suite = unittest.TestSuite((mail_connection_suite, \ + pop3_connection_suite, \ + imap_connection_suite, \ + mc_factory_suite, \ + component_suite, \ + component2_suite, \ + storage_suite, \ + dbmstorage_suite)) + test_support.run_suite(component_suite) + +# coverage.stop() +# coverage.analysis(jabber.mailconnection_factory) +# coverage.analysis(jabber.mailconnection) +# coverage.analysis(jabber.component) +# coverage.analysis(jabber.x) +# coverage.report([jabber.mailconnection_factory, jabber.mailconnection, \ +# jabber.component, jabber.x]) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dummy_server.py b/tests/dummy_server.py new file mode 100644 index 0000000..fba36cb --- /dev/null +++ b/tests/dummy_server.py @@ -0,0 +1,169 @@ +## +## dummy_server.py +## Login : +## Started on Fri May 13 12:53:17 2005 adro8400 +## $Id: dummy_server.py,v 1.1 2005/07/11 20:39:31 dax Exp $ +## +## Copyright (C) 2005 adro8400 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import sys +import time +import traceback +import re +import socket +import types +import select +import xml.dom.minidom +import utils +from pyxmpp import xmlextra + +class DummyServer: + def __init__(self, host, port, responses = None): + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE): + af, socktype, proto, canonname, sa = res + try: + s = socket.socket(af, socktype, proto) + except socket.error, msg: + s = None + continue + try: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(sa) + s.listen(1) + except socket.error, msg: + s.close() + s = None + continue + break + self.socket = s + self.responses = None + self.queries = None + self.real_queries = [] + + def __del__(self): + self.socket.close() + + def serve(self): + conn, addr = self.socket.accept() + rfile = conn.makefile('rb', -1) + if self.responses: + data = None + for idx in range(len(self.responses)): + # if response is a function apply it (it must return a string) + # it is given previous received data + if isinstance(self.responses[idx], types.FunctionType): + response = self.responses[idx](data) + else: + response = self.responses[idx] + if response is not None: + print >>sys.stderr, 'Sending : ', response + conn.send(response) + data = rfile.readline() + if not data: + break + else: + self.real_queries.append(data) + print >>sys.stderr, 'Receive : ', data + conn.close() + + def verify_queries(self): + result = True + queries_len = len(self.queries) + if queries_len == len(self.real_queries): + for idx in range(queries_len): + real_query = self.real_queries[idx] + match = (re.compile(self.queries[idx], re.M).match(real_query) is not None) + if not match: + result = False + print >>sys.stderr, "Unexpected query :\n" + \ + "Expected query : _" + self.queries[idx] + "_\n" + \ + "Receive query : _" + real_query + "_\n" + else: + result = False + print >>sys.stderr, "Expected " + str(queries_len) + \ + " queries, got " + str(len(self.real_queries)) + return result + +class XMLDummyServer(DummyServer): + def __init__(self, host, port, responses, stream_handler): + DummyServer.__init__(self, host, port, responses) + self._reader = xmlextra.StreamReader(stream_handler) + + def serve(self): + try: + conn, addr = self.socket.accept() + if self.responses: + data = None + for idx in range(len(self.responses)): + try: + # TODO : this approximation is not clean + # received size is based on the expected size in self.queries + data = conn.recv(1024 + len(self.queries[idx])) + if data: + print "-----------RECEIVE " + data + r = self._reader.feed(data) + except: + type, value, stack = sys.exc_info() + print "".join (traceback.format_exception + (type, value, stack, 5)) + raise + # TODO verify got all data + if data: + self.real_queries.append(data) + # if response is a function apply it (it must return a string) + # it is given previous received data + if isinstance(self.responses[idx], types.FunctionType): + response = self.responses[idx](data) + else: + response = self.responses[idx] + if response is not None: + print >>sys.stderr, '---------SENDING : ', response + conn.send(response) + conn.close() + except: + type, value, stack = sys.exc_info() + print "".join (traceback.format_exception + (type, value, stack, 5)) + raise + + def verify_queries(self): + result = True + queries_len = len(self.queries) + if queries_len == len(self.real_queries): + for idx in range(queries_len): + real_query = xml.dom.minidom.parseString(self.real_queries[idx]) + recv_query = xml.dom.minidom.parseString(self.queries[idx]) + if not utils.xmldiff(real_query, recv_query): + result = False + print >>sys.stderr, "Unexpected query :\n" + \ + "Expected query : _" + self.queries[idx] + "_\n" + \ + "Receive query : _" + self.real_queries[idx] + "_\n" + else: + result = False + print >>sys.stderr, "Expected " + str(queries_len) + \ + " queries, got " + str(len(self.real_queries)) + return result + +def test(): + server = DummyServer(("localhost", 4242)) + server.responses = ["rep1\n", "rep2\n"] + server.serve() + +if __name__ == '__main__': + test() + + diff --git a/tests/email_generator.py b/tests/email_generator.py new file mode 100644 index 0000000..93f5e40 --- /dev/null +++ b/tests/email_generator.py @@ -0,0 +1,65 @@ +## +# -*- coding: iso-8859-1 -*- +## email_generator.py +## Login : +## Started on Tue May 17 15:33:35 2005 adro8400 +## $Id: email_generator.py,v 1.1 2005/07/11 20:39:31 dax Exp $ +## +## Copyright (C) 2005 adro8400 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +from email.Header import Header +from email.MIMEText import MIMEText +from email.MIMEMultipart import MIMEMultipart + +def _create_multipart(encoded): + msg = MIMEMultipart() + if encoded: + part1 = MIMEText("Encoded multipart1 with 'iso-8859-15' charset (יאך)", \ + _charset = "iso-8859-15") + msg.attach(part1) + part2 = MIMEText("Encoded multipart2 with 'iso-8859-15' charset (יאך)", \ + _charset = "iso-8859-15") + msg.attach(part2) + else: + part1 = MIMEText("Not encoded multipart1") + msg.attach(part1) + part2 = MIMEText("Not encoded multipart2") + msg.attach(part2) + return msg + +def _create_singlepart(encoded): + if encoded: + return MIMEText("Encoded single part with 'iso-8859-15' charset (יאך)", \ + _charset = "iso-8859-15") + else: + return MIMEText("Not encoded single part") + +def generate(encoded, multipart, header): + msg = None + if multipart: + msg = _create_multipart(encoded) + else: + msg = _create_singlepart(encoded) + if header: + if encoded: + msg['Subject'] = Header("encoded subject (יאך)", "iso-8859-15") + msg['From'] = Header("encoded from (יאך)", "iso-8859-15") + else: + msg['Subject'] = Header("not encoded subject") + msg['From'] = Header("not encoded from") + return msg + diff --git a/tests/jmc-test.xml b/tests/jmc-test.xml new file mode 100644 index 0000000..efa4dfe --- /dev/null +++ b/tests/jmc-test.xml @@ -0,0 +1,18 @@ + + + 127.0.0.1 + 55555 + secret + jmc.localhost + 5 + fr + + Jabber Mail Component + A Jabber mail server component + http://people.happycoders.org/dax/jabber/jmc/ + + + DBM + . + 1 + diff --git a/tests/test_component.py b/tests/test_component.py new file mode 100644 index 0000000..cce9c71 --- /dev/null +++ b/tests/test_component.py @@ -0,0 +1,275 @@ +## +## test_mailconnection_factory.py +## Login : +## Started on Fri May 20 10:46:58 2005 adro8400 +## $Id: test_component.py,v 1.2 2005/09/18 20:24:07 dax Exp $ +## +## Copyright (C) 2005 adro8400 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import thread +import unittest +import dummy_server +import time +import traceback +from pyxmpp import xmlextra +from jabber.component import * +from jabber.config import Config + +class TestStreamHandler(xmlextra.StreamHandler): + def __init__(self, expected_balises = []): + xmlextra.StreamHandler.__init__(self) + self.expected_balises = expected_balises + + def stream_start(self, doc): + pass + + def stream_end(self, doc): + pass + + def stanza(self, notused, node): + pass + +class MailComponent_TestCase_NoConnection(unittest.TestCase): + def setUp(self): + self.mail_component = MailComponent(Config("tests/jmc-test.xml")) + + def tearDown(self): + self.mail_component = None + + def test_get_reg_form(self): + reg_form = self.mail_component.get_reg_form() + reg_form2 = self.mail_component.get_reg_form() + self.assertTrue(reg_form is reg_form2) + +#TODO +class MailComponent_TestCase_Basic(unittest.TestCase): + def setUp(self): + self.handler = TestStreamHandler() + self.mail_component = MailComponent(Config("tests/jmc-test.xml")) + self.server = dummy_server.XMLDummyServer("localhost", 55555, None, self.handler) + thread.start_new_thread(self.server.serve, ()) + + def tearDown(self): + self.server = None + self.mail_component = None + + def test_run(self): + self.server.responses = ["", \ + ""] + # TODO : concatenate all queries to parse xml + self.server.queries = ["", \ + ""] +# self.server.queries = ["<\?xml version=\"1.0\" encoding=\"UTF-8\"\?>\s", \ +# "\s*"] + self.mail_component.run(1) + self.failUnless(self.server.verify_queries()) + # TODO : more assertion + +class MailComponent_TestCase_NoReg(unittest.TestCase): + def setUp(self): + self.handler = TestStreamHandler() + logger = logging.getLogger() + logger.addHandler(logging.StreamHandler()) + logger.setLevel(logging.DEBUG) + self.mail_component = MailComponent(Config("tests/jmc-test.xml")) + self.server = dummy_server.XMLDummyServer("localhost", 55555, None, self.handler) + thread.start_new_thread(self.server.serve, ()) + + def tearDown(self): + self.server = None + self.mail_component = None + + def test_disco_get_items(self): + self.server.responses = ["", \ + "", \ + ""] + self.server.queries = ["<\?xml version=\"1.0\" encoding=\"UTF-8\"\?>\s?" + \ + "", \ + "\s*", \ + ""] + self.mail_component.run(1) + self.failUnless(self.server.verify_queries()) + + def test_get_register(self): + pass + self.server.responses = ["", \ + "", \ + ""] + self.server.queries = ["<\?xml version=\"1.0\" encoding=\"UTF-8\"\?>\s?" + \ + "", \ + "\s*", \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "Jabber Mail connection registration\s?" + \ + "Enter anything below\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "INBOX\s?" + \ + "\s?" + \ + "\s?" + \ + "2\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "2\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "1\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "1\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "0\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + "5\s?" + \ + "\s?" + \ + "\s?" + \ + "\s?" + \ + ""] + self.mail_component.run(1) + self.failUnless(self.server.verify_queries()) + + + #self.mail_component.get_version() + +# def test_disco_get_info(self): +# pass + +# def test_get_register(self): +# pass + +# def test_set_register(self): +# pass + +class MailComponent_TestCase_Reg(unittest.TestCase): + def setUp(self): + self.mail_component = MailComponent(Config("tests/jmc-test.xml")) + self.mail_component.set_register(iq = None) + + def test_get_reg_form_init(self): + pass + + def test_get_reg_form_init_2pass(self): + pass + + def test_disco_get_items(self): + pass + + def test_get_register(self): + pass + + def test_set_register_update(self): + pass + + def test_set_register_remove(self): + pass + + def test_presence_available_transport(self): + pass + + def test_presence_available_mailbox(self): + pass + + def test_presence_unavailable_transport(self): + pass + + def test_presence_unavailable_mailbox(self): + pass + + def test_presence_subscribe(self): + pass + + def test_presence_subscribed_transport(self): + pass + + def test_presence_subscribed_mailbox(self): + pass + + def test_presence_unsubscribe(self): + pass + + def test_presence_unsubscribed(self): + pass + + def test_check_mail(self): + pass + diff --git a/tests/test_mailconnection.py b/tests/test_mailconnection.py new file mode 100644 index 0000000..9844682 --- /dev/null +++ b/tests/test_mailconnection.py @@ -0,0 +1,264 @@ +# -*- coding: iso-8859-15 -*- +## +## mailconnection_test.py +## Login : +## Started on Fri May 13 11:32:51 2005 adro8400 +## $Id: test_mailconnection.py,v 1.2 2005/09/18 20:24:07 dax Exp $ +## +## Copyright (C) 2005 adro8400 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import unittest +from jabber.mailconnection import IMAPConnection, \ + POP3Connection, \ + MailConnection +import dummy_server +import email_generator +import thread +import re +import sys +import string + +class MailConnection_TestCase(unittest.TestCase): + def setUp(self): + self.connection = MailConnection() + + def make_test(email_type, tested_func, expected_res): + def inner(self): + encoded, multipart, header = email_type + email = email_generator.generate(encoded, \ + multipart, \ + header) + part = tested_func(self, email) + self.assertEquals(part, expected_res) + return inner + + test_get_decoded_part_not_encoded = \ + make_test((False, False, False), \ + lambda self, email: self.connection.get_decoded_part(email), \ + u"Not encoded single part") + + test_get_decoded_part_encoded = \ + make_test((True, False, False), \ + lambda self, email: self.connection.get_decoded_part(email), \ + u"Encoded single part with 'iso-8859-15' charset (יאך)") + + test_format_message_summary_not_encoded = \ + make_test((False, False, True), \ + lambda self, email: self.connection.format_message_summary(email), \ + u"From : not encoded from\nSubject : not encoded subject\n\n") + + test_format_message_summary_encoded = \ + make_test((True, False, True), \ + lambda self, email: self.connection.format_message_summary(email), \ + u"From : encoded from (יאך)\nSubject : encoded subject " + \ + u"(יאך)\n\n") + + test_format_message_summary_partial_encoded = \ + make_test((True, False, True), \ + lambda self, email: \ + email.replace_header("Subject", \ + "\"" + str(email["Subject"]) \ + + "\" not encoded part") or \ + email.replace_header("From", \ + "\"" + str(email["From"]) \ + + "\" not encoded part") or \ + self.connection.format_message_summary(email), \ + u"From : \"encoded from (יאך)\" not encoded part\nSubject " + \ + u": \"encoded subject (יאך)\" not encoded part\n\n") + + test_format_message_single_not_encoded = \ + make_test((False, False, True), \ + lambda self, email: self.connection.format_message(email), \ + u"From : not encoded from\nSubject : not encoded subject" + \ + u"\n\nNot encoded single part\n") + + test_format_message_single_encoded = \ + make_test((True, False, True), \ + lambda self, email: self.connection.format_message(email), \ + u"From : encoded from (יאך)\nSubject : encoded subject " + \ + u"(יאך)\n\nEncoded single part with 'iso-8859-15' charset" + \ + u" (\xe9\xe0\xea)\n") + + test_format_message_multi_not_encoded = \ + make_test((False, True, True), \ + lambda self, email: self.connection.format_message(email), \ + u"From : not encoded from\nSubject : not encoded subject" + \ + u"\n\nNot encoded multipart1\nNot encoded multipart2\n") + + test_format_message_multi_encoded = \ + make_test((True, True, True), \ + lambda self, email: self.connection.format_message(email), \ + u"From : encoded from (יאך)\nSubject : encoded subject (יא" + \ + u"ך)\n\nEncoded multipart1 with 'iso-8859-15' charset (יאך" + \ + u")\nEncoded multipart2 with 'iso-8859-15' charset (יאך)\n") + + +class POP3Connection_TestCase(unittest.TestCase): + def setUp(self): + self.server = dummy_server.DummyServer("localhost", 1110) + thread.start_new_thread(self.server.serve, ()) + self.pop3connection = POP3Connection("login", \ + "pass", \ + "localhost", \ + 1110, \ + ssl = False) + + def tearDown(self): + self.server = None + self.pop3connection = None + + def make_test(responses = None, queries = None, core = None): + def inner(self): + self.server.responses = ["+OK connected\r\n", \ + "+OK name is a valid mailbox\r\n", \ + "+OK pass\r\n"] + if responses: + self.server.responses += responses + self.server.queries = ["USER login\r\n", \ + "PASS pass\r\n"] + if queries: + self.server.queries += queries + self.server.queries += ["QUIT\r\n"] + self.pop3connection.connect() + self.failUnless(self.pop3connection.connection, \ + "Cannot establish connection") + if core: + core(self) + self.pop3connection.disconnect() + self.failUnless(self.server.verify_queries(), \ + "Sended queries does not match expected queries.") + return inner + + test_connection = make_test() + + test_get_mail_list = \ + make_test(["+OK 2 20\r\n"], \ + ["STAT\r\n"], \ + lambda self: \ + self.assertEquals(self.pop3connection.get_mail_list(), \ + ["1", "2"])) + + test_get_mail_summary = \ + make_test(["+OK 10 octets\r\n" + \ + "From: user@test.com\r\n" + \ + "Subject: subject test\r\n\r\n" + \ + "mymessage\r\n.\r\n"], \ + ["RETR 1\r\n"], \ + lambda self: \ + self.assertEquals(self.pop3connection.get_mail_summary(1), \ + "From : user@test.com\n" + \ + "Subject : subject test\n\n")) + + test_get_mail = \ + make_test(["+OK 10 octets\r\n" + \ + "From: user@test.com\r\n" + \ + "Subject: subject test\r\n\r\n" + \ + "mymessage\r\n.\r\n"], \ + ["RETR 1\r\n"], \ + lambda self: \ + self.assertEquals(self.pop3connection.get_mail(1), \ + "From : user@test.com\n" + \ + "Subject : subject test\n\n" + \ + "mymessage\n")) + + +class IMAPConnection_TestCase(unittest.TestCase): + def setUp(self): + self.server = dummy_server.DummyServer("localhost", 1143) + thread.start_new_thread(self.server.serve, ()) + self.imap_connection = IMAPConnection("login", \ + "pass", \ + "localhost", \ + 1143, \ + ssl = False) + + def tearDown(self): + self.server = None + self.imap_connection = None + + def make_test(responses = None, queries = None, core = None): + def inner(self): + self.server.responses = ["* OK [CAPABILITY IMAP4 LOGIN-REFERRALS " + \ + "AUTH=PLAIN]\n", \ + lambda data: "* CAPABILITY IMAP4 " + \ + "LOGIN-REFERRALS AUTH=PLAIN\n" + \ + data.split()[0] + \ + " OK CAPABILITY completed\n", \ + lambda data: data.split()[0] + \ + " OK LOGIN completed\n"] + if responses: + self.server.responses += responses + self.server.queries = ["^[^ ]* CAPABILITY", \ + "^[^ ]* LOGIN login \"pass\""] + if queries: + self.server.queries += queries + self.server.queries += ["^[^ ]* LOGOUT"] + self.imap_connection.connect() + self.failUnless(self.imap_connection.connection, \ + "Cannot establish connection") + if core: + core(self) + self.imap_connection.disconnect() + self.failUnless(self.server.verify_queries("Sended queries does not match expected queries.")) + return inner + + test_connection = make_test() + + test_get_mail_list = make_test([lambda data: "* 42 EXISTS\n* 1 RECENT\n* OK" +\ + " [UNSEEN 9]\n* FLAGS (\Deleted \Seen\*)\n*" +\ + " OK [PERMANENTFLAGS (\Deleted \Seen\*)\n" + \ + data.split()[0] + \ + " OK [READ-WRITE] SELECT completed\n", \ + lambda data: "* SEARCH 9 10 \n" + \ + data.split()[0] + " OK SEARCH completed\n"], \ + ["^[^ ]* SELECT INBOX", \ + "^[^ ]* SEARCH UNSEEN"], \ + lambda self: \ + self.assertEquals(self.imap_connection.get_mail_list(), ['9', '10'])) + + test_get_mail_summary = make_test([lambda data: "* 42 EXISTS\r\n* 1 RECENT\r\n* OK" +\ + " [UNSEEN 9]\r\n* FLAGS (\Deleted \Seen\*)\r\n*" +\ + " OK [PERMANENTFLAGS (\Deleted \Seen\*)\r\n" + \ + data.split()[0] + \ + " OK [READ-WRITE] SELECT completed\r\n", \ + lambda data: "* 1 FETCH ((RFC822) {12}\r\nbody" + \ + " text\r\n)\r\n" + \ + data.split()[0] + " OK FETCH completed\r\n", \ + lambda data: "* 1 FETCH (FLAGS (\UNSEEN))\r\n" + \ + data.split()[0] + " OK STORE completed\r\n"], \ + ["^[^ ]* SELECT INBOX", \ + "^[^ ]* FETCH 1 \(RFC822\)", \ + "^[^ ]* STORE 1 FLAGS \(UNSEEN\)"], \ + lambda self: self.assertEquals(self.imap_connection.get_mail_summary(1), \ + "From : None\nSubject : None\n\n")) + + test_get_mail = make_test([lambda data: "* 42 EXISTS\r\n* 1 RECENT\r\n* OK" +\ + " [UNSEEN 9]\r\n* FLAGS (\Deleted \Seen\*)\r\n*" +\ + " OK [PERMANENTFLAGS (\Deleted \Seen\*)\r\n" + \ + data.split()[0] + \ + " OK [READ-WRITE] SELECT completed\r\n", \ + lambda data: "* 1 FETCH ((RFC822) {11}\r\nbody" + \ + " text\r\n)\r\n" + \ + data.split()[0] + " OK FETCH completed\r\n", \ + lambda data: "* 1 FETCH (FLAGS (\UNSEEN))\r\n" + \ + data.split()[0] + " OK STORE completed\r\n"], \ + ["^[^ ]* SELECT INBOX", \ + "^[^ ]* FETCH 1 \(RFC822\)", \ + "^[^ ]* STORE 1 FLAGS \(UNSEEN\)"], \ + lambda self: self.assertEquals(self.imap_connection.get_mail(1), \ + "From : None\nSubject : None\n\nbody text\r\n\n")) + diff --git a/tests/test_mailconnection_factory.py b/tests/test_mailconnection_factory.py new file mode 100644 index 0000000..1d0ad5f --- /dev/null +++ b/tests/test_mailconnection_factory.py @@ -0,0 +1,96 @@ +## +## test_mailconnection_factory.py +## Login : +## Started on Fri May 20 10:46:58 2005 adro8400 +## $Id: test_mailconnection_factory.py,v 1.1 2005/07/11 20:39:31 dax Exp $ +## +## Copyright (C) 2005 adro8400 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import unittest +from jabber.mailconnection_factory import * + +class MailConnectionFactory_TestCase(unittest.TestCase): + def test_new_mail_connection_imap(self): + mc = get_new_mail_connection("imap") + self.assertEquals(mc, mc) + + def test_new_mail_connection_imaps(self): + mc = get_new_mail_connection("imaps") + self.assertEquals(mc, mc) + + def test_new_mail_connection_pop3(self): + mc = get_new_mail_connection("pop3") + self.assertEquals(mc, mc) + + def test_new_mail_connection_pop3s(self): + mc = get_new_mail_connection("pop3s") + self.assertEquals(mc, mc) + + def test_str_to_mail_connection_imap(self): + mc = str_to_mail_connection("imap#login#passwd#host#193#nothing#nothing#nothing#nothing#retrieve#INBOX") + self.assertEquals(mc.get_type(), "imap") + self.assertEquals(mc.login, "login") + self.assertEquals(mc.password, "passwd") + self.assertEquals(mc.host, "host") + self.assertEquals(mc.port, 193) + self.assertEquals(mc.mailbox, "INBOX") + self.assertEquals(mc.ffc_action, "nothing") + self.assertEquals(mc.online_action, "nothing") + self.assertEquals(mc.away_action, "nothing") + self.assertEquals(mc.ea_action, "nothing") + self.assertEquals(mc.offline_action, "retrieve") + + def test_str_to_mail_connection_imaps(self): + mc = str_to_mail_connection("imaps#login#passwd#host#993#nothing#nothing#nothing#nothing#retrieve#INBOX.SubDir") + self.assertEquals(mc.get_type(), "imaps") + self.assertEquals(mc.login, "login") + self.assertEquals(mc.password, "passwd") + self.assertEquals(mc.host, "host") + self.assertEquals(mc.port, 993) + self.assertEquals(mc.mailbox, "INBOX.SubDir") + self.assertEquals(mc.ffc_action, "nothing") + self.assertEquals(mc.online_action, "nothing") + self.assertEquals(mc.away_action, "nothing") + self.assertEquals(mc.ea_action, "nothing") + self.assertEquals(mc.offline_action, "retrieve") + + def test_str_to_mail_connection_pop3(self): + mc = str_to_mail_connection("pop3#login#passwd#host#110#nothing#nothing#nothing#nothing#retrieve") + self.assertEquals(mc.get_type(), "pop3") + self.assertEquals(mc.login, "login") + self.assertEquals(mc.password, "passwd") + self.assertEquals(mc.host, "host") + self.assertEquals(mc.port, 110) + self.assertEquals(mc.ffc_action, "nothing") + self.assertEquals(mc.online_action, "nothing") + self.assertEquals(mc.away_action, "nothing") + self.assertEquals(mc.ea_action, "nothing") + self.assertEquals(mc.offline_action, "retrieve") + + def test_str_to_mail_connection_pop3s(self): + mc = str_to_mail_connection("pop3s#login#passwd#host#995#nothing#nothing#nothing#nothing#retrieve") + self.assertEquals(mc.get_type(), "pop3s") + self.assertEquals(mc.login, "login") + self.assertEquals(mc.password, "passwd") + self.assertEquals(mc.host, "host") + self.assertEquals(mc.port, 995) + self.assertEquals(mc.ffc_action, "nothing") + self.assertEquals(mc.online_action, "nothing") + self.assertEquals(mc.away_action, "nothing") + self.assertEquals(mc.ea_action, "nothing") + self.assertEquals(mc.offline_action, "retrieve") + diff --git a/tests/test_storage.py b/tests/test_storage.py new file mode 100644 index 0000000..1dd3fb7 --- /dev/null +++ b/tests/test_storage.py @@ -0,0 +1,150 @@ +## +## test_storage.py +## Login : +## Started on Fri May 20 10:46:58 2005 dax +## $Id: test_component.py,v 1.1 2005/07/11 20:39:31 dax Exp $ +## +## Copyright (C) 2005 adro8400 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import os +import unittest +import dummy_server +from jabber.storage import * +from jabber import mailconnection +from jabber.mailconnection import * + +class Storage_TestCase(unittest.TestCase): + def test_init(self): + spool_dir = "./spool/test" + self._storage = Storage(spool_dir = spool_dir) + self.assertTrue(os.access(spool_dir, os.F_OK)) + self.assertEquals(self._storage.spool_dir, spool_dir) + os.removedirs(spool_dir) + +# TODO +class SQLiteStorage_TestCase(unittest.TestCase): + def setUp(self): + self._storage = SQLiteStorage() + + def tearDown(self): + pass + +class DBMStorage_TestCase(unittest.TestCase): + def setUp(self): + self._storage = DBMStorage(nb_pk_fields = 2) + self._account1 = IMAPConnection(login = "login1", + password = "password1", + host = "host1", + port = 993, + ssl = True, + mailbox = "INBOX.box1") + self._account1.ffc_action = mailconnection.DO_NOTHING + self._account1.onlline_action = mailconnection.DO_NOTHING + self._account1.away_action = mailconnection.DO_NOTHING + self._account1.ea_action = mailconnection.DO_NOTHING + self._account1.offline_action = mailconnection.DO_NOTHING + self._account2 = IMAPConnection(login = "login2", + password = "password2", + host = "host2", + port = 1993, + ssl = False, + mailbox = "INBOX.box2") + self._account2.ffc_action = mailconnection.DO_NOTHING + self._account2.onlline_action = mailconnection.DO_NOTHING + self._account2.away_action = mailconnection.DO_NOTHING + self._account2.ea_action = mailconnection.DO_NOTHING + self._account2.offline_action = mailconnection.DO_NOTHING + + def tearDown(self): + os.remove(self._storage.file) + self._storage = None + + def test_set_get(self): + self._storage[("test@localhost", "account1")] = self._account1 + self._storage[("test@localhost", "account2")] = self._account2 + self.assertEquals(self._storage[("test@localhost", "account1")], self._account1) + self.assertEquals(self._storage[("test@localhost", "account2")], self._account2) + + def test_set_sync_get(self): + self._storage[("test@localhost", "account1")] = self._account1 + self._storage[("test@localhost", "account2")] = self._account2 + loaded_storage = DBMStorage(nb_pk_fields = 2) + self.assertEquals(loaded_storage[("test@localhost", "account1")], self._account1) + self.assertEquals(loaded_storage[("test@localhost", "account2")], self._account2) + + def test_set_del_get(self): + self._storage[("test@localhost", "account2")] = self._account2 + del self._storage[("test@localhost", "account2")] + try: + self._storage[("test@localhost", "account2")] + except KeyError: + return + self.fail("KeyError was expected") + + def test_haskey(self): + self._storage[("test@localhost", "account2")] = self._account2 + self.assertTrue(self._storage.has_key(("test@localhost", "account2"))) + + + def test_get_filtered(self): + self._storage[("test@localhost", "account1")] = self._account1 + self._storage[("test@localhost", "account2")] = self._account2 + result = self._storage[("test@localhost",)] + self.assertEquals(type(result), list) + self.assertEquals(len(result), 2) + self.assertEquals(result[0], self._account1) + self.assertEquals(result[1], self._account2) + + def test_get_filtered2(self): + self._storage[("test@localhost", "account1")] = self._account1 + self._storage[("test@localhost", "account2")] = self._account2 + result = self._storage[("account1",)] + self.assertEquals(type(result), list) + self.assertEquals(len(result), 1) + self.assertEquals(result[0], self._account1) + + def test_keys(self): + self._storage[("test@localhost", "account1")] = self._account1 + self._storage[("test@localhost", "account2")] = self._account2 + result = self._storage.keys() + self.assertEquals(type(result), list) + self.assertEquals(len(result), 2) + self.assertEquals(type(result[0]), tuple) + self.assertEquals(len(result[0]), 2) + self.assertEquals(result[0][0], "test@localhost") + self.assertEquals(result[0][1], "account1") + self.assertEquals(type(result[1]), tuple) + self.assertEquals(len(result[1]), 2) + self.assertEquals(result[1][0], "test@localhost") + self.assertEquals(result[1][1], "account2") + + def test_keys_filtered(self): + self._storage[("test@localhost", "account1")] = self._account1 + self._storage[("test@localhost", "account2")] = self._account2 + result = self._storage.keys(()) + self.assertEquals(type(result), list) + self.assertEquals(len(result), 1) + self.assertEquals(result[0], "test@localhost") + + def test_keys_filtered2(self): + self._storage[("test@localhost", "account1")] = self._account1 + self._storage[("test@localhost", "account2")] = self._account2 + result = self._storage.keys(("test@localhost",)) + self.assertEquals(type(result), list) + self.assertEquals(len(result), 2) + self.assertEquals(result[0], "account2") + self.assertEquals(result[1], "account1") diff --git a/tests/test_x.py b/tests/test_x.py new file mode 100644 index 0000000..71bf685 --- /dev/null +++ b/tests/test_x.py @@ -0,0 +1,72 @@ +## +## test_x.py +## Login : +## Started on Fri May 20 10:46:58 2005 adro8400 +## $Id: test_x.py,v 1.1 2005/07/11 20:39:31 dax Exp $ +## +## Copyright (C) 2005 adro8400 +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import unittest +from jabber.x import * + +class X_TestCase(unittest.TestCase): + def setUp(self): + self.mail_component = MailComponent() + + def test_get_form(self): + self.reg_form.add_field(type = "text-single", \ + label = "Connection name", \ + var = "name") + + self.reg_form.add_field(type = "text-single", \ + label = "Login", \ + var = "login") + + self.reg_form.add_field(type = "text-private", \ + label = "password", \ + var = "password") + + self.reg_form.add_field(type = "text-single", \ + label = "Host", \ + var = "host") + + self.reg_form.add_field(type = "text-single", \ + label = "Port", \ + var = "port") + + field = self.reg_form.add_field(type = "list-single", \ + label = "Mailbox type", \ + var = "type") + field.add_option(label = "POP3", \ + value = "pop3") + field.add_option(label = "POP3S", \ + value = "pop3s") + field.add_option(label = "IMAP", \ + value = "imap") + field.add_option(label = "IMAPS", \ + value = "imaps") + + self.reg_form.add_field(type = "text-single", \ + label = "Mailbox (IMAP)", \ + var = "mailbox", \ + value = "INBOX") + + self.reg_form.add_field(type = "boolean", \ + label = "Retrieve mail", \ + var = "retrieve") + pass + diff --git a/tests/tmp.py b/tests/tmp.py new file mode 100644 index 0000000..709ab09 --- /dev/null +++ b/tests/tmp.py @@ -0,0 +1,28 @@ +import dummy_server + +server = dummy_server.DummyServer(("localhost", 1143)) +server.responses = ["* OK [CAPABILITY IMAP4 LOGIN-REFERRALS " + \ + "AUTH=PLAIN]\r\n", \ + lambda data: "* CAPABILITY IMAP4 " + \ + "LOGIN-REFERRALS AUTH=PLAIN\r\n" + \ + data.split()[0] + \ + " OK CAPABILITY completed\r\n", \ + lambda data: data.split()[0] + \ + " OK LOGIN completed\r\n", \ + lambda data: "* 42 EXISTS\r\n* 1 RECENT\r\n* OK" +\ + " [UNSEEN 9]\r\n* FLAGS (\Deleted \Seen\*)\r\n*" +\ + " OK [PERMANENTFLAGS (\Deleted \Seen\*)\r\n" + \ + data.split()[0] + \ + " OK [READ-WRITE] SELECT completed\r\n", \ + lambda data: "* 1 FETCH ((RFC822) {12}\r\nbody text\r\n)\r\n" + \ + data.split()[0] + " OK FETCH completed\r\n", \ + lambda data: "* 1 FETCH (FLAGS (\UNSEEN))\r\n" + \ + data.split()[0] + " OK STORE completed\r\n"] +server.queries = ["CAPABILITY", \ + "LOGIN login \"pass\"", \ + "SELECT INBOX", \ + "FETCH 1 (RFC822)", \ + "STORE 1 FLAGS (UNSEEN)", \ + "LOGOUT"] +server.serve() +#server.verify_queries() diff --git a/tests/tmp1 b/tests/tmp1 new file mode 100644 index 0000000..d40b8a4 --- /dev/null +++ b/tests/tmp1 @@ -0,0 +1,121 @@ + + + + +Jabber Mail connection registration + +Enter anything below + + + + + + + + + + + + + +INBOX + + + +2 + + + + + + +2 + + + + + + +1 + + + + + + +1 + + + + + + +0 + + + + + + +5 + + + + diff --git a/tests/tmp2 b/tests/tmp2 new file mode 100644 index 0000000..d40b8a4 --- /dev/null +++ b/tests/tmp2 @@ -0,0 +1,121 @@ + + + + +Jabber Mail connection registration + +Enter anything below + + + + + + + + + + + + + +INBOX + + + +2 + + + + + + +2 + + + + + + +1 + + + + + + +1 + + + + + + +0 + + + + + + +5 + + + + diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..a24f93f --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,100 @@ +## +## utils.py +## Login : +## Started on Mon Oct 24 21:44:43 2005 dax +## $Id$ +## +## Copyright (C) 2005 dax +## 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +## + +import xml.dom.minidom + +#document = "" +# document = """\ +# +# Demo slideshow +# Slide title +# This is a demo +# Of a program for processing slides +# + +# Another demo slide +# It is important +# To have more than +# one slide +# +# +# """ + +# document1 = """\ +# +# Demo slideshow +# Slide title +# This is a demo +# Of a program for processing slides +# + +# Another demo slide +# It is important1 +# To have more than +# one slide +# +# +# """ + +#dom = xml.dom.minidom.parseString(document) +#dom1 = xml.dom.minidom.parseString(document) + +# def getText(nodelist): +# rc = "" +# for node in nodelist: +# if node.nodeType == node.TEXT_NODE: +# rc = rc + node.data +# return rc + +def xmldiff(node1, node2): + if node1.nodeType == node1.TEXT_NODE: + if not node2.nodeType == node2.TEXT_NODE \ + or node1.data != node2.data: + return False + elif node1.nodeType == node1.DOCUMENT_NODE: + if not node2.nodeType == node2.DOCUMENT_NODE: + return False + elif node1.tagName != node2.tagName: + return False + else: + for attr in node1._get_attributes().keys(): + if not node2.hasAttribute(attr) \ + or node1.getAttribute(attr) != node2.getAttribute(attr): + return False + for i in range(len(node1.childNodes)): + if not xmldiff(node1.childNodes[i], node2.childNodes[i]): + return False + return True + +#print xmldiff(dom, dom1) + +# def nodediff(node1, node2): +# if not node1.name == node2.name: +# return False +# for properties in node1.properties: +# if node2.hasAttribute(attr): + +# def xmldiff(xpath, node1, node2): +# if not nodediff(node1, node2): +# return False +# for child in node1.children: +