Package rrlog :: Package contrib :: Module mail
[hide private]
[frames] | no frames]

Source Code for Module rrlog.contrib.mail

  1  #Copyright (c) 2007 
  2  #        Ruben Reifenberg, Germany, 07381. 
  3  #    All rights reserved. 
  4  # 
  5  #Redistribution and use in source and binary forms, with or without 
  6  #modification, are permitted provided that the following conditions 
  7  #are met: 
  8  #1. Redistributions of source code must retain the above copyright 
  9  #   notice, this list of conditions and the following disclaimer as 
 10  #   the first lines of this file unmodified. 
 11  #2. Redistributions in binary form must reproduce the above copyright 
 12  #   notice, this list of conditions and the following disclaimer in the 
 13  #   documentation and/or other materials provided with the distribution. 
 14  # 
 15  # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 
 16  # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 17  # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 18  # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 
 19  # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
 20  # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 
 21  # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 22  # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 23  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 
 24  # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
 25  # SUCH DAMAGE. 
 26   
 27  """ 
 28  @summary: Sends emails with a "digest" of selected messages 
 29  @author: Ruben Reifenberg 
 30  """ 
 31   
 32  from threading import Thread, currentThread, RLock, enumerate as threading_enumerate 
 33  from time import sleep 
 34  from datetime import datetime,timedelta 
 35   
 36  from rrlog.tool import mail_smtp 
 37  from rrlog.server.textwriter import default_format_line 
 38   
 39   
 40  _spoolers = [] 
 41   
