1 | |
---|
2 | import threading |
---|
3 | import time |
---|
4 | import XMailPermissions |
---|
5 | |
---|
6 | from trac import __version__ |
---|
7 | |
---|
8 | from trac.core import * |
---|
9 | from trac.web.api import ITemplateStreamFilter |
---|
10 | from trac.notification import SmtpEmailSender, IEmailSender, NotifyEmail,\ |
---|
11 | NotificationSystem |
---|
12 | from threading import Thread |
---|
13 | from trac.util.datefmt import to_datetime, format_datetime, utc |
---|
14 | from trac.ticket.api import TicketSystem |
---|
15 | from xmail.XMailFilterObject import FilterObject |
---|
16 | from trac.loader import get_plugin_info |
---|
17 | from trac.ticket.notification import TicketNotifyEmail |
---|
18 | from trac.ticket.model import Ticket |
---|
19 | from trac.util.translation import domain_functions, get_available_locales,\ |
---|
20 | activate |
---|
21 | from traceback import print_exc, format_tb |
---|
22 | import pkg_resources |
---|
23 | from trac.web.main import RequestDispatcher |
---|
24 | from trac.util import translation |
---|
25 | from trac.prefs.web_ui import Locale |
---|
26 | import sys |
---|
27 | from trac.util.text import to_unicode |
---|
28 | from trac.config import IntOption |
---|
29 | |
---|
30 | _, tag_, N_, add_domain = domain_functions('xmail', '_', 'tag_', 'N_', 'add_domain') |
---|
31 | |
---|
32 | SEC_MULTIPLIER = 1000000 |
---|
33 | |
---|
34 | #=============================================================================== |
---|
35 | # Module which handels the email notification logic |
---|
36 | #=============================================================================== |
---|
37 | class XMailEventHandler(Component): |
---|
38 | implements(ITemplateStreamFilter) |
---|
39 | |
---|
40 | """ |
---|
41 | Email Modul, required |
---|
42 | """ |
---|
43 | _locale_string = 'en' |
---|
44 | |
---|
45 | def __init__(self): |
---|
46 | self.log.debug( "+++++++++++++++ init EMailEventHandler" ) |
---|
47 | locale_dir = pkg_resources.resource_filename(__name__, 'locale') |
---|
48 | add_domain(self.env.path, locale_dir) |
---|
49 | |
---|
50 | |
---|
51 | def filter_stream(self, req, method, filename, stream, data): |
---|
52 | if not self._TimerIsStillAlive(): |
---|
53 | # copied from main.py:310ff |
---|
54 | available = [locale_id.replace('_', '-') for locale_id in |
---|
55 | translation.get_available_locales()] |
---|
56 | |
---|
57 | preferred = req.session.get('language', req.languages) |
---|
58 | if not isinstance(preferred, list): |
---|
59 | preferred = [preferred] |
---|
60 | self._locale_string = Locale.negotiate(preferred, available, sep='-') |
---|
61 | self.log.debug("Negotiated locale: %s -> %s", |
---|
62 | preferred, self._locale_string) |
---|
63 | |
---|
64 | return stream |
---|
65 | |
---|
66 | #=============================================================================== |
---|
67 | # Default Config |
---|
68 | #=============================================================================== |
---|
69 | DEFAULT_SLEEP_TIME = IntOption('xmail-plugin', 'sleeptime', 120, |
---|
70 | """Sleep time in seconds for thread. |
---|
71 | This is the time which determines how often the filter should be checked.""") |
---|
72 | currentTimeInMicroSec = 0 |
---|
73 | |
---|
74 | |
---|
75 | #=========================================================================== |
---|
76 | # Controll if the timer thread is still running |
---|
77 | #=========================================================================== |
---|
78 | def _TimerIsStillAlive(self): |
---|
79 | threadList = threading.enumerate() |
---|
80 | for threadOb in threadList: |
---|
81 | # if threadOb.__class__.__dict__.has_key('name') and threadOb.name == "xmailThread": |
---|
82 | if threadOb.getName() == "xmailThread": |
---|
83 | if threadOb.isAlive() == False: |
---|
84 | self._createXMailThread() |
---|
85 | self.log.warn('_TimerIsStillAlive: Thread has not been alive; started new Thread') |
---|
86 | return False |
---|
87 | return True # return since it has already a Thread |
---|
88 | self.log.info("[_TimerIsStillAlive] have not found any thread named 'xmailThread'") |
---|
89 | # self.log.debug('[_TimerIsStillAlive] started Thread') |
---|
90 | self._createXMailThread() |
---|
91 | return False |
---|
92 | |
---|
93 | def _createXMailThread(self): |
---|
94 | myThread = XMailTimerThread(self._sendAllMails, self.DEFAULT_SLEEP_TIME, self.log) |
---|
95 | myThread.setName("xmailThread") |
---|
96 | myThread.start() |
---|
97 | |
---|
98 | def _get_current_time(self): |
---|
99 | return time.time() * SEC_MULTIPLIER |
---|
100 | |
---|
101 | #=============================================================================== |
---|
102 | # EMail Send Logic |
---|
103 | #=============================================================================== |
---|
104 | def _sendAllMails(self): |
---|
105 | """Central send logic for sending e-mails""" |
---|
106 | self.log.info( "check mails, language is: %s" % self._locale_string ) |
---|
107 | sys_desc = self.get_system_info() |
---|
108 | filterids = self._get_all_relevant_filters() |
---|
109 | |
---|
110 | for filter_id, username in filterids: |
---|
111 | self.log.info ( "[XMail._sendAllMails] -- filter_id: %s -- username: %s" % (filter_id, username) ) |
---|
112 | tickets = {} |
---|
113 | subject = "[%s] " % self.env.project_name |
---|
114 | filter = FilterObject(filter_id, db=self.env.get_db_cnx()) |
---|
115 | |
---|
116 | # retrieve new tickets |
---|
117 | new_tickets = self._get_relevant_tickets('time', filter) |
---|
118 | |
---|
119 | # retrieve ticket changes |
---|
120 | changed_tickets = self._get_relevant_tickets('time != changetime and changetime', filter) |
---|
121 | |
---|
122 | if new_tickets: |
---|
123 | for t in new_tickets: |
---|
124 | tickets[t['id']] = 'new' |
---|
125 | if changed_tickets: |
---|
126 | for t in changed_tickets: |
---|
127 | if not tickets.has_key(t['id']): |
---|
128 | tickets[t['id']] = 'changed' |
---|
129 | else: |
---|
130 | changed_tickets.remove(t) |
---|
131 | |
---|
132 | if len(tickets) == 0: |
---|
133 | if filter.values['interval'] > 0: |
---|
134 | self._set_next_exe(filter, self._get_current_time()) |
---|
135 | self.log.debug("[XMail._sendAllMails] no relevant tickets, since length of tickets is %s" % len(tickets)) |
---|
136 | # nothing to do, so continue with next filter |
---|
137 | continue |
---|
138 | |
---|
139 | user_data = self._get_user_data(username) |
---|
140 | self.log.debug( "user data: %r -- negotiated: %s" % (user_data, self._locale_string) ) |
---|
141 | if not user_data.has_key('language'): |
---|
142 | self.log.debug("got no user-specific language, so using locale %s" % self._locale_string) |
---|
143 | activate(self._locale_string, self.env.path) |
---|
144 | else: |
---|
145 | activate(user_data['language'], self.env.path) |
---|
146 | |
---|
147 | subject += filter.name |
---|
148 | notifyer = XMailTicketNotify(self.env, {'user_data' : user_data, |
---|
149 | 'new_tickets': new_tickets, |
---|
150 | 'changed_tickets': changed_tickets, |
---|
151 | 'filter': filter, |
---|
152 | 'sys_desc': sys_desc}) |
---|
153 | notifyer.notify(resid=None, subject=subject) |
---|
154 | self._set_next_exe(filter, self._get_current_time()) |
---|
155 | self.log.info( "[XMail._sendAllMails] -----> sent email with %r tickets by XMailTicketNotify" % len(tickets) ) |
---|
156 | return |
---|
157 | |
---|
158 | def get_system_info(self): |
---|
159 | trac_ver = 'xmail' |
---|
160 | try: |
---|
161 | # get trac-version |
---|
162 | info = self.env.get_systeminfo() |
---|
163 | val = info[0][1] |
---|
164 | if type(val) != list: |
---|
165 | trac_ver = "Trac %s" % (val) |
---|
166 | |
---|
167 | # get xmail-version |
---|
168 | plugins = get_plugin_info(self.env) |
---|
169 | xmail_ver = 'XMail' |
---|
170 | for plugin in plugins: |
---|
171 | if plugin['name'] == "xmailplugin": |
---|
172 | xmail_ver = "%s %s" % (xmail_ver, plugin['version']) |
---|
173 | trac_ver = "%s (%s)" % (xmail_ver, trac_ver) |
---|
174 | except Exception, e: |
---|
175 | print "error when getting trac_ver: %s" % e |
---|
176 | return trac_ver |
---|
177 | |
---|
178 | |
---|
179 | #=============================================================================== |
---|
180 | # DB Access logic |
---|
181 | #=============================================================================== |
---|
182 | #=========================================================================== |
---|
183 | # Returns all filterids and filternaems, which have to been processed |
---|
184 | # in this cycle |
---|
185 | #=========================================================================== |
---|
186 | def _get_all_relevant_filters(self): |
---|
187 | sqlQuery = ( "select id, username from xmail" \ |
---|
188 | " where nextexe <= %s and active is not null" |
---|
189 | % self._get_current_time() ) |
---|
190 | self.log.debug( "_get_all_relevant_filters: sqlQuery: %s" % sqlQuery ) |
---|
191 | |
---|
192 | filterIds = self._execute_SQL_Query_and_Feedback(True, sqlQuery) |
---|
193 | if isinstance(filterIds,list): |
---|
194 | return filterIds |
---|
195 | else: |
---|
196 | return [] |
---|
197 | |
---|
198 | def _get_relevant_tickets(self, timefilter, filter): |
---|
199 | """retrieve relevant tickets for filterId |
---|
200 | timefilter is usually 'time' (for new tickets) or 'time != changetime and changetime' (for ticket changes) |
---|
201 | """ |
---|
202 | if filter.values['interval'] > 0: |
---|
203 | t = filter.values['nextexe'] - (filter.values['interval'] * 1000000) |
---|
204 | else: |
---|
205 | t = filter.values['lastsuccessexe'] or filter.values['nextexe'] |
---|
206 | |
---|
207 | sql = filter.get_filter_select( additional_where_clause="%s>=%s" % |
---|
208 | (timefilter, t) ) |
---|
209 | self.log.debug('_get_relevant_tickets -- sql: %s' % sql) |
---|
210 | |
---|
211 | data = None |
---|
212 | try: |
---|
213 | db = self.env.get_db_cnx() |
---|
214 | myCursor = db.cursor() |
---|
215 | result = [] |
---|
216 | # data_dict = {} |
---|
217 | try: |
---|
218 | myCursor.execute(sql) |
---|
219 | data = list(myCursor.fetchall()) |
---|
220 | except Exception, e: |
---|
221 | self.log.error("error while fetching data: %s" % e) |
---|
222 | |
---|
223 | # print "data: %r" % data |
---|
224 | for row in data: |
---|
225 | data_dict = {} |
---|
226 | # print "row: %r" % row |
---|
227 | for i, col in enumerate(row): |
---|
228 | if i == 0: |
---|
229 | data_dict['id'] = col |
---|
230 | else: |
---|
231 | data_dict[filter.select_fields[i-1]] = col |
---|
232 | result.append(data_dict) |
---|
233 | # print "result: %r" % result |
---|
234 | except Exception, e: |
---|
235 | self.log.info( "could not get relevant ticket, since error occured: %s\n sql: %s" % (e, sql) ) |
---|
236 | |
---|
237 | return result |
---|
238 | |
---|
239 | |
---|
240 | #=========================================================================== |
---|
241 | # Returns the corresponding email-address to the given username |
---|
242 | #=========================================================================== |
---|
243 | def _get_user_data(self, username): |
---|
244 | sql = ("select name, value from session_attribute where name in ('language', 'email')" |
---|
245 | " and sid='%s'" % username) |
---|
246 | db = self.env.get_db_cnx() |
---|
247 | myCursor = db.cursor() |
---|
248 | result = None |
---|
249 | try: |
---|
250 | myCursor.execute(sql) |
---|
251 | data = list( myCursor.fetchall() ) |
---|
252 | result = {} |
---|
253 | for row in data: |
---|
254 | result[row[0]] = row[1] |
---|
255 | except Exception, e: |
---|
256 | self.log.debug( "e: %s" % e ) # TODO[fm]: remove in prod-mode |
---|
257 | return result |
---|
258 | |
---|
259 | #=========================================================================== |
---|
260 | # |
---|
261 | #=========================================================================== |
---|
262 | def _set_next_exe(self, filter, current_time): |
---|
263 | next_exe = current_time |
---|
264 | if filter.values['interval'] > 0: |
---|
265 | next_exe = (filter.values['interval'] * SEC_MULTIPLIER) + filter.values['nextexe'] |
---|
266 | |
---|
267 | sql = "update xmail set nextexe=%i, lastsuccessexe=%i where id=%i" % (next_exe, current_time, filter.id) |
---|
268 | self.log.debug("[_set_next_exe] sql: %s" % sql) |
---|
269 | db = self.env.get_db_cnx() |
---|
270 | cursor = db.cursor() |
---|
271 | try: |
---|
272 | cursor.execute(sql) |
---|
273 | db.commit() |
---|
274 | return True |
---|
275 | except Exception, e: |
---|
276 | self.log.error('updating failed, because of error: %s' % e) |
---|
277 | |
---|
278 | return False |
---|
279 | |
---|
280 | #=========================================================================== |
---|
281 | # Executes given sql statement and returns the sql result |
---|
282 | #=========================================================================== |
---|
283 | def _execute_SQL_Query_and_Feedback(self, select, sqlQuery, *params): |
---|
284 | |
---|
285 | if sqlQuery != None and params != None: |
---|
286 | db = self.env.get_db_cnx() |
---|
287 | myCursor = db.cursor() |
---|
288 | data = {} |
---|
289 | try: |
---|
290 | myCursor.execute(sqlQuery, params) |
---|
291 | if(select == True): |
---|
292 | """ result is an 2-dimensinal array of the select results""" |
---|
293 | data = list(myCursor.fetchall()) |
---|
294 | else: |
---|
295 | """ Result is the amount of insered rows""" |
---|
296 | data = myCursor.rowcount |
---|
297 | db.commit() |
---|
298 | except Exception, e: |
---|
299 | self.log.error ("Error executing SQL Statement \n ( %s ) \n %s" % (sqlQuery, e)) |
---|
300 | db.rollback(); |
---|
301 | try: |
---|
302 | db.close() |
---|
303 | except e: |
---|
304 | self.log.error("DB close fails. \n %s" % e) |
---|
305 | return data |
---|
306 | |
---|
307 | |
---|
308 | class XMailTicketNotify(NotifyEmail): |
---|
309 | _data = None |
---|
310 | # _locale_string = 'en' |
---|
311 | _email_adr = None |
---|
312 | template_name = "timed_email.txt" |
---|
313 | COLS = 75 |
---|
314 | COL_DESC = 20 |
---|
315 | |
---|
316 | def __init__(self, env, data, template_name=None): |
---|
317 | NotifyEmail.__init__(self, env) |
---|
318 | locale_dir = pkg_resources.resource_filename(__name__, 'locale') |
---|
319 | add_domain(self.env.path, locale_dir) |
---|
320 | |
---|
321 | if template_name: |
---|
322 | self.template_name = template_name |
---|
323 | self._data = data |
---|
324 | if self._data and self._data['user_data']: |
---|
325 | # self._locale_string = self._data['user_data']['language'] # not used at the moment |
---|
326 | self._email_adr = self._data['user_data']['email'] |
---|
327 | # print "[XMailMultiTicketNotify] self._data: %r --- data: %r --- tickets: %r" % (self._data, data, data['new_tickets']) |
---|
328 | |
---|
329 | # override from TicketNotifyEmail |
---|
330 | def get_recipients(self, tktid): |
---|
331 | ccrecipients = [] |
---|
332 | torecipients = [] |
---|
333 | |
---|
334 | if self._email_adr: |
---|
335 | torecipients.append(self._email_adr) |
---|
336 | filter_sep = ('=' * self.COLS) |
---|
337 | filter = self._data['filter'] |
---|
338 | |
---|
339 | # format objects |
---|
340 | self.data['new_tickets_count'] = len(self._data['new_tickets']) |
---|
341 | self.data['new_tickets'] = self._format_tickets(self._data['new_tickets']) |
---|
342 | self.data['changed_tickets_count'] = len(self._data['changed_tickets']) |
---|
343 | self.data['changed_tickets'] = self._format_tickets(self._data['changed_tickets']) |
---|
344 | self.data['filter_sep'] = filter_sep |
---|
345 | self.data['new_ticket_hdr'] = _('New tickets:') |
---|
346 | self.data['changed_ticket_hdr'] = _('Changed tickets:') |
---|
347 | # simply copy some data objects |
---|
348 | self.data['filter'] = filter |
---|
349 | self.data['sys_desc'] = self._data['sys_desc'] |
---|
350 | link = "%s/xmail/xmail-edit.html?id=%i" % (self.env.abs_href.base, filter.id) |
---|
351 | self.data['change_hint'] = tag_('To change the filter go to %(link)s', link=link) |
---|
352 | |
---|
353 | # return ([], []) |
---|
354 | return (torecipients, ccrecipients) |
---|
355 | |
---|
356 | def _format_tickets(self, tickets): |
---|
357 | if not tickets or not type(tickets) in (list, tuple): |
---|
358 | return None |
---|
359 | |
---|
360 | sep = ('-' * self.COLS) + '\n' |
---|
361 | href = self.env.abs_href.base |
---|
362 | format = "%%%is: %%s\n" % self.COL_DESC |
---|
363 | txt = None |
---|
364 | |
---|
365 | for ticket in tickets: |
---|
366 | id = ticket['id'] |
---|
367 | summary = ticket['summary'] |
---|
368 | big_cols = [] |
---|
369 | time_fld = "" |
---|
370 | |
---|
371 | if not txt: |
---|
372 | txt = "#%s" % id |
---|
373 | else: |
---|
374 | txt += "\n\n#%s" % id |
---|
375 | |
---|
376 | if summary: |
---|
377 | txt += ": %s" % summary |
---|
378 | txt += "\n%s/ticket/%s\n" % (href, id) # add link |
---|
379 | txt += sep |
---|
380 | for key in ticket.keys(): |
---|
381 | if key in ('time', 'changetime'): |
---|
382 | time_fld += format % (_(key), format_datetime(ticket[key])) |
---|
383 | elif not key in ('id', 'summary'): |
---|
384 | content = ticket[key] |
---|
385 | if not content: continue # ignore empty fields |
---|
386 | elif type(tickets) == str and len(content) > (self.COLS - self.COL_DESC): |
---|
387 | big_cols.append({key: ticket[key]}) |
---|
388 | else: |
---|
389 | txt += format % (_(key), content) |
---|
390 | txt += time_fld |
---|
391 | |
---|
392 | for bg in big_cols: |
---|
393 | for key in bg: # should only be key = value |
---|
394 | txt += "%s:\n%s" % (_(key), bg[key]) |
---|
395 | |
---|
396 | return txt |
---|
397 | |
---|
398 | |
---|
399 | #=============================================================================== |
---|
400 | # Timer Implementation |
---|
401 | #=============================================================================== |
---|
402 | #=========================================================================== |
---|
403 | # Timer Implementation with execute a given function in an given intervall |
---|
404 | # @param func: Function to call |
---|
405 | # @param sec: Sleep intervall in seconds |
---|
406 | #=========================================================================== |
---|
407 | class XMailTimerThread(Thread): |
---|
408 | def __init__(self, func, sec=30, log_func=None): |
---|
409 | Thread.__init__(self) |
---|
410 | self.func = func |
---|
411 | self.sec = sec |
---|
412 | self.log = log_func |
---|
413 | if self.log: |
---|
414 | self.log.info('XMailTimerThread: init done with sec: %s' % sec) |
---|
415 | |
---|
416 | def run(self): |
---|
417 | while True: |
---|
418 | try: |
---|
419 | # error occured: 'ascii' codec can't encode character u'\xfc' in position 19: ordinal not in range(128) |
---|
420 | self.func() |
---|
421 | except Exception, e: |
---|
422 | if self.log: |
---|
423 | self.log.error('==============================\n' \ |
---|
424 | '[XMailTimerThread.run] -- Exception occured: %r' % e) |
---|
425 | exc_traceback = sys.exc_info()[2] |
---|
426 | self.log.error('TraceBack: %s' % format_tb(exc_traceback) ) |
---|
427 | |
---|
428 | if self.log: |
---|
429 | self.log.debug('sleeping %s secs' % self.sec) |
---|
430 | time.sleep(self.sec) |
---|