Trac still does not do HTML mails. As I've written before, it can be hacked to send good looking HTML notifications, though. I happily used trac-0.12.2 until I discovered some issues with subversion-1.7.
So I upgraded to trac-0.12.3, the upgrade went seamlessly. All it took as a repository resync for changesets with removed files to show up again. Yay!
Of course trac still doesn't send HTML notifications, so I had to apply my previous patches from trac-0.12.2. Except for trac/ticket/notification.py this was rather easy. See my diff here or grab it from pastebin.
--- Trac-0.12.3/trac/ticket/web_ui.py Mon Feb 6 21:50:02 2012This proper diff patch was brought to you by TortoiseSVN's TortoiseMerge.
+++ Trac-0.12.3-fizze/trac/ticket/web_ui.py Mon Feb 27 14:24:15 2012
@@ -1194,7 +1194,7 @@
# Notify
try:
- tn = TicketNotifyEmail(self.env)
+ tn = TicketNotifyEmail(self.env, req) #rlrj60:4/10/09
tn.notify(ticket, newticket=True)
except Exception, e:
self.log.error("Failure sending notification on creation of "
@@ -1238,7 +1238,7 @@
cnum=internal_cnum):
fragment = cnum and '#comment:' + cnum or ''
try:
- tn = TicketNotifyEmail(self.env)
+ tn = TicketNotifyEmail(self.env, req) #rlrj60:4/10/09
tn.notify(ticket, newticket=False, modtime=now)
except Exception, e:
self.log.error("Failure sending notification on change to "
--- Trac-0.12.3/trac/notification.py Mon Feb 6 21:50:22 2012
+++ Trac-0.12.3-fizze/trac/notification.py Mon Feb 27 14:22:30 2012
@@ -277,6 +277,7 @@
self.longaddr_re = re.compile(r'^\s*(.*)\s+<\s*(%s)\s*>\s*$' % addrfmt)
self._init_pref_encoding()
domains = self.env.config.get('notification', 'ignore_domains', '')
+ self.from_email = self.env.config.get('notification', 'smtp_from')
self._ignore_domains = [x.strip() for x in domains.lower().split(',')]
# Get the email addresses of all known users
self.email_map = {}
@@ -454,7 +455,7 @@
if pcc:
headers['Cc'] = ', '.join(pcc)
headers['Date'] = formatdate()
- msg = MIMEText(body, 'plain')
+ msg = MIMEText(body, 'html')
# Message class computes the wrong type from MIMEText constructor,
# which does not take a Charset object as initializer. Reset the
# encoding type to force a new, valid evaluation
--- Trac-0.12.3/trac/ticket/notification.py Mon Feb 6 21:50:04 2012
+++ Trac-0.12.3-fizze/trac/ticket/notification.py Tue Feb 28 10:06:34 2012
@@ -18,13 +18,14 @@
from genshi.template.text import NewTextTemplate
+from trac.wiki.formatter import *
from trac.core import *
from trac.config import *
from trac.notification import NotifyEmail
from trac.ticket.api import TicketSystem
from trac.util import md5
from trac.util.datefmt import to_utimestamp
-from trac.util.text import obfuscate_email_address, text_width, wrap
+from trac.util.text import obfuscate_email_address, text_width, wrap, CRLF
from trac.util.translation import deactivate, reactivate
class TicketNotificationSystem(Component):
@@ -73,9 +74,10 @@
from_email = 'trac+ticket@localhost'
COLS = 75
- def __init__(self, env):
+ def __init__(self, env, req):
NotifyEmail.__init__(self, env)
self.prev_cc = []
+ self.req = req
ambiguous_char_width = env.config.get('notification',
'ambiguous_char_width',
'single')
@@ -102,6 +104,7 @@
self.owner = ''
changes_descr = ''
change_data = {}
+ BRCRLF = '<br />' + CRLF
link = self.env.abs_href.ticket(ticket.id)
summary = self.ticket['summary']
@@ -112,9 +115,9 @@
if not change['permanent']: # attachment with same time...
continue
change_data.update({
- 'author': self.obfuscate_email(change['author']),
- 'comment': wrap(change['comment'], self.COLS, ' ', ' ',
- '\n', self.ambiwidth)
+ 'author': change['author'],
+ 'comment': wiki_to_html(change['comment'], env=self.env, req=self.req, absurls=True)
+
})
link += '#comment:%s' % str(change.get('cnum', ''))
for field, values in change['fields'].iteritems():
@@ -122,17 +125,14 @@
new = values['new']
newv = ''
if field == 'description':
- new_descr = wrap(new, self.COLS, ' ', ' ', '\n',
- self.ambiwidth)
- old_descr = wrap(old, self.COLS, '> ', '> ', '\n',
- self.ambiwidth)
- old_descr = old_descr.replace(2 * '\n', '\n' + '>' + \
- '\n')
- cdescr = '\n'
- cdescr += 'Old description:' + 2 * '\n' + old_descr + \
- 2 * '\n'
- cdescr += 'New description:' + 2 * '\n' + new_descr + \
- '\n'
+ new_descr = wrap(new, self.COLS, ' ', ' ', BRCRLF)
+ old_descr = wrap(old, self.COLS, '>', '>', BRCRLF)
+ old_descr = old_descr.replace(2*CRLF, CRLF + '>' + BRCRLF)
+
+ cdescr = CRLF
+ cdescr += 'Old description:' + 2*CRLF + old_descr + CRLF + BRCRLF
+ cdescr += 'New description:' + 2*CRLF + new_descr + CRLF + BRCRLF
+
changes_descr = cdescr
elif field == 'summary':
summary = "%s (was: %s)" % (new, old)
@@ -140,15 +140,14 @@
(addcc, delcc) = self.diff_cc(old, new)
chgcc = ''
if delcc:
- chgcc += wrap(" * cc: %s (removed)" %
- ', '.join(delcc),
- self.COLS, ' ', ' ', '\n',
- self.ambiwidth) + '\n'
+ chgcc += '<li>' + wrap("cc: %s (removed)" % ', '.join(delcc),
+ self.COLS, ' ', ' ', BRCRLF) + '</li>'
+ chgcc += CRLF
if addcc:
- chgcc += wrap(" * cc: %s (added)" %
- ', '.join(addcc),
- self.COLS, ' ', ' ', '\n',
- self.ambiwidth) + '\n'
+ chgcc += '<li>' + wrap("cc: %s (added)" % ', '.join(addcc),
+ self.COLS, ' ', ' ', BRCRLF) + '</li>'
+ chgcc += CRLF
+
if chgcc:
changes_body += chgcc
self.prev_cc += old and self.parse_cc(old) or []
@@ -162,25 +161,23 @@
if len(old + new) + length > self.COLS:
length = 5
if len(old) + length > self.COLS:
- spacer_old = '\n'
+ spacer_old = CRLF
if len(new) + length > self.COLS:
- spacer_new = '\n'
- chg = '* %s: %s%s%s=>%s%s' % (field, spacer_old, old,
- spacer_old, spacer_new,
- new)
- chg = chg.replace('\n', '\n' + length * ' ')
- chg = wrap(chg, self.COLS, '', length * ' ', '\n',
- self.ambiwidth)
- changes_body += ' %s%s' % (chg, '\n')
+ spacer_new = CRLF
+ chg = wrap('%s → %s' % (old, new), self.COLS , '',
+ ' ', CRLF)
+ changes_body += '<li>' + '%s: %s%s' % (field, chg, CRLF) + '</li>'
+
if newv:
change_data[field] = {'oldvalue': old, 'newvalue': new}
+ if changes_body:
+ changes_body = '<ul>' + changes_body + '</ul>'
ticket_values = ticket.values.copy()
ticket_values['id'] = ticket.id
- ticket_values['description'] = wrap(
- ticket_values.get('description', ''), self.COLS,
- initial_indent=' ', subsequent_indent=' ', linesep='\n',
- ambiwidth=self.ambiwidth)
+ # convert wiki syntax to html
+ ticket_values['description'] = wiki_to_html(ticket_values.get('description', ''), env=self.env, req=self.req, absurls=True)
+
ticket_values['new'] = self.newticket
ticket_values['link'] = link
@@ -200,6 +197,7 @@
def format_props(self):
tkt = self.ticket
+ BRCRLF = '<br />' + CRLF
fields = [f for f in tkt.fields
if f['name'] not in ('summary', 'cc', 'time', 'changetime')]
width = [0, 0, 0, 0]
@@ -233,8 +231,11 @@
else:
width_r = min((self.COLS - 1) * 2 / 3, width_r)
width_l = self.COLS - width_r - 1
- sep = width_l * '-' + '+' + width_r * '-'
- txt = sep + '\n'
+ format = ('<tr valign="top"><td align="right"><b>%s:</b></td><td align="left">%s</td>','<td align="right"><b>%s:</b></td><td align="left">%s</td></tr>'+CRLF)
+ #sep = width_l * '-' + '+' + width_r * '-'
+ sep = CRLF
+ txt = sep + CRLF
+ txt = '<table border="0" cellpadding="2" cellspacing="0">' + CRLF
cell_tmp = [u'', u'']
big = []
i = 0
@@ -247,10 +248,11 @@
if fname in ['owner', 'reporter']:
fval = self.obfuscate_email(fval)
if f['type'] == 'textarea' or '\n' in unicode(fval):
- big.append((f['label'], '\n'.join(fval.splitlines())))
+ big.append((f['label'], CRLF.join(fval.splitlines())))
else:
# Note: f['label'] is a Babel's LazyObject, make sure its
# __str__ method won't be called.
+ txt += format[i % 2] % (f['label'], unicode(fval))
str_tmp = u'%s: %s' % (f['label'], unicode(fval))
idx = i % 2
cell_tmp[idx] += wrap(str_tmp, width_lr[idx] - 2 + 2 * idx,
@@ -269,12 +271,19 @@
cell_r.append('')
fmt_width = width_l - self.get_text_width(cell_l[i]) \
+ len(cell_l[i])
- txt += u'%-*s|%s%s' % (fmt_width, cell_l[i], cell_r[i], '\n')
+ #txt += u'%-*s|%s%s' % (fmt_width, cell_l[i], cell_r[i], '\n')
+
+ if i % 2:
+ txt += '<td colspan="2"> </td></tr>' + CRLF
+ txt += CRLF
+
if big:
txt += sep
for name, value in big:
- txt += '\n'.join(['', name + ':', value, '', ''])
+ txt += CRLF.join(['<tr align="left"><td colspan="2"><b>' + name + ':' + '</b></td></tr>', '<tr align="left"><td colspan="2">' + value + '</td></tr>'])
+ """txt += CRLF.join(['', name + ':', value, '', ''])"""
txt += sep
+ txt += '</table>' + CRLF
return txt
def parse_cc(self, txt):
@@ -283,15 +292,13 @@
def diff_cc(self, old, new):
oldcc = NotifyEmail.addrsep_re.split(old)
newcc = NotifyEmail.addrsep_re.split(new)
- added = [self.obfuscate_email(x) \
- for x in newcc if x and x not in oldcc]
- removed = [self.obfuscate_email(x) \
- for x in oldcc if x and x not in newcc]
+ added = [x for x in newcc if x and x not in oldcc]
+ removed = [x for x in oldcc if x and x not in newcc]
return (added, removed)
def format_hdr(self):
return '#%s: %s' % (self.ticket.id, wrap(self.ticket['summary'],
- self.COLS, linesep='\n',
+ self.COLS, linesep=CRLF,
ambiwidth=self.ambiwidth))
def format_subj(self, summary):
@@ -383,6 +390,7 @@
hdrs = {}
hdrs['Message-ID'] = self.get_message_id(dest, self.modtime)
hdrs['X-Trac-Ticket-ID'] = str(self.ticket.id)
+ hdrs['Content-Type'] = 'text/html; charset=utf-8'
hdrs['X-Trac-Ticket-URL'] = self.data['ticket']['link']
if not self.newticket:
msgid = self.get_message_id(dest)
--- Trac-0.12.3/trac/ticket/templates/ticket_notify_email.txt Mon Feb 6 21:50:02 2012
+++ Trac-0.12.3-fizze/trac/ticket/templates/ticket_notify_email.txt Thu Dec 29 08:58:44 2011
@@ -1,32 +1,51 @@
-$ticket_body_hdr
-$ticket_props
-{% choose ticket.new %}\
-{% when True %}\
-$ticket.description
-{% end %}\
-{% otherwise %}\
-{% if changes_body %}\
-${_('Changes (by %(author)s):', author=change.author)}
-
-$changes_body
+<div>
+ <div style="font-family: Verdana, Arial, Helvetica, sans-serif; background-color:#f8f8f8">
+ <hr>
+ <a style="text-decoration:none;color:#069; font-size: 19px" href="${project.url or abs_href()}"><strong>$project.name</strong></a>
+ <hr>
+ <a style="text-decoration:none;color:#666666; font-size: 17px" href="$ticket.link">$ticket_body_hdr</a>
+ <hr>
+ </div>
+ {% choose ticket.new %}\
+ {% when True %}\
+ <div style="color:#069; font-size: 15px"><em>New ticket</em> (by <strong>$ticket.reporter</strong>)</div>
+ <br/>
+ <div style="padding:1.5em;">$ticket.description</div>
+ <br/>
+ {% end %}\
+ {% otherwise %}\
+ {% if changes_body %}\
+ <div style="color:#069; font-size: 15px"><em>Changes</em> (by <strong>$change.author</strong>)</div>
+ $changes_body
{% end %}\
{% if changes_descr %}\
{% if not changes_body and not change.comment and change.author %}\
-${_('Description changed by %(author)s:', author=change.author)}
+ <div style="color:#069; font-size: 15px"><em>Description changed by</em> <strong>$change.author</strong></div>
{% end %}\
-$changes_descr
---
+{% if changes_body or change.comment or not change.author %}\
+ <div style="color:#069; font-size: 15px"><em>Description</em></div>
{% end %}\
-{% if change.comment %}\
+ <div style="padding:1.5em; font-size: 14px">$changes_descr</div>
+ <br/>
+{% end %}\
+{% if change.comment %}\
+ <div style="color:#069;"><em>Comment</em> ${not changes_body and '(by <strong>%s</strong>)' % change.author or ''}</div>
+ <div style="padding:1.5em;">$change.comment</div>
+ <br/>
+{% end %}\
+{% end %}\
+ <hr/>
+{% end %}\
-${changes_body and _('Comment:') or _('Comment (by %(author)s):', author=change.author)}
+<style type="text/css">
+ th { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 12px }
+ td { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 12px }
+</style>
+
+ $ticket_props
+
+ <hr/>
+ <div style="font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 13px;background-color:#f0f0f0;color:#999">$project.descr</div>
+ <cite style="display:block;padding:4px;background-color:#f0f0f0;color:#999;font-size:95%;border-top:1px dotted #ccc;">$project.descr</cite>
+</div>
-$change.comment
-{% end %}\
-{% end %}\
-{% end %}\
-
---
-${_('Ticket URL: <%(link)s>', link=ticket.link)}
-$project.name <${project.url or abs_href()}>
-$project.descr
4 comments:
I'VE commented on the older post, but this one IS FREAKING GREAT!
Really, thank you!
I'll try this on my Trac install, and if I succeed I'll be back to spread the good word :D
Thanks!
THAT IS REALLY HELPFUL!!!!
My team got really crazy over here.
The IT Manager gave me that smile.
YAAAAAY
Now I can be lazy the rest of the day :P
Thank you!!!
Really, thank you.
I'm really really happy righ now :D
HEY THERE!
I found, and fixed, two bugs on your modification, and it's working fine for me now.
First problem I found, it wasn't sending emails :P
Log was dumping a problem with /trac/ticket/web_ui.py
But the problem wasn't there.
The problem was in
/trac/ticket/notification.py
new line 250
where you wrote the code
txt += format[i % 2] % (f['label'], unicode(fval))
I have to say, I have no idea what this code does (I'm new to Python =/), but this was bugging.
I'm brazillian and on my language we have characters like ç or õ or even à.
This piece of code couldnt those characters and Trac wasnt sending emails.
Without them, Trac got working like a charm.
The second problem I found, the comments in emails didnt have breake lines.
If in a comment I write
"hey
there"
on emails it would go like "hey there"
But, if I insert 2 break-lines
"hey
there"
on emails I could read
"hey
there"
To fix this, I did another small change on your set of changes.
ON the file
/trac/ticket/notification.py
near line 112
WHERE you wrote
'comment': wiki_to_html(change['comment'], env=self.env, req=self.req, absurls=True)
I added another parameter to the function wiki_to_html
I've set escape_newlines=True
So, at that line my code was like
'comment': wiki_to_html(change['comment'], env=self.env, req=self.req, absurls=True, escape_newlines=True)
And, that way, I fixed two problems.
sorry for my kind of bad english, and thanks for you code, and time :D
You're welcome!
I also found out that this hack isn't compatible with the tracopt.ticket.commit_updater plugin.
I thought about using the new TracNotification plugin but for now this is not a viable option.
Thanks for your fixes, I'll probably incorporate them into an updated post.
Post a Comment