root / pykota / trunk / pykota / tool.py @ 1192

Revision 1192, 23.8 kB (checked in by jalet, 21 years ago)

Mailto can be any external command now, as usual.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
RevLine 
[1144]1# PyKota
2# -*- coding: ISO-8859-15 -*-
[695]3
[952]4# PyKota - Print Quotas for CUPS and LPRng
[695]5#
6# (c) 2003 Jerome Alet <alet@librelogiciel.com>
[873]7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
[695]11#
[873]12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
[695]20#
21# $Id$
22#
23# $Log$
[1192]24# Revision 1.55  2003/11/18 23:43:12  jalet
25# Mailto can be any external command now, as usual.
26#
[1171]27# Revision 1.54  2003/10/24 21:52:46  jalet
28# Now can force language when coming from CGI script.
29#
[1152]30# Revision 1.53  2003/10/08 21:41:38  jalet
31# External policies for printers works !
32# We can now auto-add users on first print, and do other useful things if needed.
33#
[1144]34# Revision 1.52  2003/10/07 09:07:28  jalet
35# Character encoding added to please latest version of Python
36#
[1140]37# Revision 1.51  2003/10/06 14:21:41  jalet
38# Test reversed to not retrieve group members when no messages for them.
39#
[1130]40# Revision 1.50  2003/10/02 20:23:18  jalet
41# Storage caching mechanism added.
42#
[1113]43# Revision 1.49  2003/07/29 20:55:17  jalet
44# 1.14 is out !
45#
[1091]46# Revision 1.48  2003/07/21 23:01:56  jalet
47# Modified some messages aout soft limit
48#
[1087]49# Revision 1.47  2003/07/16 21:53:08  jalet
50# Really big modifications wrt new configuration file's location and content.
51#
[1079]52# Revision 1.46  2003/07/09 20:17:07  jalet
53# Email field added to PostgreSQL schema
54#
[1077]55# Revision 1.45  2003/07/08 19:43:51  jalet
56# Configurable warning messages.
57# Poor man's treshold value added.
58#
[1068]59# Revision 1.44  2003/07/07 11:49:24  jalet
60# Lots of small fixes with the help of PyChecker
61#
[1061]62# Revision 1.43  2003/07/04 09:06:32  jalet
63# Small bug fix wrt undefined "LimitBy" field.
64#
[1048]65# Revision 1.42  2003/06/30 12:46:15  jalet
66# Extracted reporting code.
67#
[1041]68# Revision 1.41  2003/06/25 14:10:01  jalet
69# Hey, it may work (edpykota --reset excepted) !
70#
[1021]71# Revision 1.40  2003/06/10 16:37:54  jalet
72# Deletion of the second user which is not needed anymore.
73# Added a debug configuration field in /etc/pykota.conf
74# All queries can now be sent to the logger in debug mode, this will
75# greatly help improve performance when time for this will come.
76#
[973]77# Revision 1.39  2003/04/29 18:37:54  jalet
78# Pluggable accounting methods (actually doesn't support external scripts)
79#
[956]80# Revision 1.38  2003/04/24 11:53:48  jalet
81# Default policy for unknown users/groups is to DENY printing instead
82# of the previous default to ALLOW printing. This is to solve an accuracy
83# problem. If you set the policy to ALLOW, jobs printed by in nexistant user
84# (from PyKota's POV) will be charged to the next user who prints on the
85# same printer.
86#
[955]87# Revision 1.37  2003/04/24 08:08:27  jalet
88# Debug message forgotten
89#
[954]90# Revision 1.36  2003/04/24 07:59:40  jalet
91# LPRng support now works !
92#
[952]93# Revision 1.35  2003/04/23 22:13:57  jalet
94# Preliminary support for LPRng added BUT STILL UNTESTED.
95#
[929]96# Revision 1.34  2003/04/17 09:26:21  jalet
97# repykota now reports account balances too.
98#
[927]99# Revision 1.33  2003/04/16 12:35:49  jalet
100# Groups quota work now !
101#
[925]102# Revision 1.32  2003/04/16 08:53:14  jalet
103# Printing can now be limited either by user's account balance or by
104# page quota (the default). Quota report doesn't include account balance
105# yet, though.
106#
[915]107# Revision 1.31  2003/04/15 11:30:57  jalet
108# More work done on money print charging.
109# Minor bugs corrected.
110# All tools now access to the storage as priviledged users, repykota excepted.
111#
[900]112# Revision 1.30  2003/04/10 21:47:20  jalet
113# Job history added. Upgrade script neutralized for now !
114#
[873]115# Revision 1.29  2003/03/29 13:45:27  jalet
116# GPL paragraphs were incorrectly (from memory) copied into the sources.
117# Two README files were added.
118# Upgrade script for PostgreSQL pre 1.01 schema was added.
119#
[872]120# Revision 1.28  2003/03/29 13:08:28  jalet
121# Configuration is now expected to be found in /etc/pykota.conf instead of
122# in /etc/cups/pykota.conf
123# Installation script can move old config files to the new location if needed.
124# Better error handling if configuration file is absent.
125#
[852]126# Revision 1.27  2003/03/15 23:01:28  jalet
127# New mailto option in configuration file added.
128# No time to test this tonight (although it should work).
129#
[844]130# Revision 1.26  2003/03/09 23:58:16  jalet
131# Comment
132#
[834]133# Revision 1.25  2003/03/07 22:56:14  jalet
134# 0.99 is out with some bug fixes.
135#
[825]136# Revision 1.24  2003/02/27 23:48:41  jalet
137# Correctly maps PyKota's log levels to syslog log levels
138#
[824]139# Revision 1.23  2003/02/27 22:55:20  jalet
140# WARN log priority doesn't exist.
141#
[817]142# Revision 1.22  2003/02/27 09:09:20  jalet
143# Added a method to match strings against wildcard patterns
144#
[804]145# Revision 1.21  2003/02/17 23:01:56  jalet
146# Typos
147#
[802]148# Revision 1.20  2003/02/17 22:55:01  jalet
149# More options can now be set per printer or globally :
150#
[804]151#       admin
152#       adminmail
153#       gracedelay
154#       requester
[802]155#
156# the printer option has priority when both are defined.
157#
[788]158# Revision 1.19  2003/02/10 11:28:45  jalet
159# Localization
160#
[782]161# Revision 1.18  2003/02/10 01:02:17  jalet
162# External requester is about to work, but I must sleep
163#
[773]164# Revision 1.17  2003/02/09 13:05:43  jalet
165# Internationalization continues...
166#
[772]167# Revision 1.16  2003/02/09 12:56:53  jalet
168# Internationalization begins...
169#
[764]170# Revision 1.15  2003/02/08 22:09:52  jalet
171# Name check method moved here
172#
[742]173# Revision 1.14  2003/02/07 10:42:45  jalet
174# Indentation problem
175#
[733]176# Revision 1.13  2003/02/07 08:34:16  jalet
177# Test wrt date limit was wrong
178#
[729]179# Revision 1.12  2003/02/06 23:20:02  jalet
180# warnpykota doesn't need any user/group name argument, mimicing the
181# warnquota disk quota tool.
182#
[728]183# Revision 1.11  2003/02/06 22:54:33  jalet
184# warnpykota should be ok
185#
[722]186# Revision 1.10  2003/02/06 15:03:11  jalet
187# added a method to set the limit date
188#
[715]189# Revision 1.9  2003/02/06 10:39:23  jalet
190# Preliminary edpykota work.
191#
[713]192# Revision 1.8  2003/02/06 09:19:02  jalet
193# More robust behavior (hopefully) when the user or printer is not managed
194# correctly by the Quota System : e.g. cupsFilter added in ppd file, but
195# printer and/or user not 'yet?' in storage.
196#
[712]197# Revision 1.7  2003/02/06 00:00:45  jalet
198# Now includes the printer name in email messages
199#
[711]200# Revision 1.6  2003/02/05 23:55:02  jalet
201# Cleaner email messages
202#
[709]203# Revision 1.5  2003/02/05 23:45:09  jalet
204# Better DateTime manipulation wrt grace delay
205#
[708]206# Revision 1.4  2003/02/05 23:26:22  jalet
207# Incorrect handling of grace delay
208#
[699]209# Revision 1.3  2003/02/05 22:16:20  jalet
210# DEVICE_URI is undefined outside of CUPS, i.e. for normal command line tools
211#
[698]212# Revision 1.2  2003/02/05 22:10:29  jalet
213# Typos
214#
[695]215# Revision 1.1  2003/02/05 21:28:17  jalet
216# Initial import into CVS
217#
218#
219#
220
221import sys
[817]222import fnmatch
[715]223import getopt
[695]224import smtplib
[772]225import gettext
[782]226import locale
[695]227
[708]228from mx import DateTime
229
[715]230from pykota import version, config, storage, logger
[695]231
232class PyKotaToolError(Exception):
233    """An exception for PyKota config related stuff."""
234    def __init__(self, message = ""):
235        self.message = message
236        Exception.__init__(self, message)
237    def __repr__(self):
238        return self.message
239    __str__ = __repr__
240   
241class PyKotaTool :   
242    """Base class for all PyKota command line tools."""
[1171]243    def __init__(self, lang=None, doc="PyKota %s (c) 2003 %s" % (version.__version__, version.__author__)) :
[695]244        """Initializes the command line tool."""
[772]245        # locale stuff
[788]246        try :
[1171]247            locale.setlocale(locale.LC_ALL, lang)
[788]248            gettext.install("pykota")
249        except (locale.Error, IOError) :
250            gettext.NullTranslations().install()
[1171]251            sys.stderr.write("PyKota : Error while loading translations\n")
[772]252   
253        # pykota specific stuff
[715]254        self.documentation = doc
[1087]255        self.config = config.PyKotaConfig("/etc/pykota")
[1021]256        self.logger = logger.openLogger(self)
[1130]257        self.debug = self.config.getDebug()
[1021]258        self.storage = storage.openConnection(self)
[695]259        self.smtpserver = self.config.getSMTPServer()
260       
[1130]261    def logdebug(self, message) :   
262        """Logs something to debug output if debug is enabled."""
263        if self.debug :
264            self.logger.log_message(message, "debug")
265       
[1113]266    def clean(self) :   
267        """Ensures that the database is closed."""
268        try :
269            self.storage.close()
270        except (TypeError, NameError, AttributeError) :   
271            pass
272           
[715]273    def display_version_and_quit(self) :
274        """Displays version number, then exists successfully."""
[1113]275        self.clean()
[715]276        print version.__version__
277        sys.exit(0)
278   
279    def display_usage_and_quit(self) :
280        """Displays command line usage, then exists successfully."""
[1113]281        self.clean()
[715]282        print self.documentation
283        sys.exit(0)
284       
[729]285    def parseCommandline(self, argv, short, long, allownothing=0) :
[715]286        """Parses the command line, controlling options."""
287        # split options in two lists: those which need an argument, those which don't need any
288        withoutarg = []
289        witharg = []
290        lgs = len(short)
291        i = 0
292        while i < lgs :
293            ii = i + 1
294            if (ii < lgs) and (short[ii] == ':') :
295                # needs an argument
296                witharg.append(short[i])
297                ii = ii + 1 # skip the ':'
298            else :
299                # doesn't need an argument
300                withoutarg.append(short[i])
301            i = ii
302               
303        for option in long :
304            if option[-1] == '=' :
305                # needs an argument
306                witharg.append(option[:-1])
307            else :
308                # doesn't need an argument
309                withoutarg.append(option)
310       
311        # we begin with all possible options unset
312        parsed = {}
313        for option in withoutarg + witharg :
314            parsed[option] = None
315       
316        # then we parse the command line
317        args = []       # to not break if something unexpected happened
318        try :
319            options, args = getopt.getopt(argv, short, long)
320            if options :
321                for (o, v) in options :
322                    # we skip the '-' chars
323                    lgo = len(o)
324                    i = 0
325                    while (i < lgo) and (o[i] == '-') :
326                        i = i + 1
327                    o = o[i:]
328                    if o in witharg :
329                        # needs an argument : set it
330                        parsed[o] = v
331                    elif o in withoutarg :
332                        # doesn't need an argument : boolean
333                        parsed[o] = 1
334                    else :
335                        # should never occur
336                        raise PyKotaToolError, "Unexpected problem when parsing command line"
[729]337            elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
[715]338                self.display_usage_and_quit()
339        except getopt.error, msg :
340            sys.stderr.write("%s\n" % msg)
341            sys.stderr.flush()
342            self.display_usage_and_quit()
343        return (parsed, args)
344   
[764]345    def isValidName(self, name) :
346        """Checks if a user or printer name is valid."""
347        # unfortunately Python 2.1 string modules doesn't define ascii_letters...
348        asciiletters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
349        digits = '0123456789'
350        if name[0] in asciiletters :
351            validchars = asciiletters + digits + "-_"
352            for c in name[1:] :
353                if c not in validchars :
354                    return 0
355            return 1       
356        return 0
357       
[817]358    def matchString(self, s, patterns) :
359        """Returns 1 if the string s matches one of the patterns, else 0."""
360        for pattern in patterns :
361            if fnmatch.fnmatchcase(s, pattern) :
362                return 1
363        return 0
364       
[802]365    def sendMessage(self, adminmail, touser, fullmessage) :
[695]366        """Sends an email message containing headers to some user."""
367        if "@" not in touser :
368            touser = "%s@%s" % (touser, self.smtpserver)
369        server = smtplib.SMTP(self.smtpserver)
[1041]370        try :
371            server.sendmail(adminmail, [touser], fullmessage)
372        except smtplib.SMTPRecipientsRefused, answer :   
373            for (k, v) in answer.recipients.items() :
374                self.logger.log_message(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
[695]375        server.quit()
376       
[1079]377    def sendMessageToUser(self, admin, adminmail, user, subject, message) :
[695]378        """Sends an email message to a user."""
[802]379        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
[1079]380        self.sendMessage(adminmail, user.Email or user.Name, "Subject: %s\n\n%s" % (subject, message))
[695]381       
[802]382    def sendMessageToAdmin(self, adminmail, subject, message) :
[695]383        """Sends an email message to the Print Quota administrator."""
[802]384        self.sendMessage(adminmail, adminmail, "Subject: %s\n\n%s" % (subject, message))
[695]385       
[1041]386    def checkGroupPQuota(self, grouppquota) :   
[927]387        """Checks the group quota on a printer and deny or accept the job."""
[1041]388        group = grouppquota.Group
389        printer = grouppquota.Printer
[1061]390        if group.LimitBy and (group.LimitBy.lower() == "balance") : 
[1077]391            if group.AccountBalance <= 0.0 :
[1041]392                action = "DENY"
[1077]393            elif group.AccountBalance <= self.config.getPoorMan() :   
394                action = "WARN"
[927]395            else :   
[1041]396                action = "ALLOW"
[927]397        else :
[1041]398            if grouppquota.SoftLimit is not None :
399                softlimit = int(grouppquota.SoftLimit)
400                if grouppquota.PageCounter < softlimit :
401                    action = "ALLOW"
[927]402                else :   
[1041]403                    if grouppquota.HardLimit is None :
404                        # only a soft limit, this is equivalent to having only a hard limit
405                        action = "DENY"
[927]406                    else :   
[1041]407                        hardlimit = int(grouppquota.HardLimit)
408                        if softlimit <= grouppquota.PageCounter < hardlimit :   
409                            now = DateTime.now()
410                            if grouppquota.DateLimit is not None :
411                                datelimit = DateTime.ISO.ParseDateTime(grouppquota.DateLimit)
412                            else :
413                                datelimit = now + self.config.getGraceDelay(printer.Name)
414                                grouppquota.setDateLimit(datelimit)
415                            if now < datelimit :
416                                action = "WARN"
417                            else :   
[927]418                                action = "DENY"
[1041]419                        else :         
[927]420                            action = "DENY"
[1041]421            else :       
422                if grouppquota.HardLimit is not None :
423                    # no soft limit, only a hard one.
424                    hardlimit = int(grouppquota.HardLimit)
425                    if grouppquota.PageCounter < hardlimit :
[927]426                        action = "ALLOW"
[1041]427                    else :     
428                        action = "DENY"
429                else :
430                    # Both are unset, no quota, i.e. accounting only
431                    action = "ALLOW"
[927]432        return action
433   
[1041]434    def checkUserPQuota(self, userpquota) :
[708]435        """Checks the user quota on a printer and deny or accept the job."""
[1041]436        user = userpquota.User
437        printer = userpquota.Printer
438       
[927]439        # first we check any group the user is a member of
[1041]440        for group in self.storage.getUserGroups(user) :
441            grouppquota = self.storage.getGroupPQuota(group, printer)
442            if grouppquota.Exists :
443                action = self.checkGroupPQuota(grouppquota)
444                if action == "DENY" :
445                    return action
[927]446               
447        # then we check the user's own quota
[1152]448        # if we get there we are sure that policy is not EXTERNAL
449        (policy, dummy) = self.config.getPrinterPolicy(printer.Name)
[1061]450        if user.LimitBy and (user.LimitBy.lower() == "balance") : 
[1041]451            if user.AccountBalance is None :
[956]452                if policy == "ALLOW" :
[925]453                    action = "POLICY_ALLOW"
454                else :   
455                    action = "POLICY_DENY"
[1041]456                self.logger.log_message(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (user.Name, action, printer.Name))
[713]457            else :   
[1077]458                val = float(user.AccountBalance or 0.0)
459                if val <= 0.0 :
[925]460                    action = "DENY"
[1077]461                elif val <= self.config.getPoorMan() :   
462                    action = "WARN"
[925]463                else :   
[713]464                    action = "ALLOW"
[925]465        else :
[1041]466            if not userpquota.Exists :
467                # Unknown userquota
[956]468                if policy == "ALLOW" :
[925]469                    action = "POLICY_ALLOW"
[834]470                else :   
[925]471                    action = "POLICY_DENY"
[1041]472                self.logger.log_message(_("Unable to match user %s on printer %s, applying default policy (%s)") % (user.Name, printer.Name, action))
[925]473            else :   
[1041]474                pagecounter = int(userpquota.PageCounter or 0)
475                if userpquota.SoftLimit is not None :
476                    softlimit = int(userpquota.SoftLimit)
[925]477                    if pagecounter < softlimit :
478                        action = "ALLOW"
[834]479                    else :   
[1041]480                        if userpquota.HardLimit is None :
[925]481                            # only a soft limit, this is equivalent to having only a hard limit
482                            action = "DENY"
483                        else :   
[1041]484                            hardlimit = int(userpquota.HardLimit)
[925]485                            if softlimit <= pagecounter < hardlimit :   
486                                now = DateTime.now()
[1041]487                                if userpquota.DateLimit is not None :
488                                    datelimit = DateTime.ISO.ParseDateTime(userpquota.DateLimit)
[925]489                                else :
[1041]490                                    datelimit = now + self.config.getGraceDelay(printer.Name)
491                                    userpquota.setDateLimit(datelimit)
[925]492                                if now < datelimit :
493                                    action = "WARN"
494                                else :   
495                                    action = "DENY"
496                            else :         
[834]497                                action = "DENY"
[925]498                else :       
[1041]499                    if userpquota.HardLimit is not None :
[925]500                        # no soft limit, only a hard one.
[1041]501                        hardlimit = int(userpquota.HardLimit)
[925]502                        if pagecounter < hardlimit :
503                            action = "ALLOW"
504                        else :     
[742]505                            action = "DENY"
[925]506                    else :
507                        # Both are unset, no quota, i.e. accounting only
[834]508                        action = "ALLOW"
509        return action
[708]510   
[1192]511    def externalMailTo(self, cmd, action, user, printername, message) :
512        """Warns the user with an external command."""
513        username = user.Name
514        email = user.Email or user.Name
515        if "@" not in email :
516            email = "%s@%s" % (email, self.smtpserver)
517        os.system(cmd % locals())
518   
[1041]519    def warnGroupPQuota(self, grouppquota) :
[927]520        """Checks a group quota and send messages if quota is exceeded on current printer."""
[1041]521        group = grouppquota.Group
522        printer = grouppquota.Printer
523        admin = self.config.getAdmin(printer.Name)
524        adminmail = self.config.getAdminMail(printer.Name)
[1192]525        (mailto, arguments) = self.config.getMailTo(printer.Name)
[1068]526        action = self.checkGroupPQuota(grouppquota)
[927]527        if action.startswith("POLICY_") :
528            action = action[7:]
529        if action == "DENY" :
[1041]530            adminmessage = _("Print Quota exceeded for group %s on printer %s") % (group.Name, printer.Name)
[927]531            self.logger.log_message(adminmessage)
532            if mailto in [ "BOTH", "ADMIN" ] :
533                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
[1192]534            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
[1140]535                for user in self.storage.getGroupMembers(group) :
[1192]536                    if mailto != "EXTERNAL" :
537                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), self.config.getHardWarn(printer.Name))
538                    else :   
539                        self.externalMailTo(arguments, action, user, printer.Name, message)
[927]540        elif action == "WARN" :   
[1091]541            adminmessage = _("Print Quota low for group %s on printer %s") % (group.Name, printer.Name)
[927]542            self.logger.log_message(adminmessage)
543            if mailto in [ "BOTH", "ADMIN" ] :
544                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
[1077]545            if group.LimitBy and (group.LimitBy.lower() == "balance") : 
546                message = self.config.getPoorWarn()
547            else :     
548                message = self.config.getSoftWarn(printer.Name)
[1192]549            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
[1140]550                for user in self.storage.getGroupMembers(group) :
[1192]551                    if mailto != "EXTERNAL" :
552                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
553                    else :   
554                        self.externalMailTo(arguments, action, user, printer.Name, message)
[927]555        return action       
[728]556       
[1041]557    def warnUserPQuota(self, userpquota) :
[728]558        """Checks a user quota and send him a message if quota is exceeded on current printer."""
[1041]559        user = userpquota.User
560        printer = userpquota.Printer
561        admin = self.config.getAdmin(printer.Name)
562        adminmail = self.config.getAdminMail(printer.Name)
[1192]563        (mailto, arguments) = self.config.getMailTo(printer.Name)
[1041]564        action = self.checkUserPQuota(userpquota)
[834]565        if action.startswith("POLICY_") :
566            action = action[7:]
[695]567        if action == "DENY" :
[1041]568            adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
[834]569            self.logger.log_message(adminmessage)
[1192]570            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
571                message = self.config.getHardWarn(printer.Name)
572                if mailto != "EXTERNAL" :
573                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
574                else :   
575                    self.externalMailTo(arguments, action, user, printer.Name, message)
[852]576            if mailto in [ "BOTH", "ADMIN" ] :
577                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
[695]578        elif action == "WARN" :   
[1091]579            adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
[695]580            self.logger.log_message(adminmessage)
[1192]581            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
[1077]582                if user.LimitBy and (user.LimitBy.lower() == "balance") : 
583                    message = self.config.getPoorWarn()
584                else :     
585                    message = self.config.getSoftWarn(printer.Name)
[1192]586                if mailto != "EXTERNAL" :   
587                    self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
588                else :   
589                    self.externalMailTo(arguments, action, user, printer.Name, message)
[852]590            if mailto in [ "BOTH", "ADMIN" ] :
591                self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
[695]592        return action       
Note: See TracBrowser for help on using the browser.