Index: /tea4cups/tags/3.12/TODO
===================================================================
--- /tea4cups/tags/3.12/TODO (revision 674)
+++ /tea4cups/tags/3.12/TODO (revision 674)
@@ -0,0 +1,29 @@
+# $Id$
+
+Tea4CUPS : Tee for CUPS
+
+(c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
+(c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+============================================================
+
+TODO, in no particular order :
+        
+        - Give some complex examples.
+        
+============================================================
+
+Please e-mail bugs to: alet@librelogiciel.com (Jerome Alet)
Index: /tea4cups/tags/3.12/COPYING
===================================================================
--- /tea4cups/tags/3.12/COPYING (revision 644)
+++ /tea4cups/tags/3.12/COPYING (revision 644)
@@ -0,0 +1,340 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                          51 Franklin Street, Fifth Floor
+                          Boston, MA 02110-1301, USA.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU 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
+
+        Appendix: How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
Index: /tea4cups/tags/3.12/tea4cups
===================================================================
--- /tea4cups/tags/3.12/tea4cups (revision 684)
+++ /tea4cups/tags/3.12/tea4cups (revision 684)
@@ -0,0 +1,1452 @@
+#! /usr/bin/env python
+# -*- coding: ISO-8859-15 -*-
+
+# Tea4CUPS : Tee for CUPS
+#
+# (c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
+# (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# $Id$
+#
+#
+
+"""Tea4CUPS is the Swiss Army's knife of the CUPS Administrator.
+
+
+Licensing terms :
+
+  (c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
+  (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+  
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+  
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+Setup :
+
+  Copy the different files where they belong :
+  
+    $ cp tea4cups /usr/lib/cups/backend/
+    $ chown root.root /usr/lib/cups/backend/tea4cups
+    $ chmod 700 /usr/lib/cups/backend/tea4cups
+    $ cp tea4cups.conf /etc/cupsd/
+  
+  Now edit the configuration file to suit your needs :
+  
+    $ vi /etc/cupsd/tea4cups.conf
+    
+    NB : you can use emacs as well :-)
+    
+  Finally restart CUPS :
+  
+    $ /etc/init.d/cupsys restart
+  
+  You can now create "Tea4CUPS Managed" print queues from
+  CUPS' web interface, or using lpadmin.
+  
+Send bug reports to : alet@librelogiciel.com  
+"""        
+
+import sys
+import os
+import time
+import pwd
+import errno
+import md5
+import cStringIO
+import shlex
+import tempfile
+import ConfigParser
+import signal
+import socket
+import urllib2
+from struct import pack, unpack
+
+__version__ = "3.12_unofficial"
+
+class TeeError(Exception):
+    """Base exception for Tea4CUPS related stuff."""
+    def __init__(self, message = ""):
+        self.message = message
+        Exception.__init__(self, message)
+    def __repr__(self):
+        return self.message
+    __str__ = __repr__
+
+class ConfigError(TeeError) :
+    """Configuration related exceptions."""
+    pass
+
+class IPPError(TeeError) :
+    """IPP related exceptions."""
+    pass
+    
+IPP_VERSION = "1.1"     # default version number
+
+IPP_PORT = 631
+
+IPP_MAX_NAME = 256
+IPP_MAX_VALUES = 8
+
+IPP_TAG_ZERO = 0x00
+IPP_TAG_OPERATION = 0x01
+IPP_TAG_JOB = 0x02
+IPP_TAG_END = 0x03
+IPP_TAG_PRINTER = 0x04
+IPP_TAG_UNSUPPORTED_GROUP = 0x05
+IPP_TAG_SUBSCRIPTION = 0x06
+IPP_TAG_EVENT_NOTIFICATION = 0x07
+IPP_TAG_UNSUPPORTED_VALUE = 0x10
+IPP_TAG_DEFAULT = 0x11
+IPP_TAG_UNKNOWN = 0x12
+IPP_TAG_NOVALUE = 0x13
+IPP_TAG_NOTSETTABLE = 0x15
+IPP_TAG_DELETEATTR = 0x16
+IPP_TAG_ADMINDEFINE = 0x17
+IPP_TAG_INTEGER = 0x21
+IPP_TAG_BOOLEAN = 0x22
+IPP_TAG_ENUM = 0x23
+IPP_TAG_STRING = 0x30
+IPP_TAG_DATE = 0x31
+IPP_TAG_RESOLUTION = 0x32
+IPP_TAG_RANGE = 0x33
+IPP_TAG_BEGIN_COLLECTION = 0x34
+IPP_TAG_TEXTLANG = 0x35
+IPP_TAG_NAMELANG = 0x36
+IPP_TAG_END_COLLECTION = 0x37
+IPP_TAG_TEXT = 0x41
+IPP_TAG_NAME = 0x42
+IPP_TAG_KEYWORD = 0x44
+IPP_TAG_URI = 0x45
+IPP_TAG_URISCHEME = 0x46
+IPP_TAG_CHARSET = 0x47
+IPP_TAG_LANGUAGE = 0x48
+IPP_TAG_MIMETYPE = 0x49
+IPP_TAG_MEMBERNAME = 0x4a
+IPP_TAG_MASK = 0x7fffffff
+IPP_TAG_COPY = -0x7fffffff-1
+
+IPP_RES_PER_INCH = 3
+IPP_RES_PER_CM = 4
+
+IPP_FINISHINGS_NONE = 3
+IPP_FINISHINGS_STAPLE = 4
+IPP_FINISHINGS_PUNCH = 5
+IPP_FINISHINGS_COVER = 6
+IPP_FINISHINGS_BIND = 7
+IPP_FINISHINGS_SADDLE_STITCH = 8
+IPP_FINISHINGS_EDGE_STITCH = 9
+IPP_FINISHINGS_FOLD = 10
+IPP_FINISHINGS_TRIM = 11
+IPP_FINISHINGS_BALE = 12
+IPP_FINISHINGS_BOOKLET_MAKER = 13
+IPP_FINISHINGS_JOB_OFFSET = 14
+IPP_FINISHINGS_STAPLE_TOP_LEFT = 20
+IPP_FINISHINGS_STAPLE_BOTTOM_LEFT = 21
+IPP_FINISHINGS_STAPLE_TOP_RIGHT = 22
+IPP_FINISHINGS_STAPLE_BOTTOM_RIGHT = 23
+IPP_FINISHINGS_EDGE_STITCH_LEFT = 24
+IPP_FINISHINGS_EDGE_STITCH_TOP = 25
+IPP_FINISHINGS_EDGE_STITCH_RIGHT = 26
+IPP_FINISHINGS_EDGE_STITCH_BOTTOM = 27
+IPP_FINISHINGS_STAPLE_DUAL_LEFT = 28
+IPP_FINISHINGS_STAPLE_DUAL_TOP = 29
+IPP_FINISHINGS_STAPLE_DUAL_RIGHT = 30
+IPP_FINISHINGS_STAPLE_DUAL_BOTTOM = 31
+IPP_FINISHINGS_BIND_LEFT = 50
+IPP_FINISHINGS_BIND_TOP = 51
+IPP_FINISHINGS_BIND_RIGHT = 52
+IPP_FINISHINGS_BIND_BOTTO = 53
+
+IPP_PORTRAIT = 3
+IPP_LANDSCAPE = 4
+IPP_REVERSE_LANDSCAPE = 5
+IPP_REVERSE_PORTRAIT = 6
+
+IPP_QUALITY_DRAFT = 3
+IPP_QUALITY_NORMAL = 4
+IPP_QUALITY_HIGH = 5
+
+IPP_JOB_PENDING = 3
+IPP_JOB_HELD = 4
+IPP_JOB_PROCESSING = 5
+IPP_JOB_STOPPED = 6
+IPP_JOB_CANCELLED = 7
+IPP_JOB_ABORTED = 8
+IPP_JOB_COMPLETE = 9
+
+IPP_PRINTER_IDLE = 3
+IPP_PRINTER_PROCESSING = 4
+IPP_PRINTER_STOPPED = 5
+
+IPP_ERROR = -1
+IPP_IDLE = 0
+IPP_HEADER = 1
+IPP_ATTRIBUTE = 2
+IPP_DATA = 3
+
+IPP_PRINT_JOB = 0x0002
+IPP_PRINT_URI = 0x0003
+IPP_VALIDATE_JOB = 0x0004
+IPP_CREATE_JOB = 0x0005
+IPP_SEND_DOCUMENT = 0x0006
+IPP_SEND_URI = 0x0007
+IPP_CANCEL_JOB = 0x0008
+IPP_GET_JOB_ATTRIBUTES = 0x0009
+IPP_GET_JOBS = 0x000a
+IPP_GET_PRINTER_ATTRIBUTES = 0x000b
+IPP_HOLD_JOB = 0x000c
+IPP_RELEASE_JOB = 0x000d
+IPP_RESTART_JOB = 0x000e
+IPP_PAUSE_PRINTER = 0x0010
+IPP_RESUME_PRINTER = 0x0011
+IPP_PURGE_JOBS = 0x0012
+IPP_SET_PRINTER_ATTRIBUTES = 0x0013
+IPP_SET_JOB_ATTRIBUTES = 0x0014
+IPP_GET_PRINTER_SUPPORTED_VALUES = 0x0015
+IPP_CREATE_PRINTER_SUBSCRIPTION = 0x0016
+IPP_CREATE_JOB_SUBSCRIPTION = 0x0017
+IPP_GET_SUBSCRIPTION_ATTRIBUTES = 0x0018
+IPP_GET_SUBSCRIPTIONS = 0x0019
+IPP_RENEW_SUBSCRIPTION = 0x001a
+IPP_CANCEL_SUBSCRIPTION = 0x001b
+IPP_GET_NOTIFICATIONS = 0x001c
+IPP_SEND_NOTIFICATIONS = 0x001d
+IPP_GET_PRINT_SUPPORT_FILES = 0x0021
+IPP_ENABLE_PRINTER = 0x0022
+IPP_DISABLE_PRINTER = 0x0023
+IPP_PAUSE_PRINTER_AFTER_CURRENT_JOB = 0x0024
+IPP_HOLD_NEW_JOBS = 0x0025
+IPP_RELEASE_HELD_NEW_JOBS = 0x0026
+IPP_DEACTIVATE_PRINTER = 0x0027
+IPP_ACTIVATE_PRINTER = 0x0028
+IPP_RESTART_PRINTER = 0x0029
+IPP_SHUTDOWN_PRINTER = 0x002a
+IPP_STARTUP_PRINTER = 0x002b
+IPP_REPROCESS_JOB = 0x002c
+IPP_CANCEL_CURRENT_JOB = 0x002d
+IPP_SUSPEND_CURRENT_JOB = 0x002e
+IPP_RESUME_JOB = 0x002f
+IPP_PROMOTE_JOB = 0x0030
+IPP_SCHEDULE_JOB_AFTER = 0x0031
+IPP_PRIVATE = 0x4000
+CUPS_GET_DEFAULT = 0x4001
+CUPS_GET_PRINTERS = 0x4002
+CUPS_ADD_PRINTER = 0x4003
+CUPS_DELETE_PRINTER = 0x4004
+CUPS_GET_CLASSES = 0x4005
+CUPS_ADD_CLASS = 0x4006
+CUPS_DELETE_CLASS = 0x4007
+CUPS_ACCEPT_JOBS = 0x4008
+CUPS_REJECT_JOBS = 0x4009
+CUPS_SET_DEFAULT = 0x400a
+CUPS_GET_DEVICES = 0x400b
+CUPS_GET_PPDS = 0x400c
+CUPS_MOVE_JOB = 0x400d
+CUPS_AUTHENTICATE_JOB = 0x400e
+
+IPP_OK = 0x0000
+IPP_OK_SUBST = 0x0001
+IPP_OK_CONFLICT = 0x0002
+IPP_OK_IGNORED_SUBSCRIPTIONS = 0x0003
+IPP_OK_IGNORED_NOTIFICATIONS = 0x0004
+IPP_OK_TOO_MANY_EVENTS = 0x0005
+IPP_OK_BUT_CANCEL_SUBSCRIPTION = 0x0006
+IPP_REDIRECTION_OTHER_SITE = 0x0300
+IPP_BAD_REQUEST = 0x0400
+IPP_FORBIDDEN = 0x0401
+IPP_NOT_AUTHENTICATED = 0x0402
+IPP_NOT_AUTHORIZED = 0x0403
+IPP_NOT_POSSIBLE = 0x0404
+IPP_TIMEOUT = 0x0405
+IPP_NOT_FOUND = 0x0406
+IPP_GONE = 0x0407
+IPP_REQUEST_ENTITY = 0x0408
+IPP_REQUEST_VALUE = 0x0409
+IPP_DOCUMENT_FORMAT = 0x040a
+IPP_ATTRIBUTES = 0x040b
+IPP_URI_SCHEME = 0x040c
+IPP_CHARSET = 0x040d
+IPP_CONFLICT = 0x040e
+IPP_COMPRESSION_NOT_SUPPORTED = 0x040f
+IPP_COMPRESSION_ERROR = 0x0410
+IPP_DOCUMENT_FORMAT_ERROR = 0x0411
+IPP_DOCUMENT_ACCESS_ERROR = 0x0412
+IPP_ATTRIBUTES_NOT_SETTABLE = 0x0413
+IPP_IGNORED_ALL_SUBSCRIPTIONS = 0x0414
+IPP_TOO_MANY_SUBSCRIPTIONS = 0x0415
+IPP_IGNORED_ALL_NOTIFICATIONS = 0x0416
+IPP_PRINT_SUPPORT_FILE_NOT_FOUND = 0x0417
+
+IPP_INTERNAL_ERROR = 0x0500
+IPP_OPERATION_NOT_SUPPORTED = 0x0501
+IPP_SERVICE_UNAVAILABLE = 0x0502
+IPP_VERSION_NOT_SUPPORTED = 0x0503
+IPP_DEVICE_ERROR = 0x0504
+IPP_TEMPORARY_ERROR = 0x0505
+IPP_NOT_ACCEPTING = 0x0506
+IPP_PRINTER_BUSY = 0x0507
+IPP_ERROR_JOB_CANCELLED = 0x0508
+IPP_MULTIPLE_JOBS_NOT_SUPPORTED = 0x0509
+IPP_PRINTER_IS_DEACTIVATED = 0x50a
+  
+CUPS_PRINTER_LOCAL = 0x0000
+CUPS_PRINTER_CLASS = 0x0001
+CUPS_PRINTER_REMOTE = 0x0002
+CUPS_PRINTER_BW = 0x0004
+CUPS_PRINTER_COLOR = 0x0008
+CUPS_PRINTER_DUPLEX = 0x0010
+CUPS_PRINTER_STAPLE = 0x0020
+CUPS_PRINTER_COPIES = 0x0040
+CUPS_PRINTER_COLLATE = 0x0080
+CUPS_PRINTER_PUNCH = 0x0100
+CUPS_PRINTER_COVER = 0x0200
+CUPS_PRINTER_BIND = 0x0400
+CUPS_PRINTER_SORT = 0x0800
+CUPS_PRINTER_SMALL = 0x1000
+CUPS_PRINTER_MEDIUM = 0x2000
+CUPS_PRINTER_LARGE = 0x4000
+CUPS_PRINTER_VARIABLE = 0x8000
+CUPS_PRINTER_IMPLICIT = 0x1000
+CUPS_PRINTER_DEFAULT = 0x2000
+CUPS_PRINTER_FAX = 0x4000
+CUPS_PRINTER_REJECTING = 0x8000
+CUPS_PRINTER_DELETE = 0x1000
+CUPS_PRINTER_NOT_SHARED = 0x2000
+CUPS_PRINTER_AUTHENTICATED = 0x4000
+CUPS_PRINTER_COMMANDS = 0x8000
+CUPS_PRINTER_OPTIONS = 0xe6ff
+  
+class FakeAttribute :
+    """Fakes an IPPRequest attribute to simplify usage syntax."""
+    def __init__(self, request, name) :
+        """Initializes the fake attribute."""
+        self.request = request
+        self.name = name
+        
+    def __setitem__(self, key, value) :
+        """Appends the value to the real attribute."""
+        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
+        for i in range(len(attributeslist)) :
+            attribute = attributeslist[i]
+            for j in range(len(attribute)) :
+                (attrname, attrvalue) = attribute[j]
+                if attrname == key :
+                    attribute[j][1].append(value)
+                    return
+            attribute.append((key, [value]))        
+            
+    def __getitem__(self, key) :
+        """Returns an attribute's value."""
+        answer = []
+        attributeslist = getattr(self.request, "_%s_attributes" % self.name)
+        for i in range(len(attributeslist)) :
+            attribute = attributeslist[i]
+            for j in range(len(attribute)) :
+                (attrname, attrvalue) = attribute[j]
+                if attrname == key :
+                    answer.extend(attrvalue)
+        if answer :
+            return answer
+        raise KeyError, key            
+    
+class IPPRequest :
+    """A class for IPP requests."""
+    attributes_types = ("operation", "job", "printer", "unsupported", \
+                                     "subscription", "event_notification")
+    def __init__(self, data="", version=IPP_VERSION, 
+                                operation_id=None, \
+                                request_id=None, \
+                                debug=False) :
+        """Initializes an IPP Message object.
+        
+           Parameters :
+           
+             data : the complete IPP Message's content.
+             debug : a boolean value to output debug info on stderr.
+        """
+        self.debug = debug
+        self._data = data
+        self.parsed = False
+        
+        # Initializes message
+        self.setVersion(version)                
+        self.setOperationId(operation_id)
+        self.setRequestId(request_id)
+        self.data = ""
+        
+        for attrtype in self.attributes_types :
+            setattr(self, "_%s_attributes" % attrtype, [[]])
+        
+        # Initialize tags    
+        self.tags = [ None ] * 256 # by default all tags reserved
+        
+        # Delimiter tags
+        self.tags[0x01] = "operation-attributes-tag"
+        self.tags[0x02] = "job-attributes-tag"
+        self.tags[0x03] = "end-of-attributes-tag"
+        self.tags[0x04] = "printer-attributes-tag"
+        self.tags[0x05] = "unsupported-attributes-tag"
+        self.tags[0x06] = "subscription-attributes-tag"
+        self.tags[0x07] = "event_notification-attributes-tag"
+        
+        # out of band values
+        self.tags[0x10] = "unsupported"
+        self.tags[0x11] = "reserved-for-future-default"
+        self.tags[0x12] = "unknown"
+        self.tags[0x13] = "no-value"
+        self.tags[0x15] = "not-settable"
+        self.tags[0x16] = "delete-attribute"
+        self.tags[0x17] = "admin-define"
+  
+        # integer values
+        self.tags[0x20] = "generic-integer"
+        self.tags[0x21] = "integer"
+        self.tags[0x22] = "boolean"
+        self.tags[0x23] = "enum"
+        
+        # octetString
+        self.tags[0x30] = "octetString-with-an-unspecified-format"
+        self.tags[0x31] = "dateTime"
+        self.tags[0x32] = "resolution"
+        self.tags[0x33] = "rangeOfInteger"
+        self.tags[0x34] = "begCollection" # TODO : find sample files for testing
+        self.tags[0x35] = "textWithLanguage"
+        self.tags[0x36] = "nameWithLanguage"
+        self.tags[0x37] = "endCollection"
+        
+        # character strings
+        self.tags[0x40] = "generic-character-string"
+        self.tags[0x41] = "textWithoutLanguage"
+        self.tags[0x42] = "nameWithoutLanguage"
+        self.tags[0x44] = "keyword"
+        self.tags[0x45] = "uri"
+        self.tags[0x46] = "uriScheme"
+        self.tags[0x47] = "charset"
+        self.tags[0x48] = "naturalLanguage"
+        self.tags[0x49] = "mimeMediaType"
+        self.tags[0x4a] = "memberAttrName"
+        
+        # Reverse mapping to generate IPP messages
+        self.tagvalues = {}
+        for i in range(len(self.tags)) :
+            value = self.tags[i]
+            if value is not None :
+                self.tagvalues[value] = i
+                                     
+    def __getattr__(self, name) :                                 
+        """Fakes attribute access."""
+        if name in self.attributes_types :
+            return FakeAttribute(self, name)
+        else :
+            raise AttributeError, name
+            
+    def __str__(self) :        
+        """Returns the parsed IPP message in a readable form."""
+        if not self.parsed :
+            return ""
+        mybuffer = []
+        mybuffer.append("IPP version : %s.%s" % self.version)
+        mybuffer.append("IPP operation Id : 0x%04x" % self.operation_id)
+        mybuffer.append("IPP request Id : 0x%08x" % self.request_id)
+        for attrtype in self.attributes_types :
+            for attribute in getattr(self, "_%s_attributes" % attrtype) :
+                if attribute :
+                    mybuffer.append("%s attributes :" % attrtype.title())
+                for (name, value) in attribute :
+                    mybuffer.append("  %s : %s" % (name, value))
+        if self.data :            
+            mybuffer.append("IPP datas : %s" % repr(self.data))
+        return "\n".join(mybuffer)
+        
+    def logDebug(self, msg) :    
+        """Prints a debug message."""
+        if self.debug :
+            sys.stderr.write("%s\n" % msg)
+            sys.stderr.flush()
+            
+    def setVersion(self, version) :
+        """Sets the request's operation id."""
+        if version is not None :
+            try :
+                self.version = [int(p) for p in version.split(".")]
+            except AttributeError :
+                if len(version) == 2 : # 2-tuple
+                    self.version = version
+                else :    
+                    try :
+                        self.version = [int(p) for p in str(float(version)).split(".")]
+                    except :
+                        self.version = [int(p) for p in IPP_VERSION.split(".")]
+        
+    def setOperationId(self, opid) :        
+        """Sets the request's operation id."""
+        self.operation_id = opid
+        
+    def setRequestId(self, reqid) :        
+        """Sets the request's request id."""
+        self.request_id = reqid
+        
+    def dump(self) :    
+        """Generates an IPP Message.
+        
+           Returns the message as a string of text.
+        """    
+        mybuffer = []
+        if None not in (self.version, self.operation_id) :
+            mybuffer.append(chr(self.version[0]) + chr(self.version[1]))
+            mybuffer.append(pack(">H", self.operation_id))
+            mybuffer.append(pack(">I", self.request_id or 1))
+            for attrtype in self.attributes_types :
+                for attribute in getattr(self, "_%s_attributes" % attrtype) :
+                    if attribute :
+                        mybuffer.append(chr(self.tagvalues["%s-attributes-tag" % attrtype]))
+                    for (attrname, value) in attribute :
+                        nameprinted = 0
+                        for (vtype, val) in value :
+                            mybuffer.append(chr(self.tagvalues[vtype]))
+                            if not nameprinted :
+                                mybuffer.append(pack(">H", len(attrname)))
+                                mybuffer.append(attrname)
+                                nameprinted = 1
+                            else :     
+                                mybuffer.append(pack(">H", 0))
+                            if vtype in ("integer", "enum") :
+                                mybuffer.append(pack(">H", 4))
+                                mybuffer.append(pack(">I", val))
+                            elif vtype == "boolean" :
+                                mybuffer.append(pack(">H", 1))
+                                mybuffer.append(chr(val))
+                            else :    
+                                mybuffer.append(pack(">H", len(val)))
+                                mybuffer.append(val)
+            mybuffer.append(chr(self.tagvalues["end-of-attributes-tag"]))
+        mybuffer.append(self.data)    
+        return "".join(mybuffer)
+            
+    def parse(self) :
+        """Parses an IPP Request.
+        
+           NB : Only a subset of RFC2910 is implemented.
+        """
+        self._curname = None
+        self._curattributes = None
+        
+        self.setVersion((ord(self._data[0]), ord(self._data[1])))
+        self.setOperationId(unpack(">H", self._data[2:4])[0])
+        self.setRequestId(unpack(">I", self._data[4:8])[0])
+        self.position = 8
+        endofattributes = self.tagvalues["end-of-attributes-tag"]
+        maxdelimiter = self.tagvalues["event_notification-attributes-tag"]
+        nulloffset = lambda : 0
+        try :
+            tag = ord(self._data[self.position])
+            while tag != endofattributes :
+                self.position += 1
+                name = self.tags[tag]
+                if name is not None :
+                    func = getattr(self, name.replace("-", "_"), nulloffset)
+                    self.position += func()
+                    if ord(self._data[self.position]) > maxdelimiter :
+                        self.position -= 1
+                        continue
+                oldtag = tag        
+                tag = ord(self._data[self.position])
+                if tag == oldtag :
+                    self._curattributes.append([])
+        except IndexError :
+            raise IPPError, "Unexpected end of IPP message."
+            
+        self.data = self._data[self.position+1:]            
+        self.parsed = True
+        
+    def parseTag(self) :    
+        """Extracts information from an IPP tag."""
+        pos = self.position
+        tagtype = self.tags[ord(self._data[pos])]
+        pos += 1
+        posend = pos2 = pos + 2
+        namelength = unpack(">H", self._data[pos:pos2])[0]
+        if not namelength :
+            name = self._curname
+        else :    
+            posend += namelength
+            self._curname = name = self._data[pos2:posend]
+        pos2 = posend + 2
+        valuelength = unpack(">H", self._data[posend:pos2])[0]
+        posend = pos2 + valuelength
+        value = self._data[pos2:posend]
+        if tagtype in ("integer", "enum") :
+            value = unpack(">I", value)[0]
+        elif tagtype == "boolean" :    
+            value = ord(value)
+        try :    
+            (oldname, oldval) = self._curattributes[-1][-1]
+            if oldname == name :
+                oldval.append((tagtype, value))
+            else :    
+                raise IndexError
+        except IndexError :    
+            self._curattributes[-1].append((name, [(tagtype, value)]))
+        self.logDebug("%s(%s) : %s" % (name, tagtype, value))
+        return posend - self.position
+        
+    def operation_attributes_tag(self) : 
+        """Indicates that the parser enters into an operation-attributes-tag group."""
+        self._curattributes = self._operation_attributes
+        return self.parseTag()
+        
+    def job_attributes_tag(self) : 
+        """Indicates that the parser enters into a job-attributes-tag group."""
+        self._curattributes = self._job_attributes
+        return self.parseTag()
+        
+    def printer_attributes_tag(self) : 
+        """Indicates that the parser enters into a printer-attributes-tag group."""
+        self._curattributes = self._printer_attributes
+        return self.parseTag()
+        
+    def unsupported_attributes_tag(self) : 
+        """Indicates that the parser enters into an unsupported-attributes-tag group."""
+        self._curattributes = self._unsupported_attributes
+        return self.parseTag()
+        
+    def subscription_attributes_tag(self) : 
+        """Indicates that the parser enters into a subscription-attributes-tag group."""
+        self._curattributes = self._subscription_attributes
+        return self.parseTag()
+        
+    def event_notification_attributes_tag(self) : 
+        """Indicates that the parser enters into an event-notification-attributes-tag group."""
+        self._curattributes = self._event_notification_attributes
+        return self.parseTag()
+        
+            
+class CUPS :
+    """A class for a CUPS instance."""
+    def __init__(self, url=None, username=None, password=None, charset="utf-8", language="en-us", debug=False) :
+        """Initializes the CUPS instance."""
+        if url is not None :
+            self.url = url.replace("ipp://", "http://")
+            if self.url.endswith("/") :
+                self.url = self.url[:-1]
+        else :        
+            self.url = self.getDefaultURL()
+        self.username = username
+        self.password = password
+        self.charset = charset
+        self.language = language
+        self.debug = debug
+        self.lastError = None
+        self.lastErrorMessage = None
+        self.requestId = None
+        
+    def getDefaultURL(self) :    
+        """Builds a default URL."""
+        # TODO : encryption methods.
+        server = os.environ.get("CUPS_SERVER") or "localhost"
+        port = os.environ.get("IPP_PORT") or 631
+        if server.startswith("/") :
+            # it seems it's a unix domain socket.
+            # we can't handle this right now, so we use the default instead.
+            return "http://localhost:%s" % port
+        else :    
+            return "http://%s:%s" % (server, port)
+            
+    def identifierToURI(self, service, ident) :
+        """Transforms an identifier into a particular URI depending on requested service."""
+        return "%s/%s/%s" % (self.url.replace("http://", "ipp://"),
+                             service,
+                             ident)
+        
+    def nextRequestId(self) :        
+        """Increments the current request id and returns the new value."""
+        try :
+            self.requestId += 1
+        except TypeError :    
+            self.requestId = 1
+        return self.requestId
+            
+    def newRequest(self, operationid=None) :
+        """Generates a new empty request."""
+        if operationid is not None :
+            req = IPPRequest(operation_id=operationid, \
+                             request_id=self.nextRequestId(), \
+                             debug=self.debug)
+            req.operation["attributes-charset"] = ("charset", self.charset)
+            req.operation["attributes-natural-language"] = ("naturalLanguage", self.language)
+            return req
+    
+    def doRequest(self, req, url=None) :
+        """Sends a request to the CUPS server.
+           returns a new IPPRequest object, containing the parsed answer.
+        """   
+        connexion = urllib2.Request(url=url or self.url, \
+                             data=req.dump())
+        connexion.add_header("Content-Type", "application/ipp")
+        if self.username :
+            pwmanager = urllib2.HTTPPasswordMgrWithDefaultRealm()
+            pwmanager.add_password(None, \
+                                   "%s%s" % (connexion.get_host(), connexion.get_selector()), \
+                                   self.username, \
+                                   self.password or "")
+            authhandler = urllib2.HTTPBasicAuthHandler(pwmanager)                       
+            opener = urllib2.build_opener(authhandler)
+            urllib2.install_opener(opener)
+        self.lastError = None    
+        self.lastErrorMessage = None
+        try :    
+            response = urllib2.urlopen(connexion)
+        except (urllib2.URLError, urllib2.HTTPError, socket.error), error :    
+            self.lastError = error
+            self.lastErrorMessage = str(error)
+            return None
+        else :    
+            datas = response.read()
+            ippresponse = IPPRequest(datas)
+            ippresponse.parse()
+            return ippresponse
+    
+    def getPPD(self, queuename) :    
+        """Retrieves the PPD for a particular queuename."""
+        req = self.newRequest(IPP_GET_PRINTER_ATTRIBUTES)
+        req.operation["printer-uri"] = ("uri", self.identifierToURI("printers", queuename))
+        for attrib in ("printer-uri-supported", "printer-type", "member-uris") :
+            req.operation["requested-attributes"] = ("nameWithoutLanguage", attrib)
+        return self.doRequest(req)  # TODO : get the PPD from the actual print server
+        
+    def getDefault(self) :
+        """Retrieves CUPS' default printer."""
+        return self.doRequest(self.newRequest(CUPS_GET_DEFAULT))
+    
+    def getJobAttributes(self, jobid) :    
+        """Retrieves a print job's attributes."""
+        req = self.newRequest(IPP_GET_JOB_ATTRIBUTES)
+        req.operation["job-uri"] = ("uri", self.identifierToURI("jobs", jobid))
+        return self.doRequest(req)
+        
+    def getPrinters(self) :    
+        """Returns the list of print queues names."""
+        req = self.newRequest(CUPS_GET_PRINTERS)
+        req.operation["requested-attributes"] = ("keyword", "printer-name")
+        req.operation["printer-type"] = ("enum", 0)
+        req.operation["printer-type-mask"] = ("enum", CUPS_PRINTER_CLASS)
+        return [printer[1] for printer in self.doRequest(req).printer["printer-name"]]
+        
+    def getDevices(self) :    
+        """Returns a list of devices as (deviceclass, deviceinfo, devicemakeandmodel, deviceuri) tuples."""
+        answer = self.doRequest(self.newRequest(CUPS_GET_DEVICES))
+        return zip([d[1] for d in answer.printer["device-class"]], \
+                   [d[1] for d in answer.printer["device-info"]], \
+                   [d[1] for d in answer.printer["device-make-and-model"]], \
+                   [d[1] for d in answer.printer["device-uri"]])
+                   
+    def getPPDs(self) :    
+        """Returns a list of PPDs as (ppdnaturallanguage, ppdmake, ppdmakeandmodel, ppdname) tuples."""
+        answer = self.doRequest(self.newRequest(CUPS_GET_PPDS))
+        return zip([d[1] for d in answer.printer["ppd-natural-language"]], \
+                   [d[1] for d in answer.printer["ppd-make"]], \
+                   [d[1] for d in answer.printer["ppd-make-and-model"]], \
+                   [d[1] for d in answer.printer["ppd-name"]])
+                   
+    def createSubscription(self, uri, events=["all"],
+                                      userdata=None,
+                                      recipient=None,
+                                      pullmethod=None,
+                                      charset=None,
+                                      naturallanguage=None,
+                                      leaseduration=None,
+                                      timeinterval=None,
+                                      jobid=None) :
+        """Creates a job, printer or server subscription.
+         
+           uri : the subscription's uri, e.g. ipp://server
+           events : a list of events to subscribe to, e.g. ["printer-added", "printer-deleted"]
+           recipient : the notifier's uri
+           pullmethod : the pull method to use
+           charset : the charset to use when sending notifications
+           naturallanguage : the language to use when sending notifications
+           leaseduration : the duration of the lease in seconds
+           timeinterval : the interval of time during notifications
+           jobid : the optional job id in case of a job subscription
+        """   
+        if jobid is not None :
+            opid = IPP_CREATE_JOB_SUBSCRIPTION
+            uritype = "job-uri"
+        else :
+            opid = IPP_CREATE_PRINTER_SUBSCRIPTION
+            uritype = "printer-uri"
+        req = self.newRequest(opid)
+        req.operation[uritype] = ("uri", uri)
+        for event in events :
+            req.subscription["notify-events"] = ("keyword", event)
+        if userdata is not None :    
+            req.subscription["notify-user-data"] = ("octetString-with-an-unspecified-format", userdata)
+        if recipient is not None :    
+            req.subscription["notify-recipient"] = ("uri", recipient)
+        if pullmethod is not None :
+            req.subscription["notify-pull-method"] = ("keyword", pullmethod)
+        if charset is not None :
+            req.subscription["notify-charset"] = ("charset", charset)
+        if naturallanguage is not None :
+            req.subscription["notify-natural-language"] = ("naturalLanguage", naturallanguage)
+        if leaseduration is not None :
+            req.subscription["notify-lease-duration"] = ("integer", leaseduration)
+        if timeinterval is not None :
+            req.subscription["notify-time-interval"] = ("integer", timeinterval)
+        if jobid is not None :
+            req.subscription["notify-job-id"] = ("integer", jobid)
+        return self.doRequest(req)
+            
+    def cancelSubscription(self, uri, subscriptionid, jobid=None) :    
+        """Cancels a subscription.
+        
+           uri : the subscription's uri.
+           subscriptionid : the subscription's id.
+           jobid : the optional job's id.
+        """
+        req = self.newRequest(IPP_CANCEL_SUBSCRIPTION)
+        if jobid is not None :
+            uritype = "job-uri"
+        else :
+            uritype = "printer-uri"
+        req.operation[uritype] = ("uri", uri)
+        req.event_notification["notify-subscription-id"] = ("integer", subscriptionid)
+        return self.doRequest(req)
+        
+
+class FakeConfig :
+    """Fakes a configuration file parser."""
+    def get(self, section, option, raw=0) :
+        """Fakes the retrieval of an option."""
+        raise ConfigError, "Invalid configuration file : no option %s in section [%s]" % (option, section)
+
+def isTrue(option) :
+    """Returns 1 if option is set to true, else 0."""
+    if (option is not None) and (option.upper().strip() in ['Y', 'YES', '1', 'ON', 'T', 'TRUE']) :
+        return 1
+    else :
+        return 0
+
+def getCupsConfigDirectives(directives=[]) :
+    """Retrieves some CUPS directives from its configuration file.
+
+       Returns a mapping with lowercased directives as keys and
+       their setting as values.
+    """
+    dirvalues = {}
+    cupsroot = os.environ.get("CUPS_SERVERROOT", "/etc/cups")
+    cupsdconf = os.path.join(cupsroot, "cupsd.conf")
+    try :
+        conffile = open(cupsdconf, "r")
+    except IOError :
+        raise TeeError, "Unable to open %s" % cupsdconf
+    else :
+        for line in conffile.readlines() :
+            linecopy = line.strip().lower()
+            for di in [d.lower() for d in directives] :
+                if linecopy.startswith("%s " % di) :
+                    try :
+                        val = line.split()[1]
+                    except :
+                        pass # ignore errors, we take the last value in any case.
+                    else :
+                        dirvalues[di] = val
+        conffile.close()
+    return dirvalues
+    
+class CupsBackend :
+    """Base class for tools with no database access."""
+    def __init__(self) :
+        """Initializes the CUPS backend wrapper."""
+        signal.signal(signal.SIGTERM, signal.SIG_IGN)
+        signal.signal(signal.SIGPIPE, signal.SIG_IGN)
+        self.MyName = "Tea4CUPS"
+        self.myname = "tea4cups"
+        self.pid = os.getpid()
+        self.debug = True
+        self.isCancelled = False
+        self.Title = None
+        self.InputFile = None
+        self.RealBackend = None
+        self.ControlFile = None
+        self.ClientHost = None
+        self.PrinterName = None
+        self.JobBilling = None
+        self.UserName = None
+        self.Copies = None
+        self.Options = None
+        self.DataFile = None
+        self.JobMD5Sum = None
+        self.JobSize = None
+        self.DeviceURI = None
+        self.JobId = None
+        self.Directory = None
+        self.pipes = {}
+        self.config = None
+        self.conffile = None
+
+    def readConfig(self) :
+        """Reads the configuration file."""
+        confdir = os.environ.get("CUPS_SERVERROOT", ".")
+        self.conffile = os.path.join(confdir, "%s.conf" % self.myname)
+        if os.path.isfile(self.conffile) :
+            self.config = ConfigParser.ConfigParser()
+            self.config.read([self.conffile])
+            self.debug = isTrue(self.getGlobalOption("debug", ignore=1))
+        else :
+            self.config = FakeConfig()
+            self.debug = 1      # no config, so force debug mode !
+
+    def logInfo(self, message, level="info") :
+        """Logs a message to CUPS' error_log file."""
+        try :
+            sys.stderr.write("%s: %s v%s (PID %i) : %s\n" % (level.upper(), self.MyName, __version__, os.getpid(), message))
+            sys.stderr.flush()
+        except IOError :
+            pass
+
+    def logDebug(self, message) :
+        """Logs something to debug output if debug is enabled."""
+        if self.debug :
+            self.logInfo(message, level="debug")
+
+    def getGlobalOption(self, option, ignore=0) :
+        """Returns an option from the global section, or raises a ConfigError if ignore is not set, else returns None."""
+        try :
+            return self.config.get("global", option, raw=1)
+        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
+            if not ignore :
+                raise ConfigError, "Option %s not found in section global of %s" % (option, self.conffile)
+
+    def getPrintQueueOption(self, printqueuename, option, ignore=0) :
+        """Returns an option from the printer section, or the global section, or raises a ConfigError."""
+        globaloption = self.getGlobalOption(option, ignore=1)
+        try :
+            return self.config.get(printqueuename, option, raw=1)
+        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError) :
+            if globaloption is not None :
+                return globaloption
+            elif not ignore :
+                raise ConfigError, "Option %s not found in section [%s] of %s" % (option, printqueuename, self.conffile)
+
+    def enumBranches(self, printqueuename, branchtype="tee") :
+        """Returns the list of branchtypes branches for a particular section's."""
+        branchbasename = "%s_" % branchtype.lower()
+        try :
+            globalbranches = [ (k, self.config.get("global", k)) for k in self.config.options("global") if k.startswith(branchbasename) ]
+        except ConfigParser.NoSectionError, msg :
+            raise ConfigError, "Invalid configuration file : %s" % msg
+        try :
+            sectionbranches = [ (k, self.config.get(printqueuename, k)) for k in self.config.options(printqueuename) if k.startswith(branchbasename) ]
+        except ConfigParser.NoSectionError, msg :
+            self.logInfo("No section for print queue %s : %s" % (printqueuename, msg))
+            sectionbranches = []
+        branches = {}
+        for (k, v) in globalbranches :
+            value = v.strip()
+            if value :
+                branches[k] = value
+        for (k, v) in sectionbranches :
+            value = v.strip()
+            if value :
+                branches[k] = value # overwrite any global option or set a new value
+            else :
+                del branches[k] # empty value disables a global option
+        return branches
+
+    def discoverOtherBackends(self) :
+        """Discovers the other CUPS backends.
+
+           Executes each existing backend in turn in device enumeration mode.
+           Returns the list of available backends.
+        """
+        # Unfortunately this method can't output any debug information
+        # to stdout or stderr, else CUPS considers that the device is
+        # not available.
+        available = []
+        (directory, myname) = os.path.split(sys.argv[0])
+        if not directory :
+            directory = "./"
+        tmpdir = tempfile.gettempdir()
+        lockfilename = os.path.join(tmpdir, "%s..LCK" % myname)
+        if os.path.exists(lockfilename) :
+            lockfile = open(lockfilename, "r")
+            pid = int(lockfile.read())
+            lockfile.close()
+            try :
+                # see if the pid contained in the lock file is still running
+                os.kill(pid, 0)
+            except OSError, error :
+                if error.errno != errno.EPERM :
+                    # process doesn't exist anymore
+                    os.remove(lockfilename)
+
+        if not os.path.exists(lockfilename) :
+            lockfile = open(lockfilename, "w")
+            lockfile.write("%i" % self.pid)
+            lockfile.close()
+            allbackends = [ os.path.join(directory, b) \
+                                for b in os.listdir(directory)
+                                    if os.access(os.path.join(directory, b), os.X_OK) \
+                                        and (b != myname)]
+            for backend in allbackends :
+                answer = os.popen(backend, "r")
+                try :
+                    devices = [deviceline.strip() for deviceline in answer.readlines()]
+                except :
+                    devices = []
+                status = answer.close()
+                if status is None :
+                    for d in devices :
+                        # each line is of the form :
+                        # 'xxxx xxxx "xxxx xxx" "xxxx xxx"'
+                        # so we have to decompose it carefully
+                        fdevice = cStringIO.StringIO(d)
+                        tokenizer = shlex.shlex(fdevice)
+                        tokenizer.wordchars = tokenizer.wordchars + \
+                                                        r".:,?!~/\_$*-+={}[]()#"
+                        arguments = []
+                        while 1 :
+                            token = tokenizer.get_token()
+                            if token :
+                                arguments.append(token)
+                            else :
+                                break
+                        fdevice.close()
+                        try :
+                            (devicetype, device, name, fullname) = arguments
+                        except ValueError :
+                            pass    # ignore this 'bizarre' device
+                        else :
+                            if name.startswith('"') and name.endswith('"') :
+                                name = name[1:-1]
+                            if fullname.startswith('"') and fullname.endswith('"') :
+                                fullname = fullname[1:-1]
+                            available.append('%s %s:%s "%s+%s" "%s managed %s"' \
+                                                 % (devicetype, self.myname, device, self.MyName, name, self.MyName, fullname))
+            os.remove(lockfilename)
+        available.append('direct %s:// "%s+Nothing" "%s managed Virtual Printer"' \
+                             % (self.myname, self.MyName, self.MyName))
+        return available
+
+    def initBackend(self) :
+        """Initializes the backend's attributes."""
+        self.JobId = sys.argv[1].strip()
+        self.UserName = sys.argv[2].strip() or pwd.getpwuid(os.geteuid())[0] # use CUPS' user when printing test pages from CUPS' web interface
+        self.Title = sys.argv[3].strip()
+        self.Copies = int(sys.argv[4].strip())
+        self.Options = sys.argv[5].strip()
+        if len(sys.argv) == 7 :
+            self.InputFile = sys.argv[6] # read job's datas from file
+        else :
+            self.InputFile = None        # read job's datas from stdin
+        self.PrinterName = os.environ.get("PRINTER", "")
+        self.Directory = self.getPrintQueueOption(self.PrinterName, "directory", ignore=1) or tempfile.gettempdir()
+        self.DataFile = os.path.join(self.Directory, "%s-%s-%s-%s" % (self.myname, self.PrinterName, self.UserName, self.JobId))
+        
+        # check that the DEVICE_URI environment variable's value is
+        # prefixed with self.myname otherwise don't touch it.
+        # If this is the case, we have to remove the prefix from
+        # the environment before launching the real backend
+        muststartwith = "%s:" % self.myname
+        device_uri = os.environ.get("DEVICE_URI", "")
+        if device_uri.startswith(muststartwith) :
+            fulldevice_uri = device_uri[:]
+            device_uri = fulldevice_uri[len(muststartwith):]
+            for dummy in range(2) :
+                if device_uri.startswith("/") :
+                    device_uri = device_uri[1:]
+        try :
+            (backend, dummy) = device_uri.split(":", 1)
+        except ValueError :
+            if not device_uri :
+                self.logDebug("Not attached to an existing print queue.")
+                backend = ""
+            else :
+                raise TeeError, "Invalid DEVICE_URI : %s\n" % device_uri
+
+        self.RealBackend = backend
+        self.DeviceURI = device_uri
+        
+        try :
+            cupsserver = CUPS() # TODO : username and password and/or encryption
+            answer = cupsserver.getJobAttributes(self.JobId)
+            self.ControlFile = "NotUsedAnymore"
+        except :    
+            (ippfilename, answer) = self.parseIPPRequestFile()
+            self.ControlFile = ippfilename
+        
+        try :
+            john = answer.job["job-originating-host-name"]
+        except KeyError :    
+            try :
+                john = answer.operation["job-originating-host-name"]
+            except KeyError :    
+                john = (None, None)
+        if type(john) == type([]) :                          
+            john = john[-1]
+        (dummy, self.ClientHost) = john                          
+        try :        
+            jbing = answer.job["job-billing"]
+        except KeyError :    
+            jbing = (None, None)
+        if type(jbing) == type([]) : 
+            jbing = jbing[-1]
+        (dummy, self.JobBilling) = jbing
+
+
+    def parseIPPRequestFile(self) :
+        """Parses the IPP message file and returns a tuple (filename, parsedvalue)."""
+        requestroot = os.environ.get("CUPS_REQUESTROOT")
+        if requestroot is None :
+            cupsdconf = getCupsConfigDirectives(["RequestRoot"])
+            requestroot = cupsdconf.get("requestroot", "/var/spool/cups")
+        if (len(self.JobId) < 5) and self.JobId.isdigit() :
+            ippmessagefile = "c%05i" % int(self.JobId)
+        else :
+            ippmessagefile = "c%s" % self.JobId
+        ippmessagefile = os.path.join(requestroot, ippmessagefile)
+        ippmessage = {}
+        try :
+            ippdatafile = open(ippmessagefile)
+        except :
+            self.logInfo("Unable to open IPP message file %s" % ippmessagefile, "warn")
+        else :
+            self.logDebug("Parsing of IPP message file %s begins." % ippmessagefile)
+            try :
+                ippmessage = IPPRequest(ippdatafile.read())
+                ippmessage.parse()
+            except IPPError, msg :
+                self.logInfo("Error while parsing %s : %s" % (ippmessagefile, msg), "warn")
+            else :
+                self.logDebug("Parsing of IPP message file %s ends." % ippmessagefile)
+            ippdatafile.close()
+        return (ippmessagefile, ippmessage)
+
+    def exportAttributes(self) :
+        """Exports our backend's attributes to the environment."""
+        os.environ["DEVICE_URI"] = self.DeviceURI       # WARNING !
+        os.environ["TEAPRINTERNAME"] = self.PrinterName
+        os.environ["TEADIRECTORY"] = self.Directory
+        os.environ["TEADATAFILE"] = self.DataFile
+        os.environ["TEAJOBSIZE"] = str(self.JobSize)
+        os.environ["TEAMD5SUM"] = self.JobMD5Sum
+        os.environ["TEACLIENTHOST"] = self.ClientHost or ""
+        os.environ["TEAJOBID"] = self.JobId
+        os.environ["TEAUSERNAME"] = self.UserName
+        os.environ["TEATITLE"] = self.Title
+        os.environ["TEACOPIES"] = str(self.Copies)
+        os.environ["TEAOPTIONS"] = self.Options
+        os.environ["TEAINPUTFILE"] = self.InputFile or ""
+        os.environ["TEABILLING"] = self.JobBilling or ""
+        os.environ["TEACONTROLFILE"] = self.ControlFile
+
+    def saveDatasAndCheckSum(self) :
+        """Saves the input datas into a static file."""
+        self.logDebug("Duplicating data stream into %s" % self.DataFile)
+        mustclose = 0
+        if self.InputFile is not None :
+            infile = open(self.InputFile, "rb")
+            mustclose = 1
+        else :
+            infile = sys.stdin
+            
+        filtercommand = self.getPrintQueueOption(self.PrinterName, "filter", \
+                                                 ignore=1)
+        if filtercommand :                                                 
+            self.logDebug("Data stream will be filtered through [%s]" % filtercommand)
+            filteroutput = "%s.filteroutput" % self.DataFile
+            outf = open(filteroutput, "wb")
+            filterstatus = self.stdioRedirSystem(filtercommand, infile.fileno(), outf.fileno())
+            outf.close()
+            self.logDebug("Filter's output status : %s" % repr(filterstatus))
+            if mustclose :
+                infile.close()
+            infile = open(filteroutput, "rb")
+            mustclose = 1
+        else :    
+            self.logDebug("Data stream will be used as-is (no filter defined)")
+            
+        CHUNK = 64*1024         # read 64 Kb at a time
+        dummy = 0
+        sizeread = 0
+        checksum = md5.new()
+        outfile = open(self.DataFile, "wb")
+        while 1 :
+            data = infile.read(CHUNK)
+            if not data :
+                break
+            sizeread += len(data)
+            outfile.write(data)
+            checksum.update(data)
+            if not (dummy % 32) : # Only display every 2 Mb
+                self.logDebug("%s bytes saved..." % sizeread)
+            dummy += 1
+        outfile.close()
+        
+        if filtercommand :
+            self.logDebug("Removing filter's output file %s" % filteroutput)
+            try :
+                os.remove(filteroutput)
+            except :    
+                pass
+                
+        if mustclose :
+            infile.close()
+            
+        self.logDebug("%s bytes saved..." % sizeread)
+        self.JobSize = sizeread
+        self.JobMD5Sum = checksum.hexdigest()
+        self.logDebug("Job %s is %s bytes long." % (self.JobId, self.JobSize))
+        self.logDebug("Job %s MD5 sum is %s" % (self.JobId, self.JobMD5Sum))
+
+    def cleanUp(self) :
+        """Cleans up the place."""
+        if (not isTrue(self.getPrintQueueOption(self.PrinterName, "keepfiles", ignore=1))) \
+            and os.path.exists(self.DataFile) :
+            try :
+                os.remove(self.DataFile)
+            except OSError, msg :    
+                self.logInfo("Problem when removing %s : %s" % (self.DataFile, msg), "error")
+
+    def runBranches(self) :
+        """Launches each hook defined for the current print queue."""
+        self.isCancelled = 0    # did a prehook cancel the print job ?
+        serialize = isTrue(self.getPrintQueueOption(self.PrinterName, "serialize", ignore=1))
+        self.pipes = { 0: (0, 1) }
+        branches = self.enumBranches(self.PrinterName, "prehook")
+        for b in branches.keys() :
+            self.pipes[b.split("_", 1)[1]] = os.pipe()
+        retcode = self.runCommands("prehook", branches, serialize)
+        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
+            os.close(p[1][1])
+        if not self.isCancelled :
+            if self.RealBackend :
+                retcode = self.launchOriginalBackend()
+                if retcode :
+                    onfail = self.getPrintQueueOption(self.PrinterName, \
+                                                      "onfail", ignore=1)
+                    if onfail :
+                        self.logDebug("Launching onfail script %s" % onfail)
+                        os.system(onfail)
+                        
+            os.environ["TEASTATUS"] = str(retcode)
+            branches = self.enumBranches(self.PrinterName, "posthook")
+            if self.runCommands("posthook", branches, serialize) :
+                self.logInfo("An error occured during the execution of posthooks.", "warn")
+                
+        for p in [ (k, v) for (k, v) in self.pipes.items() if k != 0 ] :
+            os.close(p[1][0])
+        if not retcode :
+            self.logInfo("OK")
+        else :
+            self.logInfo("An error occured, please check CUPS' error_log file.")
+        return retcode
+
+    def stdioRedirSystem(self, cmd, stdin=0, stdout=1) :
+        """Launches a command with stdio redirected."""
+        # Code contributed by Peter Stuge on May 23rd and June 7th 2005
+        pid = os.fork()
+        if pid == 0 :
+            if stdin != 0 :
+                os.dup2(stdin, 0)
+                os.close(stdin)
+            if stdout != 1 :
+                os.dup2(stdout, 1)
+                os.close(stdout)
+            try :
+                os.execl("/bin/sh", "sh", "-c", cmd)
+            except OSError, msg :
+                self.logDebug("execl() failed: %s" % msg)
+            os._exit(-1)
+        status = os.waitpid(pid, 0)[1]
+        if os.WIFEXITED(status) :
+            return os.WEXITSTATUS(status)
+        return -1
+
+    def runCommand(self, branch, command) :
+        """Runs a particular branch command."""
+        # Code contributed by Peter Stuge on June 7th 2005
+        self.logDebug("Launching %s : %s" % (branch, command))
+        btype, bname = branch.split("_", 1)
+        if bname not in self.pipes.keys() :
+            bname = 0
+        if btype == "prehook" :
+            return self.stdioRedirSystem(command, 0, self.pipes[bname][1])
+        else :
+            return self.stdioRedirSystem(command, self.pipes[bname][0])
+
+    def runCommands(self, btype, branches, serialize) :
+        """Runs the commands for a particular branch type."""
+        exitcode = 0
+        btype = btype.lower()
+        btypetitle = btype.title()
+        branchlist = branches.keys()
+        branchlist.sort()
+        if serialize :
+            self.logDebug("Begin serialized %ss" % btypetitle)
+            for branch in branchlist :
+                retcode = self.runCommand(branch, branches[branch])
+                self.logDebug("Exit code for %s %s on printer %s is %s" % (btype, branch, self.PrinterName, retcode))
+                if retcode :
+                    if (btype == "prehook") and (retcode == 255) : # -1
+                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
+                        self.isCancelled = 1
+                    else :
+                        self.logInfo("%s %s on printer %s didn't exit successfully." % (btypetitle, branch, self.PrinterName), "error")
+                        exitcode = 1
+            self.logDebug("End serialized %ss" % btypetitle)
+        else :
+            self.logDebug("Begin forked %ss" % btypetitle)
+            pids = {}
+            for branch in branchlist :
+                pid = os.fork()
+                if pid :
+                    pids[branch] = pid
+                else :
+                    os._exit(self.runCommand(branch, branches[branch]))
+            for (branch, pid) in pids.items() :
+                retcode = os.waitpid(pid, 0)[1]
+                if os.WIFEXITED(retcode) :
+                    retcode = os.WEXITSTATUS(retcode)
+                else :
+                    retcode = -1
+                self.logDebug("Exit code for %s %s (PID %s) on printer %s is %s" % (btype, branch, pid, self.PrinterName, retcode))
+                if retcode :
+                    if (btype == "prehook") and (retcode == 255) : # -1
+                        self.logInfo("Job %s cancelled by prehook %s" % (self.JobId, branch))
+                        self.isCancelled = 1
+                    else :
+                        self.logInfo("%s %s (PID %s) on printer %s didn't exit successfully." % (btypetitle, branch, pid, self.PrinterName), "error")
+                        exitcode = 1
+            self.logDebug("End forked %ss" % btypetitle)
+        return exitcode
+
+    def launchOriginalBackend(self) :
+        """Launches the original backend, optionally retrying if needed."""
+        number = 1
+        delay = 0
+        retry = self.getPrintQueueOption(self.PrinterName, "retry", ignore=1)
+        if retry is not None :
+            try :
+                (number, delay) = [int(p) for p in retry.strip().split(",")]
+            except (ValueError, AttributeError, TypeError) :    
+                self.logInfo("Invalid value '%s' for the 'retry' directive for printer %s in %s." % (retry, self.PrinterName, self.conffile), "error")
+                number = 1
+                delay = 0
+                
+        loopcount = 1 
+        while 1 :            
+            retcode = self.runOriginalBackend()
+            if not retcode :
+                break
+            else :
+                if (not number) or (loopcount < number) :
+                    self.logInfo("The real backend produced an error, we will try again in %s seconds." % delay, "warn")
+                    time.sleep(delay)
+                    loopcount += 1
+                else :    
+                    break
+        return retcode            
+        
+    def runOriginalBackend(self) :
+        """Launches the original backend."""
+        originalbackend = os.path.join(os.path.split(sys.argv[0])[0], self.RealBackend)
+        arguments = [os.environ["DEVICE_URI"]] + sys.argv[1:]
+        self.logDebug("Starting original backend %s with args %s" % (originalbackend, " ".join(['"%s"' % a for a in arguments])))
+
+        pid = os.fork()
+        if pid == 0 :
+            if self.InputFile is None :
+                f = open(self.DataFile, "rb")
+                os.dup2(f.fileno(), 0)
+                f.close()
+            else :    
+                arguments[6] = self.DataFile # in case a tea4cups filter was applied
+            try :
+                os.execve(originalbackend, arguments, os.environ)
+            except OSError, msg :
+                self.logDebug("execve() failed: %s" % msg)
+            os._exit(-1)
+        killed = 0
+        status = -1
+        while status == -1 :
+            try :
+                status = os.waitpid(pid, 0)[1]
+            except OSError, (err, msg) :
+                if err == 4 :
+                    killed = 1
+        if os.WIFEXITED(status) :
+            status = os.WEXITSTATUS(status)
+            if status :
+                self.logInfo("CUPS backend %s returned %d." % (originalbackend, \
+                                                             status), "error")
+            return status
+        elif not killed :
+            self.logInfo("CUPS backend %s died abnormally." % originalbackend, \
+                                                              "error")
+            return -1
+        else :
+            return 1
+
+if __name__ == "__main__" :
+    # This is a CUPS backend, we should act and die like a CUPS backend
+    wrapper = CupsBackend()
+    if len(sys.argv) == 1 :
+        print "\n".join(wrapper.discoverOtherBackends())
+        sys.exit(0)
+    elif len(sys.argv) not in (6, 7) :
+        sys.stderr.write("ERROR: %s job-id user title copies options [file]\n"\
+                              % sys.argv[0])
+        sys.exit(1)
+    else :
+        returncode = 1
+        try :
+            try :
+                wrapper.readConfig()
+                wrapper.initBackend()
+                wrapper.saveDatasAndCheckSum()
+                wrapper.exportAttributes()
+                returncode = wrapper.runBranches()
+            except SystemExit, e :
+                returncode = e.code
+            except KeyboardInterrupt :    
+                wrapper.logInfo("Job %s interrupted by the administrator !" % wrapper.JobId, "warn")
+            except :
+                import traceback
+                lines = []
+                for errline in traceback.format_exception(*sys.exc_info()) :
+                    lines.extend([l for l in errline.split("\n") if l])
+                errormessage = "ERROR: ".join(["%s (PID %s) : %s\n" % (wrapper.MyName, \
+                                                              wrapper.pid, l) \
+                            for l in (["ERROR: Tea4CUPS v%s" % __version__] + lines)])
+                sys.stderr.write(errormessage)
+                sys.stderr.flush()
+                returncode = 1
+        finally :        
+            wrapper.cleanUp()
+        sys.exit(returncode)
Index: /tea4cups/tags/3.12/clean.sh
===================================================================
--- /tea4cups/tags/3.12/clean.sh (revision 674)
+++ /tea4cups/tags/3.12/clean.sh (revision 674)
@@ -0,0 +1,30 @@
+#! /bin/sh
+
+# Tea4CUPS : Tee for CUPS
+#
+# (c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
+# (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# $Id$
+#
+#
+# Use this to clean the tree from temporary files
+
+find . -name "*.bak" -exec rm -f {} \;
+find . -name "*~" -exec rm -f {} \;
+find . -name "*.pyc" -exec rm -f {} \;
+find . -name "*.pyo" -exec rm -f {} \;
+find . -name "*.jem" -exec rm -f {} \;
Index: /tea4cups/tags/3.12/NEWS
===================================================================
--- /tea4cups/tags/3.12/NEWS (revision 684)
+++ /tea4cups/tags/3.12/NEWS (revision 684)
@@ -0,0 +1,99 @@
+# $Id$
+
+Tea4CUPS : Tee for CUPS
+
+(c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
+(c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+=============================================================================
+
+Tea4CUPS News :
+
+  * 3.12 (2006-08-10) :
+  
+    - Improved support for CUPS 1.2.x and higher.
+    
+    - Allows the administrator to cleanly stop tea4cups with SIGINT.
+    
+    - Added a configurable retry directive to the configuration file.
+    
+    - Fixed several minor bugs.
+    
+    - Improved the installation instructions and the sample configuration
+      file.
+    
+  * 3.11 : 
+  
+    - Fixed an incompatibility with Python 2.1, thanks to Frank
+      Koormann.
+      
+  * 3.10 :
+  
+    - Added filters.
+    
+    - Added the "onfail" directive to tea4cups.conf
+    
+  * 3.02 :
+  
+    - Fixed some problems thanks to pychecker.
+    
+  * 3.01 :
+  
+    - Fixed an IPP parsing bug in some situations.
+    
+  * 3.00 :
+  
+    - Tees don't exist anymore. Only prehooks and posthooks remain.
+      
+    - Prehooks can now send datas to Posthooks through pipes.
+    
+    - Major rewrite of the subprocess management code, thanks to Peter Stuge.
+    
+  * 2.12alpha :
+
+    - Greatly improved the IPP parser.
+
+    - When printing test pages from CUPS' web interface, the empty username
+      is now overwritten with the username CUPS is running as.
+
+  * 2.11 :
+
+    - Now works with old releases of Python (>= 2.1)
+
+    - More powerful error handling at startup time.
+
+    - Ignores errors in posthooks because the job was probably
+      successfully printed so no need to stop the print queue.
+      We log a warning instead in this case.
+
+  * 2.10 :
+
+    - Makes the IPP message filename and the Job's billing code available.
+
+  * 2.00 :
+
+    - Introduces the notions of pre and post hooks.
+
+    - Prehooks can decide to entirely cancel the print job if needed.
+
+    - Original CUPS backend's exit code is available in posthooks.
+
+    - prehooks, posthooks and tees are now launched in alphabetical order.
+
+  * 1.00 :
+
+    - First Official release.
+
Index: /tea4cups/tags/3.12/tea4cups.conf
===================================================================
--- /tea4cups/tags/3.12/tea4cups.conf (revision 684)
+++ /tea4cups/tags/3.12/tea4cups.conf (revision 684)
@@ -0,0 +1,248 @@
+# $Id$
+#
+# Tea4CUPS : Tee for CUPS
+#
+# (c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
+# (c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+#
+
+
+
+# First we set all top-level directives in the [global] section
+[global]
+
+
+
+# Should we log debugging information to CUPS' error_log file ?
+# defaults to No if unset.
+debug : yes
+
+
+
+# In which directory will we create our files ? It must already exist !
+# This directive MUST be present since there's no sane default value.
+# Can be set either in the [global] section or any print queue section.
+# The value defined in a print queue section takes precedence over the
+# value defined in the [global] section.
+#
+# directory : /var/spool/tea4cups/
+directory : /var/spool/cups/
+
+
+
+# Should Tea4CUPS keep the files it creates once all hooks have ended ?
+# Defaults to No if unset, meaning that files are automatically deleted
+# once all hooks have ended.
+# Can be set either in the [global] section or any print queue section.
+# The value defined in a print queue section takes precedence over the
+# value defined in the [global] section.
+# BEWARE : this may use huge amounts of disk space !
+#
+# keepfiles : yes
+
+
+
+# Should we retry to send the job's datas to the real backend in the case
+# it fails ? The default when not set is to try to send the datas only once
+# to the real backend.
+# Can be set either in the [global] section or any print queue section.
+# The value defined in a print queue section takes precedence over the
+# value defined in the [global] section.
+#
+# Syntax : "retry: N,S"
+#
+#           N : number of times to try. If 0, will retry indefinitely, until
+#               the backend accepts all the datas without error.              
+#
+#           S : delay in Seconds between two attempts.
+#
+# The example below would retry up to three times, at 60 seconds interval.
+# 
+# retry : 3,60
+
+
+
+# Should we launch some command when the real CUPS backend fails ?
+# Can be set either in the [global] section or any print queue section.
+# The value defined in a print queue section takes precedence over the
+# value defined in the [global] section.
+# IMPORTANT : this directive is only taken into account when the number
+# of tries as defined in the 'retry' directive above have expired and
+# the real backend still fails.
+#
+# onfail : echo "Original Backend failed" | /usr/bin/mail -s Tea4CUPS root
+
+
+
+# Should we pass incoming datas through a filter command 
+# BEFORE doing anything else ?
+# NB : obvisouly the filter command doesn't have any access to
+#      the environment variables defined below.
+# Can be set either in the [global] section or any print queue section.
+# The value defined in a print queue section takes precedence over the
+# value defined in the [global] section.
+# 
+# The sample filter below can remove the print job creation date
+# from PostScript jobs, in order to more accurately detect duplicate
+# jobs (so the MD5SUM would be identical from launch to launch)
+#
+# filter : /bin/grep -v "%%CreationDate:"
+
+
+
+# Should we serialize the launch of all hooks : launch one after
+# the other to save some system resources.
+# Defaults to No if unset, meaning that all hooks are launched in
+# parallel.
+#
+# NB : in any case, hooks' names are sorted alphabetically and
+# are launched in this sort order (obviously when launched in parallel
+# this is unnoticeable).
+#
+# Can be set either in the [global] section or any print queue section.
+# The value defined in a print queue section takes precedence over the
+# value defined in the [global] section.
+#
+# serialize : yes
+
+
+
+# When executing the contents of a prehook or posthook directive, as
+# defined below, tea4cups makes the following environment variables 
+# available to your own commands :
+#
+#      TEAPRINTERNAME : The print queue name.
+#      TEADIRECTORY : Tea4CUPS output directory.
+#      TEADATAFILE : Full name of Tea4CUPS work file (in $TEADIRECTORY).
+#      TEAJOBSIZE : Job's size in bytes.
+#      TEAMD5SUM : MD5 sum of the job's datas.
+#      TEACLIENTHOST : Client's hostname or IP address.
+#      TEAJOBID : Job's Id.
+#      TEAUSERNAME : Name of the user who launched the print job.
+#      TEATITLE : Job's title.
+#      TEACOPIES : Number of copies requested.
+#      TEAOPTIONS : Options of the print job.
+#      TEAINPUTFILE : Job's data file or empty when job read from stdin.
+#      TEABILLING : Job's billing code (lp -o job-billing=SomeCode file.ps)
+#      TEACONTROLFILE : Job's IPP message file (usually /var/spool/cups/c?????)
+#
+# Your own commands will mostly be interested in TEADATAFILE which is
+# the name of the file from which your commands may extract the final
+# job's datas. Don't rely on TEAINPUTFILE, use TEADATAFILE instead
+# since the first one may be empty depending on your printer driver.
+
+
+
+# Some prehooks and posthooks
+#
+# Prehooks are guaranteed to be launched
+# BEFORE the job's datas are sent to the printer, and
+# posthooks are guaranteed to be launched AFTER the job's
+# datas have been sent to the printer.
+#
+# prehook names are completely free BUT THEY MUST BEGIN WITH 'prehook_'
+# posthook names are completely free BUT THEY MUST BEGIN WITH 'posthook_'
+#
+# An additionnal environment variable is made available to posthooks,
+# named TEASTATUS which contains the exitcode of the real CUPS backend.
+# If defined, the normal value is 0, meaning that the real CUPS backend exited
+# successfully. Any other value indicates that a problem occured
+# in the CUPS backend which handles the transmission of the job
+# to the printer.
+#
+# prehook_0 : echo "Your print job has been accepted" | smbclient -M $TEAUSERNAME
+# posthook_0 : echo "Your print job has been printed with status $TEASTATUS" | smbclient -M $TEAUSERNAME
+#
+# NB : as a special feature, any prehook which exits with a -1 status (255)
+# causes the job to NOT be sent to the real backend, effectively cancelling it.
+# None of the posthooks gets executed in this case, but all
+# remaining prehooks are still executed.
+#
+#posthook_0 : cat $TEADATAFILE >/tmp/$TEAJOBID.prn
+
+
+
+# Another example : a PDF generator which creates PDF documents
+# in the user's home directory under the names JOB-iiii.pdf 
+# where iiii is the job id :
+#
+# Beware : using ~$TEAUSERNAME here doesn't work because the ~ is not expanded.
+# NB : for this to work you might have to print with 'lp -o raw'
+#
+# prehook_rawpdf : /bin/cat $TEADATAFILE | su $TEAUSERNAME -c "ps2pdf - `/usr/bin/getent passwd $TEAUSERNAME | /usr/bin/cut -f 6,6 -d :`/JOB-$TEAJOBID.pdf"
+#
+# The posthook below will log all error or warning messages sent by ps2pdf
+# to a job specific file in /tmp/. See the explanation on how pre and post
+# hooks can communicate at the end of this sample configuration file.
+#
+# posthook_rawpdf : /bin/cat >/tmp/log_of_pdf_creation_for_job_$TEAJOBID
+
+
+
+# Sample section for print queue HP2100
+# Uncomment and adapt to your needs.
+#
+#[HP2100]
+#
+# By using a same hook name as in the [global] section, the new
+# value takes precedence
+#
+#posthook_0 : cat $TEADATAFILE >/tmp/$TEAUSERNAME-$TEAJOBID.prn
+
+
+
+# An empty value deletes a value defined in the [global] section
+# so this particular hook doesn't get executed on this printer.
+#
+#prehook_rawpdf :
+
+
+
+# A reflector which produces 4 copies each time :
+#
+#posthook_4copies : lp -dotherprinter -n4 $TEADATAFILE
+
+
+
+# A simple accounting mechanism
+#
+#prehook_accounting : echo $TEAPRINTERNAME $TEAJOBID $TEAUSERNAME $TEABILLING `pkpgcounter $TEADATAFILE` >>/var/log/printaccounting.log
+
+
+# Some additionnal hooks to forbid duplicate jobs :
+# The prehook will use the history file to filter out duplicate jobs
+# checkdupes is an hypothetical command which exits -1 if the current print
+# job is a duplicate (same MD5 sum already found in history).
+#
+# NB : Beware of some software which embed the job printing time into the
+# PostScript job : two identical jobs may have different MD5 checksums
+# if they differ only by the value of the '%%CreationDate:' PostScript 
+# comment !
+#
+#prehook_to_filter_duplicates : /usr/local/bin/checkdupes $TEAMD5SUM /tmp/jobmd5sums
+#posthook_to_filter_duplicates : echo "$TEAJOBID : $TEAMD5SUM" >>/tmp/jobmd5sums
+
+
+
+# A particular prehook can send datas to the posthook
+# of the same name automatically through pipes :
+# The file /tmp/result1 will contain the output of prehook_dialog1
+# prehook_dialog1 : echo "This is pipe #1"
+# posthook_dialog1 : cat >/tmp/result1
+# The file /tmp/result2 will contain the output of prehook_dialog2
+# prehook_dialog2 : echo "This is pipe #2"
+# posthook_dialog2 : cat >/tmp/result2
Index: /tea4cups/tags/3.12/CREDITS
===================================================================
--- /tea4cups/tags/3.12/CREDITS (revision 674)
+++ /tea4cups/tags/3.12/CREDITS (revision 674)
@@ -0,0 +1,57 @@
+# $Id$
+
+Tea4CUPS : Tee for CUPS
+
+(c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
+(c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+============================================================
+
+Supporters :
+------------
+
+  The following people supports the Tea4CUPS project enhancement
+  by funding it :
+
+  - Anwar Bashir - University of Teesside
+  - James Cross - Student Assistance Foundation
+  - Frank Koormann - Intevation GmbH
+  - Asher Schaffer
+  - Gary Carroll
+  - Clive Bruton
+  - Mark Wolf - ESCO Corporation
+  
+
+Contributors :
+--------------
+
+  The following people contributed to the Tea4CUPS project :
+
+  - Peter Stuge : rewrote most of the subprocess management stuff.
+  - Frank Koormann : fixed a problem with Python 2.1
+
+==============================================================
+
+  IF YOUR NAME DOESN'T APPEAR IN THE LISTS ABOVE, AND YOU
+  THINK YOU DESERVE TO BE LISTED, JUST SEND ME AN EMAIL
+  ASKING FOR BEING INCLUDED.
+
+  I APOLOGIZE IN ADVANCE IF I'VE FORGOTTEN TO ADD YOUR NAME.
+
+  ALSO, SOME PEOPLE MAY HAVE REFUSED TO BE LISTED HERE, THIS
+  IS YOUR RIGHT TO DO SO TOO, JUST TELL ME.
+
+==============================================================
Index: /tea4cups/tags/3.12/README
===================================================================
--- /tea4cups/tags/3.12/README (revision 680)
+++ /tea4cups/tags/3.12/README (revision 680)
@@ -0,0 +1,162 @@
+# $Id$
+
+Tea4CUPS : Tee for CUPS
+
+(c) 2005, 2006 Jerome Alet <alet@librelogiciel.com>
+(c) 2005 Peter Stuge <stuge-tea4cups@cdy.org>
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+=============================================================================
+
+Tea4CUPS resembles the *nix command 'tee', but is a CUPS backend.
+
+Tea4CUPS behaves just like any other CUPS backend, but allows you to
+modify print jobs' datas as they pass through it and to transparently 
+send them to any number of outputs : other CUPS backends, files or pipes.
+
+This for example allows you to output the same print job on several
+printers at the same time, which is not possible with CUPS.
+
+Another possibility would be for example to send the same document to
+a printer, a PDF generator, a Fax, and as an attachment to an email
+message, all of this by printing it a single time.
+
+Tea4CUPS allows this by permitting you to easily plug your own
+commands into the very last stage of CUPS' filtering chain.
+
+Tea4CUPS provides three ways to launch commands :
+
+        - filters : A filter can modify the input datas before they
+                    are sent to the printer or made available to
+                    the pre and post hooks defined below.
+                    Currently a single filter can be defined
+                    per print queue.
+
+        - prehooks : these are guaranteed to be launched before the
+                     print job is sent to the real printer.
+                     Any prehook which exits -1 can cancel the
+                     print job. Any number of prehooks can be
+                     defined for a particular print queue.
+
+        - posthooks : these are guaranteed to be launched after the
+                      print job has been sent to the real printer,
+                      unless the job was previously cancelled by a
+                      prehook. Any number of posthooks can be 
+                      defined for a particular print queue.
+
+To help your own commands, Tea4CUPS makes available as part of the
+environment several variables which can be used from the
+commands you use in pre and post hooks :
+
+        TEAPRINTERNAME : The print queue name.
+        TEADIRECTORY : Tea4CUPS output directory.
+        TEADATAFILE : Full name of Tea4CUPS work file (in $TEADIRECTORY).
+        TEAJOBSIZE : Job's size in bytes.
+        TEAMD5SUM : MD5 sum of the job's datas.
+        TEACLIENTHOST : Client's hostname or IP address.
+        TEAJOBID : Job's Id.
+        TEAUSERNAME : Name of the user who launched the print job.
+        TEATITLE : Job's title.
+        TEACOPIES : Number of copies requested.
+        TEAOPTIONS : Options of the print job.
+        TEAINPUTFILE : Print job's data file or empty when job read from stdin.
+        TEABILLING : Job's billing code (lp -o job-billing=SomeCode file.ps)
+        TEACONTROLFILE : Job's IPP message file (usually /var/spool/cups/c?????)
+        TEASTATUS : Original CUPS backend's exit code : ONLY AVAILABLE FROM
+                    posthooks, obviously.
+
+        None of these environment variables is available to filters.
+  
+NB : Tea4CUPS requires a version of Python >= 2.3
+
+=============================================================================
+
+Installation :
+--------------
+
+  0 - Download Tea4CUPS from :
+
+        http://www.pykota.com/software/tea4cups
+
+      and extract it :
+
+        $ tar -zxf tea4cups-x.yy.tar.gz
+
+        where x.yy is Tea4CUPS' version number.
+
+  1 - Copy the 'tea4cups' command into CUPS' backend directory,
+      for example :
+
+        $ cp tea4cups /usr/lib/cups/backend
+        
+      If you use CUPS v1.2 or higher you must do this as well :
+      
+        $ chown root.root /usr/lib/cups/backend/tea4cups
+        $ chmod 700 /usr/lib/cups/backend/tea4cups
+
+  2 - Restart CUPS in order for this new backend to be detected.
+      NB : For this to work you need CUPS 1.1.15 or higher.
+      You can use Tea4CUPS with older versions if you want, but
+      without autodetection.
+
+  3 - Either from CUPS' web interface, add new printers with
+      'Tea4CUPS managed' in front of the device's name.
+
+      Or by directly modifying CUPS' printers.conf file, or with
+      the lpadmin command line tool, just prepend each DeviceURI
+      value with 'tea4cups://'. If you modified printers.conf 
+      directly instead of using lpadmin then don't forget to 
+      restart CUPS.
+      (Use this last method if autodetection doesn't work because
+      the version of CUPS you use is too old)
+
+  4 - Copy tea4cups.conf into CUPS' configuration directory (where
+      cupsd.conf resides) :
+
+        $ cp tea4cups.conf /etc/cups/
+
+  5 - Modify tea4cups.conf to suit your needs. Several examples
+      are included.
+
+  6 - There's no 6 !
+
+=============================================================================
+
+Troubleshooting :
+-----------------
+
+  1 - Set "LogLevel debug2" and "MaxLogSize 0" in CUPS' cupsd.conf
+
+  2 - Restart CUPS.
+
+  3 - Set "debug: yes" in the [global] section of tea4cups.conf
+
+  4 - Print something on a print queue managed
+      by Tea4CUPS.
+
+  5 - Look at CUPS' error_log file for diagnostic and error messages.
+
+=============================================================================
+
+CUPS (Common UNIX(r) Printing System) is trademark property of Easy Software
+Products.
+
+You can download CUPS from :
+
+        http://www.cups.org
+
+=============================================================================
+
+Please e-mail bugs to : alet@librelogiciel.com (Jerome Alet)
