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

Revision 3136, 35.0 kB (checked in by jerome, 17 years ago)

Fixed email headers when sending messages.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1# PyKota
2# -*- coding: ISO-8859-15 -*-
3
4# PyKota - Print Quotas for CUPS and LPRng
5#
6# (c) 2003, 2004, 2005, 2006, 2007 Jerome Alet <alet@librelogiciel.com>
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.
11#
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20#
21# $Id$
22#
23#
24
25import sys
26import os
27import pwd
28import fnmatch
29import getopt
30import smtplib
31import gettext
32import locale
33import socket
34import time
35from email.MIMEText import MIMEText
36from email.Header import Header
37import email.Utils
38
39from mx import DateTime
40
41try :
42    import chardet
43except ImportError :   
44    def detectCharset(text) :
45        """Fakes a charset detection if the chardet module is not installed."""
46        return "ISO-8859-15"
47else :   
48    def detectCharset(text) :
49        """Uses the chardet module to workaround CUPS lying to us."""
50        return chardet.detect(text)["encoding"]
51
52from pykota import config, storage, logger
53from pykota.version import __version__, __author__, __years__, __gplblurb__
54
55def N_(message) :
56    """Fake translation marker for translatable strings extraction."""
57    return message
58
59class PyKotaToolError(Exception):
60    """An exception for PyKota related stuff."""
61    def __init__(self, message = ""):
62        self.message = message
63        Exception.__init__(self, message)
64    def __repr__(self):
65        return self.message
66    __str__ = __repr__
67   
68class PyKotaCommandLineError(PyKotaToolError) :   
69    """An exception for Pykota command line tools."""
70    pass
71   
72def crashed(message="Bug in PyKota") :   
73    """Minimal crash method."""
74    import traceback
75    lines = []
76    for line in traceback.format_exception(*sys.exc_info()) :
77        lines.extend([l for l in line.split("\n") if l])
78    msg = "ERROR: ".join(["%s\n" % l for l in (["ERROR: PyKota v%s" % __version__, message] + lines)])
79    sys.stderr.write(msg)
80    sys.stderr.flush()
81    return msg
82
83class Percent :
84    """A class to display progress."""
85    def __init__(self, app, size=None) :
86        """Initializes the engine."""
87        self.app = app
88        self.size = None
89        if size :
90            self.setSize(size)
91        self.previous = None
92        self.before = time.time()
93       
94    def setSize(self, size) :     
95        """Sets the total size."""
96        self.number = 0
97        self.size = size
98        if size :
99            self.factor = 100.0 / float(size)
100       
101    def display(self, msg) :   
102        """Displays the value."""
103        self.app.display(msg)
104       
105    def oneMore(self) :   
106        """Increments internal counter."""
107        if self.size :
108            self.number += 1
109            percent = "%.02f" % (float(self.number) * self.factor)
110            if percent != self.previous : # optimize for large number of items
111                self.display("\r%s%%" % percent)
112                self.previous = percent
113           
114    def done(self) :         
115        """Displays the 'done' message."""
116        after = time.time()
117        if self.size :
118            speed = self.size / (after - self.before)
119            self.display("\r100.00%%\r        \r%s. %s : %.2f %s.\n" \
120                     % (_("Done"), _("Average speed"), speed, _("entries per second")))
121        else :             
122            self.display("\r100.00%%\r        \r%s.\n" % _("Done"))
123       
124class Tool :
125    """Base class for tools with no database access."""
126    def __init__(self, lang="", charset=None, doc="PyKota v%(__version__)s (c) %(__years__)s %(__author__)s") :
127        """Initializes the command line tool."""
128        # did we drop priviledges ?
129        self.privdropped = 0
130       
131        # locale stuff
132        try :
133            locale.setlocale(locale.LC_ALL, (lang, charset))
134        except (locale.Error, IOError) :
135            locale.setlocale(locale.LC_ALL, None)
136        (self.language, self.charset) = locale.getlocale()
137        self.language = self.language or "C"
138        try :
139            self.charset = self.charset or locale.getpreferredencoding()
140        except locale.Error :   
141            self.charset = sys.getfilesystemencoding()
142       
143        # translation stuff
144        try :
145            try :
146                trans = gettext.translation("pykota", languages=["%s.%s" % (self.language, self.charset)], codeset=self.charset)
147            except TypeError : # Python <2.4
148                trans = gettext.translation("pykota", languages=["%s.%s" % (self.language, self.charset)])
149            trans.install()
150        except :
151            gettext.NullTranslations().install()
152   
153        # pykota specific stuff
154        self.documentation = doc
155       
156    def deferredInit(self) :       
157        """Deferred initialization."""
158        confdir = os.environ.get("PYKOTA_HOME")
159        environHome = True
160        missingUser = False
161        if confdir is None :
162            environHome = False
163            # check for config files in the 'pykota' user's home directory.
164            try :
165                self.pykotauser = pwd.getpwnam("pykota")
166                confdir = self.pykotauser[5]
167            except KeyError :   
168                self.pykotauser = None
169                confdir = "/etc/pykota"
170                missingUser = True
171           
172        self.config = config.PyKotaConfig(confdir)
173        self.debug = self.config.getDebug()
174        self.smtpserver = self.config.getSMTPServer()
175        self.maildomain = self.config.getMailDomain()
176        self.logger = logger.openLogger(self.config.getLoggingBackend())
177           
178        # now drop priviledge if possible
179        self.dropPriv()   
180       
181        # We NEED this here, even when not in an accounting filter/backend   
182        self.softwareJobSize = 0
183        self.softwareJobPrice = 0.0
184       
185        if environHome :
186            self.printInfo("PYKOTA_HOME environment variable is set. Configuration files were searched in %s" % confdir, "info")
187        else :
188            if missingUser :     
189                self.printInfo("The 'pykota' system account is missing. Configuration files were searched in %s instead." % confdir, "warn")
190       
191        self.logdebug("Language in use : %s" % self.language)
192        self.logdebug("Charset in use : %s" % self.charset)
193       
194        arguments = " ".join(['"%s"' % arg for arg in sys.argv])
195        self.logdebug("Command line arguments : %s" % arguments)
196       
197    def dropPriv(self) :   
198        """Drops priviledges."""
199        uid = os.geteuid()
200        try :
201            self.originalUserName = pwd.getpwuid(uid)[0]
202        except (KeyError, IndexError), msg :   
203            self.printInfo(_("Strange problem with uid(%s) : %s") % (uid, msg), "warn")
204            self.originalUserName = None
205        else :
206            if uid :
207                self.logdebug(_("Running as user '%s'.") % self.originalUserName)
208            else :
209                if self.pykotauser is None :
210                    self.logdebug(_("No user named 'pykota'. Not dropping priviledges."))
211                else :   
212                    try :
213                        os.setegid(self.pykotauser[3])
214                        os.seteuid(self.pykotauser[2])
215                    except OSError, msg :   
216                        self.printInfo(_("Impossible to drop priviledges : %s") % msg, "warn")
217                    else :   
218                        self.logdebug(_("Priviledges dropped. Now running as user 'pykota'."))
219                        self.privdropped = 1
220           
221    def regainPriv(self) :   
222        """Drops priviledges."""
223        if self.privdropped :
224            try :
225                os.seteuid(0)
226                os.setegid(0)
227            except OSError, msg :   
228                self.printInfo(_("Impossible to regain priviledges : %s") % msg, "warn")
229            else :   
230                self.logdebug(_("Regained priviledges. Now running as root."))
231                self.privdropped = 0
232       
233    def UTF8ToUserCharset(self, text) :
234        """Converts from UTF-8 to user's charset."""
235        if text is None :
236            return None
237        try :
238            return text.decode("UTF-8").encode(self.charset, "replace") 
239        except (UnicodeError, AttributeError) :   
240            try :
241                # Maybe already in Unicode ?
242                return text.encode(self.charset, "replace") 
243            except (UnicodeError, AttributeError) :
244                # Try to autodetect the charset
245                return text.decode(detectCharset(text), "replace").encode(self.charset, "replace")
246       
247    def userCharsetToUTF8(self, text) :
248        """Converts from user's charset to UTF-8."""
249        if text is None :
250            return None
251        try :
252            # We don't necessarily trust the default charset, because
253            # xprint sends us titles in UTF-8 but CUPS gives us an ISO-8859-1 charset !
254            # So we first try to see if the text is already in UTF-8 or not, and
255            # if it is, we delete characters which can't be converted to the user's charset,
256            # then convert back to UTF-8. PostgreSQL 7.3.x used to reject some unicode characters,
257            # this is fixed by the ugly line below :
258            return text.decode("UTF-8").encode(self.charset, "replace").decode(self.charset).encode("UTF-8", "replace")
259        except (UnicodeError, AttributeError) :
260            try :
261                return text.decode(self.charset).encode("UTF-8", "replace") 
262            except (UnicodeError, AttributeError) :   
263                try :
264                    # Maybe already in Unicode ?
265                    return text.encode("UTF-8", "replace") 
266                except (UnicodeError, AttributeError) :
267                    # Try to autodetect the charset
268                    return text.decode(detectCharset(text), "replace").encode("UTF-8", "replace")
269        return newtext
270       
271    def display(self, message) :
272        """Display a message but only if stdout is a tty."""
273        if sys.stdout.isatty() :
274            sys.stdout.write(message)
275            sys.stdout.flush()
276           
277    def logdebug(self, message) :   
278        """Logs something to debug output if debug is enabled."""
279        if self.debug :
280            self.logger.log_message(message, "debug")
281           
282    def printInfo(self, message, level="info") :       
283        """Sends a message to standard error."""
284        sys.stderr.write("%s: %s\n" % (level.upper(), message))
285        sys.stderr.flush()
286       
287    def matchString(self, s, patterns) :
288        """Returns True if the string s matches one of the patterns, else False."""
289        if not patterns :
290            return True # No pattern, always matches.
291        else :   
292            for pattern in patterns :
293                if fnmatch.fnmatchcase(s, pattern) :
294                    return True
295            return False
296       
297    def sanitizeNames(self, options, names) :
298        """Ensures that an user can only see the datas he is allowed to see, by modifying the list of names."""
299        if not self.config.isAdmin :
300            username = pwd.getpwuid(os.geteuid())[0]
301            if not options["list"] :
302                raise PyKotaCommandLineError, "%s : %s" % (username, _("You're not allowed to use this command."))
303            else :
304                if options["groups"] :
305                    user = self.storage.getUser(username)
306                    if user.Exists :
307                        return [ g.Name for g in self.storage.getUserGroups(user) ]
308                return [ username ]
309        elif not names :       
310            return ["*"]
311        else :   
312            return names
313       
314    def display_version_and_quit(self) :
315        """Displays version number, then exists successfully."""
316        try :
317            self.clean()
318        except AttributeError :   
319            pass
320        print __version__
321        sys.exit(0)
322   
323    def display_usage_and_quit(self) :
324        """Displays command line usage, then exists successfully."""
325        try :
326            self.clean()
327        except AttributeError :   
328            pass
329        print _(self.documentation) % globals()
330        print __gplblurb__
331        print
332        print _("Please report bugs to :"), __author__
333        sys.exit(0)
334       
335    def crashed(self, message="Bug in PyKota") :   
336        """Outputs a crash message, and optionally sends it to software author."""
337        msg = crashed(message)
338        fullmessage = "========== Traceback :\n\n%s\n\n========== sys.argv :\n\n%s\n\n========== Environment :\n\n%s\n" % \
339                        (msg, \
340                         "\n".join(["    %s" % repr(a) for a in sys.argv]), \
341                         "\n".join(["    %s=%s" % (k, v) for (k, v) in os.environ.items()]))
342        try :
343            crashrecipient = self.config.getCrashRecipient()
344            if crashrecipient :
345                admin = self.config.getAdminMail("global") # Nice trick, isn't it ?
346                server = smtplib.SMTP(self.smtpserver)
347                msg = MIMEText(fullmessage, _charset=self.charset)
348                msg["Subject"] = Header("PyKota v%s crash traceback !" \
349                                        % __version__, charset=self.charset)
350                msg["From"] = admin
351                msg["To"] = crashrecipient
352                msg["Cc"] = admin
353                msg["Date"] = email.Utils.formatdate(localtime=True)
354                server.sendmail(admin, [admin, crashrecipient], msg.as_string())
355                server.quit()
356        except :
357            pass
358        return fullmessage   
359       
360    def parseCommandline(self, argv, short, long, allownothing=0) :
361        """Parses the command line, controlling options."""
362        # split options in two lists: those which need an argument, those which don't need any
363        short = "%sA:" % short
364        long.append("arguments=")
365        withoutarg = []
366        witharg = []
367        lgs = len(short)
368        i = 0
369        while i < lgs :
370            ii = i + 1
371            if (ii < lgs) and (short[ii] == ':') :
372                # needs an argument
373                witharg.append(short[i])
374                ii = ii + 1 # skip the ':'
375            else :
376                # doesn't need an argument
377                withoutarg.append(short[i])
378            i = ii
379               
380        for option in long :
381            if option[-1] == '=' :
382                # needs an argument
383                witharg.append(option[:-1])
384            else :
385                # doesn't need an argument
386                withoutarg.append(option)
387       
388        # then we parse the command line
389        done = 0
390        while not done :
391            # we begin with all possible options unset
392            parsed = {}
393            for option in withoutarg + witharg :
394                parsed[option] = None
395            args = []       # to not break if something unexpected happened
396            try :
397                options, args = getopt.getopt(argv, short, long)
398                if options :
399                    for (o, v) in options :
400                        # we skip the '-' chars
401                        lgo = len(o)
402                        i = 0
403                        while (i < lgo) and (o[i] == '-') :
404                            i = i + 1
405                        o = o[i:]
406                        if o in witharg :
407                            # needs an argument : set it
408                            parsed[o] = v
409                        elif o in withoutarg :
410                            # doesn't need an argument : boolean
411                            parsed[o] = 1
412                        else :
413                            # should never occur
414                            raise PyKotaCommandLineError, "Unexpected problem when parsing command line"
415                elif (not args) and (not allownothing) and sys.stdin.isatty() : # no option and no argument, we display help if we are a tty
416                    self.display_usage_and_quit()
417            except getopt.error, msg :
418                raise PyKotaCommandLineError, str(msg)
419            else :   
420                if parsed["arguments"] or parsed["A"] :
421                    # arguments are in a file, we ignore all other arguments
422                    # and reset the list of arguments to the lines read from
423                    # the file.
424                    argsfile = open(parsed["arguments"] or parsed["A"], "r")
425                    argv = [ l.strip() for l in argsfile.readlines() ]
426                    argsfile.close()
427                    for i in range(len(argv)) :
428                        argi = argv[i]
429                        if argi.startswith('"') and argi.endswith('"') :
430                            argv[i] = argi[1:-1]
431                else :   
432                    done = 1
433        return (parsed, args)
434   
435class PyKotaTool(Tool) :   
436    """Base class for all PyKota command line tools."""
437    def __init__(self, lang="", charset=None, doc="PyKota v%(__version__)s (c) %(__years__)s %(__author__)s") :
438        """Initializes the command line tool and opens the database."""
439        Tool.__init__(self, lang, charset, doc)
440       
441    def deferredInit(self) :   
442        """Deferred initialization."""
443        Tool.deferredInit(self)
444        self.storage = storage.openConnection(self)
445        if self.config.isAdmin : # TODO : We don't know this before, fix this !
446            self.logdebug("Beware : running as a PyKota administrator !")
447        else :   
448            self.logdebug("Don't Panic : running as a mere mortal !")
449       
450    def clean(self) :   
451        """Ensures that the database is closed."""
452        try :
453            self.storage.close()
454        except (TypeError, NameError, AttributeError) :   
455            pass
456           
457    def isValidName(self, name) :
458        """Checks if a user or printer name is valid."""
459        invalidchars = "/@?*,;&|"
460        for c in list(invalidchars) :
461            if c in name :
462                return 0
463        return 1       
464       
465    def sendMessage(self, adminmail, touser, fullmessage) :
466        """Sends an email message containing headers to some user."""
467        try :   
468            server = smtplib.SMTP(self.smtpserver)
469        except socket.error, msg :   
470            self.printInfo(_("Impossible to connect to SMTP server : %s") % msg, "error")
471        else :
472            try :
473                server.sendmail(adminmail, [touser], fullmessage)
474            except smtplib.SMTPException, answer :   
475                for (k, v) in answer.recipients.items() :
476                    self.printInfo(_("Impossible to send mail to %s, error %s : %s") % (k, v[0], v[1]), "error")
477            server.quit()
478       
479    def sendMessageToUser(self, admin, adminmail, user, subject, message) :
480        """Sends an email message to a user."""
481        message += _("\n\nPlease contact your system administrator :\n\n\t%s - <%s>\n") % (admin, adminmail)
482        usermail = user.Email or user.Name
483        if "@" not in usermail :
484            usermail = "%s@%s" % (usermail, self.maildomain or self.smtpserver or "localhost")
485        msg = MIMEText(message, _charset=self.charset)
486        msg["Subject"] = Header(subject, charset=self.charset)
487        msg["From"] = adminmail
488        msg["To"] = usermail
489        msg["Date"] = email.Utils.formatdate(localtime=True)
490        self.sendMessage(adminmail, usermail, msg.as_string())
491       
492    def sendMessageToAdmin(self, adminmail, subject, message) :
493        """Sends an email message to the Print Quota administrator."""
494        if "@" not in adminmail :
495            adminmail = "%s@%s" % (adminmail, self.maildomain or self.smtpserver or "localhost")
496        msg = MIMEText(message, _charset=self.charset)
497        msg["Subject"] = Header(subject, charset=self.charset)
498        msg["From"] = adminmail
499        msg["To"] = adminmail
500        self.sendMessage(adminmail, adminmail, msg.as_string())
501       
502    def _checkUserPQuota(self, userpquota) :           
503        """Checks the user quota on a printer and deny or accept the job."""
504        # then we check the user's own quota
505        # if we get there we are sure that policy is not EXTERNAL
506        user = userpquota.User
507        printer = userpquota.Printer
508        enforcement = self.config.getPrinterEnforcement(printer.Name)
509        self.logdebug("Checking user %s's quota on printer %s" % (user.Name, printer.Name))
510        (policy, dummy) = self.config.getPrinterPolicy(userpquota.Printer.Name)
511        if not userpquota.Exists :
512            # Unknown userquota
513            if policy == "ALLOW" :
514                action = "POLICY_ALLOW"
515            else :   
516                action = "POLICY_DENY"
517            self.printInfo(_("Unable to match user %s on printer %s, applying default policy (%s)") % (user.Name, printer.Name, action))
518        else :   
519            pagecounter = int(userpquota.PageCounter or 0)
520            if enforcement == "STRICT" :
521                pagecounter += self.softwareJobSize
522            if userpquota.SoftLimit is not None :
523                softlimit = int(userpquota.SoftLimit)
524                if pagecounter < softlimit :
525                    action = "ALLOW"
526                else :   
527                    if userpquota.HardLimit is None :
528                        # only a soft limit, this is equivalent to having only a hard limit
529                        action = "DENY"
530                    else :   
531                        hardlimit = int(userpquota.HardLimit)
532                        if softlimit <= pagecounter < hardlimit :   
533                            now = DateTime.now()
534                            if userpquota.DateLimit is not None :
535                                datelimit = DateTime.ISO.ParseDateTime(str(userpquota.DateLimit)[:19])
536                            else :
537                                datelimit = now + self.config.getGraceDelay(printer.Name)
538                                userpquota.setDateLimit(datelimit)
539                            if now < datelimit :
540                                action = "WARN"
541                            else :   
542                                action = "DENY"
543                        else :         
544                            action = "DENY"
545            else :       
546                if userpquota.HardLimit is not None :
547                    # no soft limit, only a hard one.
548                    hardlimit = int(userpquota.HardLimit)
549                    if pagecounter < hardlimit :
550                        action = "ALLOW"
551                    else :     
552                        action = "DENY"
553                else :
554                    # Both are unset, no quota, i.e. accounting only
555                    action = "ALLOW"
556        return action
557   
558    def checkGroupPQuota(self, grouppquota) :   
559        """Checks the group quota on a printer and deny or accept the job."""
560        group = grouppquota.Group
561        printer = grouppquota.Printer
562        enforcement = self.config.getPrinterEnforcement(printer.Name)
563        self.logdebug("Checking group %s's quota on printer %s" % (group.Name, printer.Name))
564        if group.LimitBy and (group.LimitBy.lower() == "balance") : 
565            val = group.AccountBalance or 0.0
566            if enforcement == "STRICT" : 
567                val -= self.softwareJobPrice # use precomputed size.
568            balancezero = self.config.getBalanceZero()
569            if val <= balancezero :
570                action = "DENY"
571            elif val <= self.config.getPoorMan() :   
572                action = "WARN"
573            else :   
574                action = "ALLOW"
575            if (enforcement == "STRICT") and (val == balancezero) :
576                action = "WARN" # we can still print until account is 0
577        else :
578            val = grouppquota.PageCounter or 0
579            if enforcement == "STRICT" :
580                val += int(self.softwareJobSize) # TODO : this is not a fix, problem is elsewhere in grouppquota.PageCounter
581            if grouppquota.SoftLimit is not None :
582                softlimit = int(grouppquota.SoftLimit)
583                if val < softlimit :
584                    action = "ALLOW"
585                else :   
586                    if grouppquota.HardLimit is None :
587                        # only a soft limit, this is equivalent to having only a hard limit
588                        action = "DENY"
589                    else :   
590                        hardlimit = int(grouppquota.HardLimit)
591                        if softlimit <= val < hardlimit :   
592                            now = DateTime.now()
593                            if grouppquota.DateLimit is not None :
594                                datelimit = DateTime.ISO.ParseDateTime(str(grouppquota.DateLimit)[:19])
595                            else :
596                                datelimit = now + self.config.getGraceDelay(printer.Name)
597                                grouppquota.setDateLimit(datelimit)
598                            if now < datelimit :
599                                action = "WARN"
600                            else :   
601                                action = "DENY"
602                        else :         
603                            action = "DENY"
604            else :       
605                if grouppquota.HardLimit is not None :
606                    # no soft limit, only a hard one.
607                    hardlimit = int(grouppquota.HardLimit)
608                    if val < hardlimit :
609                        action = "ALLOW"
610                    else :     
611                        action = "DENY"
612                else :
613                    # Both are unset, no quota, i.e. accounting only
614                    action = "ALLOW"
615        return action
616   
617    def checkUserPQuota(self, userpquota) :
618        """Checks the user quota on a printer and all its parents and deny or accept the job."""
619        user = userpquota.User
620        printer = userpquota.Printer
621       
622        # indicates that a warning needs to be sent
623        warned = 0               
624       
625        # first we check any group the user is a member of
626        for group in self.storage.getUserGroups(user) :
627            # No need to check anything if the group is in noquota mode
628            if group.LimitBy != "noquota" :
629                grouppquota = self.storage.getGroupPQuota(group, printer)
630                # for the printer and all its parents
631                for gpquota in [ grouppquota ] + grouppquota.ParentPrintersGroupPQuota :
632                    if gpquota.Exists :
633                        action = self.checkGroupPQuota(gpquota)
634                        if action == "DENY" :
635                            return action
636                        elif action == "WARN" :   
637                            warned = 1
638                       
639        # Then we check the user's account balance
640        # if we get there we are sure that policy is not EXTERNAL
641        (policy, dummy) = self.config.getPrinterPolicy(printer.Name)
642        if user.LimitBy and (user.LimitBy.lower() == "balance") : 
643            self.logdebug("Checking account balance for user %s" % user.Name)
644            if user.AccountBalance is None :
645                if policy == "ALLOW" :
646                    action = "POLICY_ALLOW"
647                else :   
648                    action = "POLICY_DENY"
649                self.printInfo(_("Unable to find user %s's account balance, applying default policy (%s) for printer %s") % (user.Name, action, printer.Name))
650                return action       
651            else :   
652                if user.OverCharge == 0.0 :
653                    self.printInfo(_("User %s will not be charged for printing.") % user.Name)
654                    action = "ALLOW"
655                else :
656                    val = float(user.AccountBalance or 0.0)
657                    enforcement = self.config.getPrinterEnforcement(printer.Name)
658                    if enforcement == "STRICT" : 
659                        val -= self.softwareJobPrice # use precomputed size.
660                    balancezero = self.config.getBalanceZero()   
661                    if val <= balancezero :
662                        action = "DENY"
663                    elif val <= self.config.getPoorMan() :   
664                        action = "WARN"
665                    else :
666                        action = "ALLOW"
667                    if (enforcement == "STRICT") and (val == balancezero) :
668                        action = "WARN" # we can still print until account is 0
669                return action   
670        else :
671            # Then check the user quota on current printer and all its parents.               
672            policyallowed = 0
673            for upquota in [ userpquota ] + userpquota.ParentPrintersUserPQuota :               
674                action = self._checkUserPQuota(upquota)
675                if action in ("DENY", "POLICY_DENY") :
676                    return action
677                elif action == "WARN" :   
678                    warned = 1
679                elif action == "POLICY_ALLOW" :   
680                    policyallowed = 1
681            if warned :       
682                return "WARN"
683            elif policyallowed :   
684                return "POLICY_ALLOW" 
685            else :   
686                return "ALLOW"
687               
688    def externalMailTo(self, cmd, action, user, printer, message) :
689        """Warns the user with an external command."""
690        username = user.Name
691        printername = printer.Name
692        email = user.Email or user.Name
693        if "@" not in email :
694            email = "%s@%s" % (email, self.maildomain or self.smtpserver)
695        os.system(cmd % locals())
696   
697    def formatCommandLine(self, cmd, user, printer) :
698        """Executes an external command."""
699        username = user.Name
700        printername = printer.Name
701        return cmd % locals()
702       
703    def warnGroupPQuota(self, grouppquota) :
704        """Checks a group quota and send messages if quota is exceeded on current printer."""
705        group = grouppquota.Group
706        printer = grouppquota.Printer
707        admin = self.config.getAdmin(printer.Name)
708        adminmail = self.config.getAdminMail(printer.Name)
709        (mailto, arguments) = self.config.getMailTo(printer.Name)
710        if group.LimitBy in ("noquota", "nochange") :
711            action = "ALLOW"
712        else :   
713            action = self.checkGroupPQuota(grouppquota)
714            if action.startswith("POLICY_") :
715                action = action[7:]
716            if action == "DENY" :
717                adminmessage = _("Print Quota exceeded for group %s on printer %s") % (group.Name, printer.Name)
718                self.printInfo(adminmessage)
719                if mailto in [ "BOTH", "ADMIN" ] :
720                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
721                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
722                    for user in self.storage.getGroupMembers(group) :
723                        if mailto != "EXTERNAL" :
724                            self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), self.config.getHardWarn(printer.Name))
725                        else :   
726                            self.externalMailTo(arguments, action, user, printer, self.config.getHardWarn(printer.Name))
727            elif action == "WARN" :   
728                adminmessage = _("Print Quota low for group %s on printer %s") % (group.Name, printer.Name)
729                self.printInfo(adminmessage)
730                if mailto in [ "BOTH", "ADMIN" ] :
731                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
732                if group.LimitBy and (group.LimitBy.lower() == "balance") : 
733                    message = self.config.getPoorWarn()
734                else :     
735                    message = self.config.getSoftWarn(printer.Name)
736                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
737                    for user in self.storage.getGroupMembers(group) :
738                        if mailto != "EXTERNAL" :
739                            self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
740                        else :   
741                            self.externalMailTo(arguments, action, user, printer, message)
742        return action       
743       
744    def warnUserPQuota(self, userpquota) :
745        """Checks a user quota and send him a message if quota is exceeded on current printer."""
746        user = userpquota.User
747        printer = userpquota.Printer
748        admin = self.config.getAdmin(printer.Name)
749        adminmail = self.config.getAdminMail(printer.Name)
750        (mailto, arguments) = self.config.getMailTo(printer.Name)
751       
752        if user.LimitBy in ("noquota", "nochange") :
753            action = "ALLOW"
754        elif user.LimitBy == "noprint" :
755            action = "DENY"
756            message = _("User %s is not allowed to print at this time.") % user.Name
757            self.printInfo(message)
758            if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
759                if mailto != "EXTERNAL" :
760                    self.sendMessageToUser(admin, adminmail, user, _("Printing denied."), message)
761                else :   
762                    self.externalMailTo(arguments, action, user, printer, message)
763            if mailto in [ "BOTH", "ADMIN" ] :
764                self.sendMessageToAdmin(adminmail, _("Print Quota"), message)
765        else :
766            action = self.checkUserPQuota(userpquota)
767            if action.startswith("POLICY_") :
768                action = action[7:]
769               
770            if action == "DENY" :
771                adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
772                self.printInfo(adminmessage)
773                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
774                    message = self.config.getHardWarn(printer.Name)
775                    if mailto != "EXTERNAL" :
776                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Exceeded"), message)
777                    else :   
778                        self.externalMailTo(arguments, action, user, printer, message)
779                if mailto in [ "BOTH", "ADMIN" ] :
780                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
781            elif action == "WARN" :   
782                adminmessage = _("Print Quota low for user %s on printer %s") % (user.Name, printer.Name)
783                self.printInfo(adminmessage)
784                if mailto in [ "BOTH", "USER", "EXTERNAL" ] :
785                    if user.LimitBy and (user.LimitBy.lower() == "balance") : 
786                        message = self.config.getPoorWarn()
787                    else :     
788                        message = self.config.getSoftWarn(printer.Name)
789                    if mailto != "EXTERNAL" :   
790                        self.sendMessageToUser(admin, adminmail, user, _("Print Quota Low"), message)
791                    else :   
792                        self.externalMailTo(arguments, action, user, printer, message)
793                if mailto in [ "BOTH", "ADMIN" ] :
794                    self.sendMessageToAdmin(adminmail, _("Print Quota"), adminmessage)
795        return action       
796       
Note: See TracBrowser for help on using the browser.