42 -def stop_all():
43 """ 44 Stops all running spooler threads. 45 """ 46 for spooler in _spoolers: 47 spooler.stop()
48
49 -def now(): return datetime.now()
50
51 -def create_observer_smtp(server, serverpw, from_address, to_address, rules, format_line=None, subject="Log Digest", charset="latin-1", spooler_autostop=True):
52 """ 53 Creates a mail notifier with a background worker thread. 54 Convenience method which sets default values appropriate for many use cases. 55 Appends the spooler to the modules spoolers list so that the L{stop_all} function can stop it. 56 @param rules: see L{CatBuffer.__init__} 57 @param spooler_autostop: see L{Spooler.__init__}. Defaults to True, so that explicit stop of the spooler threads is not necessary usually. 58 @return: An observer object, intended to be added into the observers list of the log. 59 """ 60 buffer = CatBuffer(rules=rules, format_line=format_line) 61 mailer = SMTPMailer(server=server, serverpw=serverpw, from_address=from_address, to_address=to_address, default_subject=subject, charset=charset) 62 _spoolers.append( 63 Spooler( 64 mailer=mailer, 65 buffer=buffer, 66 autostop=spooler_autostop, 67 ) 68 ) 69 return buffer
70 71 72
73 -class CatRule(object):
74 - def __init__(self, cats, max_delay_secs=0):
75 """ 76 @param max_delay_secs: Maximum time that a message of that categories can be buffered before sending. 77 High values result in less (but bigger) mails. 78 0 == send each line nearly immediately 79 (Note: because the worker thread pauses frequently, 80 we have still a littly delay, so that still multiple messages might be collected per mail.) 81 @param cats: tuple of cats to catch, e.g. ("E","S"). 82 None == catch any category (could cause high email traffic) 83 """ 84 assert not isinstance(cats,str),"cats must be a tuple/list of cat, not '%s'"%(cats) 85 self._cats = cats 86 self._max_delay_secs = max_delay_secs
87
88 - def max_delay_secs(self, job):
89 """ 90 @return: Max.remaining buffering time for that job. None if rule does not apply. 91 """ 92 if self._cats is None or job.cat in self._cats: 93 return self._max_delay_secs 94 else: 95 return None
96
97 - def __str__(self):
98 return "%s[%s:%s secs]"%(self.__class__.__name__,str(self._cats),self._max_delay_secs)
99 100 101
102 -class Spooler(object):
103 """ 104 """
105 - def __init__(self, mailer, buffer, pauseSecs=10, autostop=False):
106 """ 107 Starts a worker thread which reads buffered lines and sends them when their time has come. 108 @param autostop: 109 When True, the worker thread will check the number of non-daemon threads after each work interval. 110 When only one (itself) is left, it stops automatically. 111 But when there are more threads doing the same trick, explicit stop is required. 112 """ 113 self._mailer = mailer 114 self._buffer = buffer 115 self._pauseSecs = pauseSecs 116 self._autostop = autostop 117 self._stopped = False 118 Thread(target=self.run).start()
119
120 - def _i_am_orphan(self):
121 current = currentThread() 122 for thread in threading_enumerate(): 123 if not thread.isDaemon() and (thread is not current): 124 return False 125 return True
126
127 - def _work(self):
128 if self._buffer.deadline_reached(): 129 lines = tuple(self._buffer.iter_all_and_clear()) 130 self._mailer.mail(lines)
131
132 - def run(self):
133 while not self._stopped: 134 sleep(self._pauseSecs) 135 self._work() 136 if self._autostop and self._i_am_orphan(): 137 self.stop() 138 self._work()
139
140 - def stop(self):
141 """ 142 Sets my stop flag, causing the worker thread to finish after its next working interval. 143 (All remaining buffered lines are sent before exit.) 144 """ 145 self._stopped = True
146
147 - def is_active(self):
148 return not self._stopped
149 150 151
152 -class CatBuffer(object):
153 - def __init__(self, rules, format_line=None):
154 """ 155 @param rules: list of rules which decide about the log jobs to send. Use e.g. a tuple of L{CatRule} objects. 156 If empty list, no messages are buffered. 157 The rules are processed in the given order. If a rule applies, the following rules in the list are ignored. 158 """ 159 assert hasattr(rules, "__iter__"), "rules must be iterable, not:%s (althought empty list is allowed)"%(type(rules)) 160 if format_line is None: format_line = default_format_line 161 self._format_line = (format_line,) 162 self._lines = [] 163 self._rules = rules 164 self._deadline = None 165 self._rlock = RLock()
166
167 - def _calc_deadline(self, secs_remaining):
168 return now() + timedelta(seconds=secs_remaining)
169
170 - def __call__(self, jobhist, writer):
171 """ 172 The current implementation locks the current thread if a rule applies, to safely buffer it. 173 """ 174 currentJob = jobhist[-1] 175 for rule in self._rules: 176 secs = rule.max_delay_secs(currentJob) 177 if secs is not None: 178 self._rlock.acquire() 179 self._buffer(currentJob, secs) 180 self._rlock.release() 181 return
182
183 - def _buffer(self, job, secs):
184 self._lines.append( 185 self._format_line[0](job) 186 ) 187 deadline = self._calc_deadline(secs) 188 if self._deadline is None or deadline < self._deadline: 189 self._deadline = deadline
190
191 - def iter_all_and_clear(self):
192 self._rlock.acquire() 193 for x in self._lines: 194 yield x 195 self._lines = [] 196 self._deadline = None 197 self._rlock.release()
198 199
200 - def deadline_reached(self):
201 return (self._deadline is not None) and (now() >= self._deadline)
202
203 - def size(self):
204 return len(self._lines)
205 206
207 -class SMTPMailer(object):
208 - def __init__(self, server, serverpw, to_address, from_address, default_subject, charset="latin-1"):
209 self._server = server 210 self._serverpw = serverpw 211 self._to_address = to_address 212 self._from_address = from_address 213 self._default_subject = default_subject 214 self._attach = False # may allow to attach everything as text file 215 self._charset = charset
216 217
218 - def _subject(self):
219 """ 220 Override to dynamically create a subject 221 """ 222 return self._default_subject
223
224 - def _content(self, lines):
225 def lines_(): 226 for line in lines: 227 yield line+"\n"
228 return "".join(lines_())
229
230 - def mail(self, lines):
231 mail_smtp( 232 server = self._server, 233 serverpw = self._serverpw, 234 to_address = self._to_address, 235 from_address = self._from_address, 236 subject = self._subject(), 237 content = self._content(lines), 238 charset = self._charset, 239 )
240