| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2003-2006 Edgewall Software |
|---|
| 4 | # Copyright (C) 2003-2005 Daniel Lundin <daniel@edgewall.com> |
|---|
| 5 | # Copyright (C) 2005-2006 Emmanuel Blot <emmanuel.blot@free.fr> |
|---|
| 6 | # All rights reserved. |
|---|
| 7 | # |
|---|
| 8 | # This software is licensed as described in the file COPYING, which |
|---|
| 9 | # you should have received as part of this distribution. The terms |
|---|
| 10 | # are also available at http://trac.edgewall.com/license.html. |
|---|
| 11 | # |
|---|
| 12 | # This software consists of voluntary contributions made by many |
|---|
| 13 | # individuals. For the exact contribution history, see the revision |
|---|
| 14 | # history and logs, available at http://projects.edgewall.com/trac/. |
|---|
| 15 | # |
|---|
| 16 | # Author: Daniel Lundin <daniel@edgewall.com> |
|---|
| 17 | # |
|---|
| 18 | |
|---|
| 19 | import md5 |
|---|
| 20 | |
|---|
| 21 | from trac import __version__ |
|---|
| 22 | from trac.core import * |
|---|
| 23 | from trac.config import * |
|---|
| 24 | from trac.util import CRLF, wrap |
|---|
| 25 | from trac.notification import NotifyEmail |
|---|
| 26 | |
|---|
| 27 | |
|---|
| 28 | class TicketNotificationSystem(Component): |
|---|
| 29 | |
|---|
| 30 | always_notify_owner = BoolOption('notification', 'always_notify_owner', |
|---|
| 31 | 'false', |
|---|
| 32 | """Always send notifications to the ticket owner (''since 0.9'').""") |
|---|
| 33 | |
|---|
| 34 | always_notify_reporter = BoolOption('notification', 'always_notify_reporter', |
|---|
| 35 | 'false', |
|---|
| 36 | """Always send notifications to any address in the ''reporter'' |
|---|
| 37 | field.""") |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | class TicketNotifyEmail(NotifyEmail): |
|---|
| 41 | """Notification of ticket changes.""" |
|---|
| 42 | |
|---|
| 43 | template_name = "ticket_notify_email.cs" |
|---|
| 44 | ticket = None |
|---|
| 45 | newticket = None |
|---|
| 46 | modtime = 0 |
|---|
| 47 | from_email = 'trac+ticket@localhost' |
|---|
| 48 | COLS = 75 |
|---|
| 49 | |
|---|
| 50 | def __init__(self, env): |
|---|
| 51 | NotifyEmail.__init__(self, env) |
|---|
| 52 | self.prev_cc = [] |
|---|
| 53 | |
|---|
| 54 | def notify(self, ticket, newticket=True, modtime=0): |
|---|
| 55 | self.ticket = ticket |
|---|
| 56 | self.modtime = modtime |
|---|
| 57 | self.newticket = newticket |
|---|
| 58 | self.ticket['description'] = wrap(self.ticket.values.get('description', ''), |
|---|
| 59 | self.COLS, initial_indent=' ', |
|---|
| 60 | subsequent_indent=' ', linesep=CRLF) |
|---|
| 61 | self.ticket['link'] = self.env.abs_href.ticket(ticket.id) |
|---|
| 62 | self.hdf.set_unescaped('email.ticket_props', self.format_props()) |
|---|
| 63 | self.hdf.set_unescaped('email.ticket_body_hdr', self.format_hdr()) |
|---|
| 64 | self.hdf.set_unescaped('ticket', self.ticket.values) |
|---|
| 65 | self.hdf['ticket.new'] = self.newticket |
|---|
| 66 | subject = self.format_subj() |
|---|
| 67 | if not self.newticket: |
|---|
| 68 | subject = 'Re: ' + subject |
|---|
| 69 | self.hdf.set_unescaped('email.subject', subject) |
|---|
| 70 | changes = '' |
|---|
| 71 | if not self.newticket and modtime: # Ticket change |
|---|
| 72 | changelog = ticket.get_changelog(modtime) |
|---|
| 73 | for date, author, field, old, new in changelog: |
|---|
| 74 | self.hdf.set_unescaped('ticket.change.author', author) |
|---|
| 75 | pfx = 'ticket.change.%s' % field |
|---|
| 76 | newv = '' |
|---|
| 77 | if field == 'comment': |
|---|
| 78 | newv = wrap(new, self.COLS, ' ', ' ', CRLF) |
|---|
| 79 | elif field == 'description': |
|---|
| 80 | new_descr = wrap(new, self.COLS, ' ', ' ', CRLF) |
|---|
| 81 | old_descr = wrap(old, self.COLS, '> ', '> ', CRLF) |
|---|
| 82 | old_descr = old_descr.replace(2*CRLF, CRLF + '>' + CRLF) |
|---|
| 83 | cdescr = CRLF |
|---|
| 84 | cdescr += 'Old description:' + 2*CRLF + old_descr + 2*CRLF |
|---|
| 85 | cdescr += 'New description:' + 2*CRLF + new_descr + CRLF |
|---|
| 86 | self.hdf.set_unescaped('email.changes_descr', cdescr) |
|---|
| 87 | elif field == 'cc': |
|---|
| 88 | (addcc, delcc) = self.diff_cc(old, new) |
|---|
| 89 | chgcc = '' |
|---|
| 90 | if delcc: |
|---|
| 91 | chgcc += wrap(" * cc: %s (removed)" % ', '.join(delcc), |
|---|
| 92 | self.COLS, ' ', ' ', CRLF) |
|---|
| 93 | chgcc += CRLF |
|---|
| 94 | if addcc: |
|---|
| 95 | chgcc += wrap(" * cc: %s (added)" % ', '.join(addcc), |
|---|
| 96 | self.COLS, ' ', ' ', CRLF) |
|---|
| 97 | chgcc += CRLF |
|---|
| 98 | if chgcc: |
|---|
| 99 | changes += chgcc |
|---|
| 100 | self.prev_cc += old and self.parse_cc(old) or [] |
|---|
| 101 | else: |
|---|
| 102 | newv = new |
|---|
| 103 | l = 7 + len(field) |
|---|
| 104 | chg = wrap('%s => %s' % (old, new), self.COLS - l, '', |
|---|
| 105 | l * ' ', CRLF) |
|---|
| 106 | changes += ' * %s: %s%s' % (field, chg, CRLF) |
|---|
| 107 | if newv: |
|---|
| 108 | self.hdf.set_unescaped('%s.oldvalue' % pfx, old) |
|---|
| 109 | self.hdf.set_unescaped('%s.newvalue' % pfx, newv) |
|---|
| 110 | self.hdf.set_unescaped('%s.author' % pfx, author) |
|---|
| 111 | if changes: |
|---|
| 112 | self.hdf.set_unescaped('email.changes_body', changes) |
|---|
| 113 | NotifyEmail.notify(self, ticket.id, subject) |
|---|
| 114 | |
|---|
| 115 | def format_props(self): |
|---|
| 116 | tkt = self.ticket |
|---|
| 117 | fields = [f for f in tkt.fields if f.name not in ('summary', 'cc')] |
|---|
| 118 | width = [0, 0, 0, 0] |
|---|
| 119 | i = 0 |
|---|
| 120 | for f in [f.name for f in fields if f.type != 'textarea']: |
|---|
| 121 | if not tkt.values.has_key(f): |
|---|
| 122 | continue |
|---|
| 123 | fval = tkt[f] |
|---|
| 124 | if fval.find('\n') != -1: |
|---|
| 125 | continue |
|---|
| 126 | idx = 2 * (i % 2) |
|---|
| 127 | if len(f) > width[idx]: |
|---|
| 128 | width[idx] = len(f) |
|---|
| 129 | if len(fval) > width[idx + 1]: |
|---|
| 130 | width[idx + 1] = len(fval) |
|---|
| 131 | i += 1 |
|---|
| 132 | format = ('%%%is: %%-%is | ' % (width[0], width[1]), |
|---|
| 133 | ' %%%is: %%-%is%s' % (width[2], width[3], CRLF)) |
|---|
| 134 | l = (width[0] + width[1] + 5) |
|---|
| 135 | sep = l * '-' + '+' + (self.COLS - l) * '-' |
|---|
| 136 | txt = sep + CRLF |
|---|
| 137 | big = [] |
|---|
| 138 | i = 0 |
|---|
| 139 | for f in [f for f in fields if f.name != 'description']: |
|---|
| 140 | fname = f.name |
|---|
| 141 | if not tkt.values.has_key(fname): |
|---|
| 142 | continue |
|---|
| 143 | fval = tkt[fname] |
|---|
| 144 | if f.type == 'textarea' or '\n' in unicode(fval): |
|---|
| 145 | big.append((fname.capitalize(), CRLF.join(fval.splitlines()))) |
|---|
| 146 | else: |
|---|
| 147 | txt += format[i % 2] % (fname.capitalize(), fval) |
|---|
| 148 | i += 1 |
|---|
| 149 | if i % 2: |
|---|
| 150 | txt += CRLF |
|---|
| 151 | if big: |
|---|
| 152 | txt += sep |
|---|
| 153 | for name, value in big: |
|---|
| 154 | txt += CRLF.join(['', name + ':', value, '', '']) |
|---|
| 155 | txt += sep |
|---|
| 156 | return txt |
|---|
| 157 | |
|---|
| 158 | def parse_cc(self, txt): |
|---|
| 159 | return filter(lambda x: '@' in x, txt.replace(',', ' ').split()) |
|---|
| 160 | |
|---|
| 161 | def diff_cc(self, old, new): |
|---|
| 162 | oldcc = NotifyEmail.addrsep_re.split(old) |
|---|
| 163 | newcc = NotifyEmail.addrsep_re.split(new) |
|---|
| 164 | added = [x for x in newcc if x not in oldcc] |
|---|
| 165 | removed = [x for x in oldcc if x not in newcc] |
|---|
| 166 | return (added, removed) |
|---|
| 167 | |
|---|
| 168 | def format_hdr(self): |
|---|
| 169 | return '#%s: %s' % (self.ticket.id, wrap(self.ticket['summary'], |
|---|
| 170 | self.COLS, linesep=CRLF)) |
|---|
| 171 | |
|---|
| 172 | def format_subj(self): |
|---|
| 173 | projname = self.config.get('project', 'name') |
|---|
| 174 | return '[%s] #%s: %s' % (projname, self.ticket.id, |
|---|
| 175 | self.ticket['summary']) |
|---|
| 176 | |
|---|
| 177 | def get_recipients(self, tktid): |
|---|
| 178 | notify_reporter = self.config.getbool('notification', |
|---|
| 179 | 'always_notify_reporter') |
|---|
| 180 | notify_owner = self.config.getbool('notification', |
|---|
| 181 | 'always_notify_owner') |
|---|
| 182 | |
|---|
| 183 | ccrecipients = self.prev_cc |
|---|
| 184 | torecipients = [] |
|---|
| 185 | cursor = self.db.cursor() |
|---|
| 186 | |
|---|
| 187 | # Harvest email addresses from the cc, reporter, and owner fields |
|---|
| 188 | cursor.execute("SELECT cc,reporter,owner FROM ticket WHERE id=%s", |
|---|
| 189 | (tktid,)) |
|---|
| 190 | row = cursor.fetchone() |
|---|
| 191 | if row: |
|---|
| 192 | ccrecipients += row[0] and row[0].replace(',', ' ').split() or [] |
|---|
| 193 | if notify_reporter: |
|---|
| 194 | torecipients.append(row[1]) |
|---|
| 195 | if notify_owner: |
|---|
| 196 | torecipients.append(row[2]) |
|---|
| 197 | |
|---|
| 198 | # Harvest email addresses from the author field of ticket_change(s) |
|---|
| 199 | if notify_reporter: |
|---|
| 200 | cursor.execute("SELECT DISTINCT author,ticket FROM ticket_change " |
|---|
| 201 | "WHERE ticket=%s", (tktid,)) |
|---|
| 202 | for author,ticket in cursor: |
|---|
| 203 | torecipients.append(author) |
|---|
| 204 | |
|---|
| 205 | return (torecipients, ccrecipients) |
|---|
| 206 | |
|---|
| 207 | def get_message_id(self, rcpt, modtime=0): |
|---|
| 208 | """Generate a predictable, but sufficiently unique message ID.""" |
|---|
| 209 | s = '%s.%08d.%d.%s' % (self.config.get('project', 'url'), |
|---|
| 210 | int(self.ticket.id), modtime, rcpt) |
|---|
| 211 | dig = md5.new(s).hexdigest() |
|---|
| 212 | host = self.from_email[self.from_email.find('@') + 1:] |
|---|
| 213 | msgid = '<%03d.%s@%s>' % (len(s), dig, host) |
|---|
| 214 | return msgid |
|---|
| 215 | |
|---|
| 216 | def send(self, torcpts, ccrcpts): |
|---|
| 217 | hdrs = {} |
|---|
| 218 | dest = torcpts or ccrcpts |
|---|
| 219 | if not dest: |
|---|
| 220 | self.env.log.info('no recipient for a ticket notification') |
|---|
| 221 | return |
|---|
| 222 | hdrs['Message-ID'] = self.get_message_id(dest[0], self.modtime) |
|---|
| 223 | hdrs['X-Trac-Ticket-ID'] = str(self.ticket.id) |
|---|
| 224 | hdrs['X-Trac-Ticket-URL'] = self.ticket['link'] |
|---|
| 225 | if not self.newticket: |
|---|
| 226 | hdrs['In-Reply-To'] = self.get_message_id(dest[0]) |
|---|
| 227 | hdrs['References'] = self.get_message_id(dest[0]) |
|---|
| 228 | NotifyEmail.send(self, torcpts, ccrcpts, hdrs) |
|---|
| 229 | |
|---|