Index: notification.py
===================================================================
--- notification.py	(revision 2118)
+++ notification.py	(working copy)
@@ -257,8 +257,8 @@
     from_email = 'trac+tickets@localhost'
     subject = ''
     template_name = None
-    nodomaddr_re = re.compile(r'[\w\d_\.\-]+')
-    addrsep_re = re.compile(r'[;\s,]+')
+    nodomaddr_re = re.compile(r'[\w\d_\.\-]+')  #'rlrj60
+    addrsep_re = re.compile(r'[;\s,]+')         #'rlrj60
 
     def __init__(self, env):
         Notify.__init__(self, env)
@@ -309,7 +309,10 @@
         if not self.config.getbool('notification', 'smtp_enabled'):
             return
         self.from_email = self.config['notification'].get('smtp_from')
-        self.from_name = self.config['notification'].get('smtp_from_name')
+        #DNK I prefer to have "Author`s name" prefix in the From e-mail field instead of static label defined by 'smtp_from_name' in Trac config 
+        self.from_name = self.config['notification'].get('smtp_from_name') # orig
+        #DNK self.from_name =  self.data['change']['author'] + ' ' + self.config['notification'].get('smtp_from_name')
+        #DNK end
         self.replyto_email = self.config['notification'].get('smtp_replyto')
         self.from_email = self.from_email or self.replyto_email
         if not self.from_email and not self.replyto_email:
@@ -457,7 +460,9 @@
         if pcc:
             headers['Cc'] = ', '.join(pcc)
         headers['Date'] = formatdate()
-        msg = MIMEText(body, 'plain')
+#rlrj60: to support html notification
+#       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
Index: ticket/notification.py
===================================================================
--- ticket/notification.py	(revision 2118)
+++ ticket/notification.py	(working copy)
@@ -16,27 +16,35 @@
 # Author: Daniel Lundin <daniel@edgewall.com>
 #
 
-from unicodedata import east_asian_width
-
-from genshi.template.text import NewTextTemplate
-
 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 CRLF, wrap, obfuscate_email_address, to_unicode
+from trac.util.text import CRLF, wrap, obfuscate_email_address
 from trac.util.translation import deactivate, reactivate
 
+#rlrj60: to support html email notification using wiki formatter
+from trac.wiki.formatter import *
+from trac.web.chrome import Chrome, add_link, add_script, add_stylesheet
+from trac.versioncontrol.diff import get_diff_options, diff_blocks
+from genshi.template import TemplateLoader
+from genshi.filters.transform import Transformer
+from genshi.core import Markup, Attrs, Namespace, QName, escape, unescape
+from trac.web.api import ITemplateStreamFilter
+from trac.mimeview import *
+#rlrj60: end of list
+
+from genshi.template.text import NewTextTemplate
+
 class TicketNotificationSystem(Component):
 
     always_notify_owner = BoolOption('notification', 'always_notify_owner',
                                      'false',
         """Always send notifications to the ticket owner (''since 0.9'').""")
 
-    always_notify_reporter = BoolOption('notification',
-                                        'always_notify_reporter',
+    always_notify_reporter = BoolOption('notification', 'always_notify_reporter',
                                         'false',
         """Always send notifications to any address in the ''reporter''
         field.""")
@@ -54,17 +62,7 @@
         `$prefix` being the value of the `smtp_subject_prefix` option.
         ''(since 0.11)''""")
 
-    ambiguous_char_width = Option('notification', 'ambiguous_char_width',
-                                  'single',
-        """Which width of ambiguous characters (e.g. 'single' or
-        'double') should be used in the table of notification mail.
 
-        If 'single', the same width as characters in US-ASCII. This is
-        expected by most users. If 'double', twice the width of
-        US-ASCII characters.  This is expected by CJK users. ''(since
-        0.12.2)''""")
-
-
 class TicketNotifyEmail(NotifyEmail):
     """Notification of ticket changes."""
 
@@ -73,15 +71,17 @@
     newticket = None
     modtime = 0
     from_email = 'trac+ticket@localhost'
-    COLS = 75
+#rlrj60: to support html email notification
+#   COLS = 75
+    COLS = 80
 
-    def __init__(self, env):
+#rlrj60: to support html email notification
+#   def __init__(self, env):
+    def __init__(self, env, req):
         NotifyEmail.__init__(self, env)
         self.prev_cc = []
-        self.ambiguous_char_width = env.config.get('notification',
-                                                   'ambiguous_char_width',
-                                                   'single')
-        self.text_widths = {}
+#rlrj60: to support html email notification
+        self.req = req
 
     def notify(self, ticket, newticket=True, modtime=None):
         """Send ticket change notification e-mail (untranslated)"""
@@ -103,6 +103,8 @@
         self.reporter = ''
         self.owner = ''
         changes_descr = ''
+#rlrj60: to support html email notification, add BRCRLF
+        BRCRLF = '<br />' + CRLF
         change_data = {}
         link = self.env.abs_href.ticket(ticket.id)
         summary = self.ticket['summary']
@@ -110,13 +112,20 @@
         if not self.newticket and modtime:  # Ticket change
             from trac.ticket.web_ui import TicketModule
             for change in TicketModule(self.env).grouped_changelog_entries(
-                                                ticket, self.db, when=modtime):
+                ticket, self.db, when=modtime):
                 if not change['permanent']: # attachment with same time...
                     continue
                 change_data.update({
                     'author': obfuscate_email_address(change['author']),
-                    'comment': wrap(change['comment'], self.COLS, ' ', ' ',
-                                    CRLF)
+#rlrj60: to support html email notification
+#                   'comment': wrap(change['comment'], self.COLS, ' ', ' ',
+#                                   CRLF)
+##rlrj60:1/24/2011: using new html format
+##def format_to_html(env, context, wikidom, escape_newlines=None):
+                    'comment': format_to_html( env    = self.env, 
+                                               context = Context.from_request(self.req, absurls=True), 
+                                               wikidom = change['comment']
+                                             )
                     })
                 link += '#comment:%s' % str(change.get('cnum', ''))
                 for field, values in change['fields'].iteritems():
@@ -124,29 +133,46 @@
                     new = values['new']
                     newv = ''
                     if field == 'description':
-                        new_descr = wrap(new, self.COLS, ' ', ' ', CRLF)
-                        old_descr = wrap(old, self.COLS, '> ', '> ', CRLF)
-                        old_descr = old_descr.replace(2 * CRLF, CRLF + '>' + \
-                                                      CRLF)
-                        cdescr = CRLF
-                        cdescr += 'Old description:' + 2 * CRLF + old_descr + \
-                                  2 * CRLF
-                        cdescr += 'New description:' + 2 * CRLF + new_descr + \
-                                  CRLF
-                        changes_descr = cdescr
+#rlrj60: to support html email notification
+#                       new_descr = wrap(new, self.COLS, ' ', ' ', CRLF)
+#                       old_descr = wrap(old, self.COLS, '> ', '> ', CRLF)
+#                       old_descr = old_descr.replace(2*CRLF, CRLF + '>' + \
+#                                                     CRLF)
+#                       cdescr = CRLF
+#                       cdescr += 'Old description:' + 2*CRLF + old_descr + \
+#                                 2*CRLF
+#                       cdescr += 'New description:' + 2*CRLF + new_descr + \
+#                                 CRLF
+#                       changes_descr = cdescr
+##def format_to_html(env, context, wikidom, escape_newlines=None):
+                        cdescr = format_to_html(  env     = self.env, 
+                                                  context = Context.from_request(self.req, absurls=True), 
+                                                  wikidom = new
+                                                )
+                        changes_descr = cdescr + self.get_diffs(self.req, old, new, ticket.id)
+                        
                     elif field == 'summary':
                         summary = "%s (was: %s)" % (new, old)
+                        
                     elif field == 'cc':
                         (addcc, delcc) = self.diff_cc(old, new)
                         chgcc = ''
                         if delcc:
-                            chgcc += wrap(" * cc: %s (removed)" %
-                                          ', '.join(delcc), 
-                                          self.COLS, ' ', ' ', CRLF) + CRLF
+#rlrj60: to support html email notification
+#                           chgcc += wrap(" * cc: %s (removed)" %
+#                                         ', '.join(delcc), 
+#                                         self.COLS, ' ', ' ', CRLF) + CRLF
+                            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, ' ', ' ', CRLF) + CRLF
+#rlrj60: to support html email notification
+#                           chgcc += wrap(" * cc: %s (added)" %
+#                                         ', '.join(addcc), 
+#                                         self.COLS, ' ', ' ', CRLF) + CRLF
+                            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 []
@@ -155,28 +181,33 @@
                             old = obfuscate_email_address(old)
                             new = obfuscate_email_address(new)
                         newv = new
-                        length = 7 + len(field)
-                        spacer_old, spacer_new = ' ', ' '
-                        if len(old + new) + length > self.COLS:
-                            length = 5
-                            if len(old) + length > self.COLS:
-                                spacer_old = CRLF
-                            if len(new) + length > self.COLS:
-                                spacer_new = CRLF
-                        chg = '* %s: %s%s%s=>%s%s' % (field, spacer_old, old,
-                                                      spacer_old, spacer_new,
-                                                      new)
-                        chg = chg.replace(CRLF, CRLF + length * ' ')
-                        chg = wrap(chg, self.COLS, '', length * ' ', CRLF)
-                        changes_body += ' %s%s' % (chg, CRLF)
+                        l = 7 + len(field)
+#rlrj60: to support html email notification
+#                       chg = wrap('%s => %s' % (old, new), self.COLS - l, '',
+#                                  l * ' ', CRLF)
+#                       changes_body += '  * %s:  %s%s' % (field, chg, CRLF)
+                        chg = wrap('%s &rarr; %s' % (old, new), self.COLS - l, '',
+                                   l * ' ', CRLF)
+                        changes_body += '<li>' + '%s:  %s%s' % (field, chg, CRLF) + '</li>'
                     if newv:
                         change_data[field] = {'oldvalue': old, 'newvalue': new}
-        
+                        
+#rlrj60: to support html email notification
+        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=CRLF)
+#rlrj60: to support html email notification, convert wiki syntax if any to html
+#       ticket_values['description'] = wrap(
+#           ticket_values.get('description', ''), self.COLS,
+#           initial_indent=' ', subsequent_indent=' ', linesep=CRLF)
+##rlrj60:1/24/2011: using new html format
+##def format_to_html(env, context, wikidom, escape_newlines=None):
+        ticket_values['description'] = format_to_html( env     = self.env,
+                                                       context = Context.from_request(self.req, absurls=True),
+                                                       wikidom = ticket_values.get('description', '')
+                                                     )
         ticket_values['new'] = self.newticket
         ticket_values['link'] = link
         
@@ -194,47 +225,76 @@
             })
         NotifyEmail.notify(self, ticket.id, subject)
 
+#rlrj60: to support html email notification, add new get_diffs function
+    def get_diffs(self, req, old_text, new_text, id):
+        diff_style, diff_options, diff_data = get_diff_options(req)
+        diff_context = 3
+        for option in diff_options:
+            if option.startswith('-U'):
+                diff_context = int(option[2:])
+                break
+        if diff_context < 0:
+            diff_context = None
+        diffs = diff_blocks(old_text.splitlines(), new_text.splitlines(), context=diff_context,
+                            tabwidth=2,       ignore_blank_lines=True,
+                            ignore_case=True, ignore_space_changes=True)
+        
+        chrome = Chrome(self.env)
+        loader = TemplateLoader(chrome.get_all_templates_dirs())
+        tmpl = loader.load('diff_div.html')
+        
+        title = "Ticket:%s description changed" %id
+        changes=[{'diffs': diffs, 'props': [],
+                  'title': title, 'href': req.abs_href('ticket/%s' % id) + '?action=diff',
+                  'new': {'path':title, 'rev':'new', 'shortrev': 'new', 'href': ''},
+                  'old': {'path':"",    'rev':'old', 'shortrev': 'old', 'href': ''}}]
+
+        data = chrome.populate_data(req,
+                                    { 'changes':changes , 'no_id':True, 'diff':diff_data,
+                                      'longcol': '', 'shortcol': ''})
+        #FIXME:to use user's setting
+        #diff_data['style']='sidebyside';
+        diff_data['style']='inline';
+
+        data.update({ 'changes':changes , 'no_id':True, 'diff':diff_data,
+                      'longcol': '', 'shortcol': ''})
+
+        stream = tmpl.generate(**data)
+        return Markup(stream.render(method='html', encoding='ascii'))
+
     def format_props(self):
         tkt = self.ticket
+#rlrj60: to support html email notification, add BRCRLF
+        BRCRLF = '<br />' + CRLF
         fields = [f for f in tkt.fields 
                   if f['name'] not in ('summary', 'cc', 'time', 'changetime')]
         width = [0, 0, 0, 0]
         i = 0
-        for f in fields:
-            if f['type'] == 'textarea':
+        for f in [f['name'] for f in fields if f['type'] != 'textarea']:
+            if not tkt.values.has_key(f):
                 continue
-            fname = f['name']
-            if not fname in tkt.values:
-                continue
-            fval = tkt[fname] or ''
+            fval = tkt[f] or ''
             if fval.find('\n') != -1:
                 continue
-            if fname in ['owner', 'reporter']:
-                fval = obfuscate_email_address(fval)
             idx = 2 * (i % 2)
-            width[idx] = max(self.get_text_width(f['label']), width[idx])
-            width[idx + 1] = max(self.get_text_width(fval), width[idx + 1])
+            if len(f) > width[idx]:
+                width[idx] = len(f)
+            if len(fval) > width[idx + 1]:
+                width[idx + 1] = len(fval)
             i += 1
-        width_l = width[0] + width[1] + 5
-        width_r = width[2] + width[3] + 5
-        half_cols = (self.COLS - 1) / 2
-        if width_l + width_r + 1 > self.COLS:
-            if ((width_l > half_cols and width_r > half_cols) or 
-                    (width[0] > half_cols / 2 or width[2] > half_cols / 2)):
-                width_l = half_cols
-                width_r = half_cols
-            elif width_l > width_r:
-                width_l = min((self.COLS - 1) * 2 / 3, width_l)
-                width_r = self.COLS - width_l - 1
-            else:
-                width_r = min((self.COLS - 1) * 2 / 3, width_r)         
-                width_l = self.COLS - width_r - 1
-        sep = width_l * '-' + '+' + width_r * '-'
+        format = (u'%%%is:  %%-%is  |  ' % (width[0], width[1]),
+                  u' %%%is:  %%-%is%s' % (width[2], width[3], CRLF))
+#rlrj60: to support html email notification, add
+        format = ('<tr><th align="right">%s:&nbsp;</th><td style="padding-right: 20px">%s</td>','<th align="right">%s:&nbsp;</th><td>%s</td></tr>'+CRLF)
+        l = (width[0] + width[1] + 5)
+#rlrj60: to support html email notification
+#       sep = l * '-' + '+' + (self.COLS - l) * '-'
+        sep = CRLF
         txt = sep + CRLF
-        cell_tmp = [u'', u'']
+#rlrj60: to support html email notification, add
+        txt = '<table border="0" cellpadding="2" cellspacing="0">' + CRLF
         big = []
         i = 0
-        width_lr = [width_l, width_r]
         for f in [f for f in fields if f['name'] != 'description']:
             fname = f['name']
             if not tkt.values.has_key(fname):
@@ -247,32 +307,25 @@
             else:
                 # Note: f['label'] is a Babel's LazyObject, make sure its
                 # __str__ method won't be called.
-                str_tmp = u'%s:  %s' % (f['label'], unicode(fval))
-                idx = i % 2
-                cell_tmp[idx] += wrap(str_tmp, width_lr[idx] - 2 + 2 * idx,
-                                      (width[2 * idx]
-                                       - self.get_text_width(f['label'])
-                                       + 2 * idx) * ' ',
-                                      2 * ' ', CRLF)
-                cell_tmp[idx] += CRLF
+                txt += format[i % 2] % (f['label'], unicode(fval))
                 i += 1
-        cell_l = cell_tmp[0].splitlines()
-        cell_r = cell_tmp[1].splitlines()
-        for i in range(max(len(cell_l), len(cell_r))):
-            if i >= len(cell_l):
-                cell_l.append(width_l * ' ')
-            elif i >= len(cell_r):
-                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], CRLF)
+        if i % 2:
+            txt += CRLF
         if big:
+#rlrj60: to support html email notification, add
+            txt += '<td colspan="2">&nbsp;</td></tr>' + CRLF
             txt += sep
             for name, value in big:
-                txt += CRLF.join(['', name + ':', value, '', ''])
+#rlrj60: to support html email notification
+#               txt += CRLF.join(['', name + ':', value, '', ''])
+                txt += CRLF.join(['<tr align="left"><td colspan="2"><em>' + name + ':' + '</em></td></tr>', '<tr align="left"><td colspan="2">' + value + '</td></tr>'])
+                """txt += CRLF.join(['', name + ':', value, '', ''])"""
         txt += sep
+#rlrj60: to support html email notification, add
+        txt += '</table>' + CRLF
         return txt
 
+
     def parse_cc(self, txt):
         return filter(lambda x: '@' in x, txt.replace(',', ' ').split())
 
@@ -379,28 +432,11 @@
         hdrs['Message-ID'] = self.get_message_id(dest, self.modtime)
         hdrs['X-Trac-Ticket-ID'] = str(self.ticket.id)
         hdrs['X-Trac-Ticket-URL'] = self.data['ticket']['link']
+#rlrj60: to support html email notification, add
+        hdrs['Content-Type'] = 'text/html; charset=utf-8'
         if not self.newticket:
             msgid = self.get_message_id(dest)
             hdrs['In-Reply-To'] = msgid
             hdrs['References'] = msgid
         NotifyEmail.send(self, torcpts, ccrcpts, hdrs)
 
-    def get_text_width(self, text):
-        ambiwidth = (1, 2)[self.ambiguous_char_width == 'double']
-        text = to_unicode(text)
-
-        if text in self.text_widths:
-            return self.text_widths[text]
-
-        width = 0
-        for ch in text:
-            eaw = east_asian_width(ch)
-            if eaw in 'WF':
-                width += 2
-            elif eaw == 'A':
-                width += ambiwidth
-            else:
-                width += 1
-        self.text_widths[text] = width
-        return width
-
Index: ticket/templates/ticket_notify_email.txt
===================================================================
--- ticket/templates/ticket_notify_email.txt	(revision 2118)
+++ ticket/templates/ticket_notify_email.txt	(working copy)
@@ -1,32 +1,35 @@
-$ticket_body_hdr
-$ticket_props
+<div style="margin:0;padding:0;font-family:Arial,Tahoma,Verdana,Helvetica,sans-serif,sans;">
+  <link type="text/css" rel="stylesheet" href="${project.url or abs_href()}chrome/common/css/diff.css" />
+  <link type="text/css" rel="stylesheet" href="${project.url or abs_href()}chrome/common/css/trac.css" />
+  <link type="text/css" rel="stylesheet" href="${project.url or abs_href()}chrome/common/css/ticket.css" />
+  <a href="${project.url or abs_href()}" style="text-decoration:none;display:block;letter-spacing:1px;font-weight:bold;background-color:#f8f8f8;color:#06c;padding:4px;margin:0;border-bottom:1px solid #eee;">$project.name</a>
+  <a href="$ticket.link" style="text-decoration:none;display:block;background-color:#fbfbfb;color:#555;padding:4px;margin:0;border-bottom:1px solid #ddd;font-weight:bold;font-size:120%;">$ticket_body_hdr</a>
 {% choose ticket.new %}\
 {%   when True %}\
-$ticket.description
+        <div style="font-size:90%;background-color:#ffffdd;border:1px outset #99966;padding:1.0em;">$ticket.description</div>
 {%   end %}\
 {%   otherwise %}\
 {%     if changes_body %}\
-${_('Changes (by %(author)s):', author=change.author)}
-
-$changes_body
+          <fieldset style="font-size:90%;margin:0.25em 0.25em;border-color:#069;border-left:none;border-right:none;">
+             <legend style="color:#069;"><em>Changes</em> (by <strong>$change.author</strong>)</legend>
+             $changes_body
+          </fieldset>
 {%     end %}\
+{%     if change.comment %}\
+          <fieldset style="font-size:90%;margin:0.25em 0.25em;border-color:#065;border-left:none;border-right:none;">
+             <legend style="color:#069;"><em>Comment</em> ${not changes_body and '(by <strong>%s</strong>)' % change.author or ''}</legend>
+             <div style="background-color:#ffffdd;padding:1.0em;">$change.comment</div>
+          </fieldset>
+{%     end %}\
 {%     if changes_descr %}\
-{%       if not changes_body and not change.comment and change.author %}\
-${_('Description changed by %(author)s:', author=change.author)}
-{%       end %}\
-$changes_descr
---
+          <fieldset style="font-size:90%;margin:0.25em 0.25em;border-color:#069;border-left:none;border-right:none;">
+             <legend style="color:#069;"><em>Description changed</em> ${not changes_body and '(by <strong>%s</strong>)' % change.author or ''}</legend>
+             <div style="background-color:#ffffdd;padding:1.0em;">$changes_descr</div>
+          </fieldset>
 {%     end %}\
-{%     if change.comment %}\
-
-${changes_body and _('Comment:') or _('Comment (by %(author)s):', author=change.author)}
-
-$change.comment
-{%     end %}\
 {%   end %}\
 {% end %}\
+  <div style="font-size:80%;color:#999;border-top:1px dotted #999;">$ticket_props</div>
+  <cite style="display:block;padding:4px;background-color:#f0f0f0;color:#999;font-size:90%;border-top:1px dotted #ccc;">$project.descr</cite>
+</div>
 
--- 
-${_('Ticket URL: <%(link)s>', link=ticket.link)}
-$project.name <${project.url or abs_href()}>
-$project.descr
