Index: trac/ticket/api.py
===================================================================
--- trac/ticket/api.py	(revision 6139)
+++ trac/ticket/api.py	(working copy)
@@ -26,7 +26,7 @@
 from trac.util import Ranges
 from trac.util.compat import set, sorted
 from trac.util.datefmt import utc
-from trac.util.text import shorten_line
+from trac.util.text import shorten_line, obfuscate_email_address
 from trac.util.translation import _
 from trac.wiki import IWikiSyntaxProvider, WikiParser
 
Index: trac/ticket/web_ui.py
===================================================================
--- trac/ticket/web_ui.py	(revision 6139)
+++ trac/ticket/web_ui.py	(working copy)
@@ -14,6 +14,7 @@
 #
 # Author: Jonas Borgström <jonas@edgewall.com>
 
+import csv
 from datetime import datetime
 import os
 import pkg_resources
@@ -31,8 +32,9 @@
 from trac.resource import Resource, get_resource_url, \
                          render_resource_link, get_resource_shortname
 from trac.search import ISearchSource, search_to_sql, shorten_result
-from trac.ticket import Milestone, Ticket, TicketSystem, ITicketManipulator
-from trac.ticket import ITicketActionController
+from trac.ticket.api import TicketSystem, ITicketManipulator, \
+                            ITicketActionController
+from trac.ticket.model import Milestone, Ticket
 from trac.ticket.notification import TicketNotifyEmail
 from trac.timeline.api import ITimelineEventProvider, TimelineEvent
 from trac.util import get_reporter_id
@@ -52,16 +54,6 @@
     title = "Invalid Ticket"
 
 
-def cc_list(cc_field):
-    """Split a CC: value in a list of addresses.
-
-    TODO: will become `CcField.cc_list(value)
-    """
-    if not cc_field:
-        return []
-    return [cc.strip() for cc in cc_field.split(',') if cc]
-
-
 class TicketModule(Component):
 
     implements(IContentConverter, INavigationContributor, IRequestHandler,
@@ -111,9 +103,9 @@
 
     def convert_content(self, req, mimetype, ticket, key):
         if key == 'csv':
-            return self.export_csv(ticket, mimetype='text/csv')
+            return self.export_csv(req, ticket, mimetype='text/csv')
         elif key == 'tab':
-            return self.export_csv(ticket, sep='\t',
+            return self.export_csv(req, ticket, sep='\t',
                                    mimetype='text/tab-separated-values')
         elif key == 'rss':
             return self.export_rss(req, ticket)
@@ -705,16 +697,22 @@
 
         return 'diff_view.html', data, None
 
-    def export_csv(self, ticket, sep=',', mimetype='text/plain'):
+    def export_csv(self, req, ticket, sep=',', mimetype='text/plain'):
+        # FIXME: consider dumping history of changes here as well
+        #        as one row of output doesn't seem to be terribly useful...
         content = StringIO()
-        content.write(sep.join(['id'] + [f['name'] for f in
-                                         ticket.fields])
-                      + CRLF)
-        content.write(sep.join([unicode(ticket.id)] +
-                                [ticket.values.get(f['name'], '')
-                                 .replace(sep, '_').replace('\\', '\\\\')
-                                 .replace('\n', '\\n').replace('\r', '\\r')
-                                 for f in ticket.fields]) + CRLF)
+        writer = csv.writer(content, delimiter=sep, quoting=csv.QUOTE_MINIMAL)
+        writer.writerow(['id'] + [unicode(f['name']) for f in ticket.fields])
+
+        context = Context.from_request(req, ticket.resource)
+        cols = [unicode(ticket.id)]
+        for f in ticket.fields:
+            name = f['name']
+            value = ticket.values.get(name, '')
+            if name in ('cc', 'reporter'):
+                value = Chrome(self.env).format_emails(context, value, ' ')
+            cols.append(value.encode('utf-8'))
+        writer.writerow(cols)
         return (content.getvalue(), '%s;charset=utf-8' % mimetype)
 
     def export_rss(self, req, ticket):
@@ -928,6 +926,7 @@
             ticket[key] = field_changes[key]['new']
 
     def _prepare_fields(self, req, ticket):
+        context = Context.from_request(req, ticket.resource)
         fields = []
         for field in ticket.fields:
             name = field['name']
@@ -963,16 +962,11 @@
                     field['options'] = [opt for opt in field['options'] if not
                                         Milestone(self.env, opt).is_completed]
                 milestone = Resource('milestone', ticket[name])
-                context = Context.from_request(req, ticket.resource)
                 field['rendered'] = render_resource_link(self.env, context,
                                                          milestone, 'compact')
             elif name == 'cc':
-                all_cc = cc_list(ticket[name])
-                if not (Chrome(self.env).show_email_addresses or \
-                        'EMAIL_VIEW' in req.perm(ticket.resource)):
-                    all_cc = [obfuscate_email_address(cc) for cc in all_cc]
-                field['rendered'] = ', '.join(all_cc)
-                    
+                emails = Chrome(self.env).format_emails(context, ticket[name])
+                field['rendered'] = emails
             # ensure sane defaults
             field.setdefault('optional', False)
             field.setdefault('options', [])
@@ -1148,7 +1142,8 @@
         old_list, new_list = None, None
         sep = ', '
         if field == 'cc':
-            old_list, new_list = cc_list(old), cc_list(new)
+            chrome = Chrome(self.env)
+            old_list, new_list = chrome.cc_list(old), chrome.cc_list(new)
             if not (Chrome(self.env).show_email_addresses or 
                     'EMAIL_VIEW' in req.perm(ticket.resource)):
                 old_list = [obfuscate_email_address(cc)
Index: trac/ticket/report.py
===================================================================
--- trac/ticket/report.py	(revision 6139)
+++ trac/ticket/report.py	(working copy)
@@ -25,6 +25,7 @@
 
 from trac.core import *
 from trac.db import get_column_names
+from trac.mimeview import Context
 from trac.perm import IPermissionRequestor
 from trac.resource import Resource, ResourceNotFound
 from trac.util import sorted
@@ -264,12 +265,16 @@
         if id > 0:
             title = '{%i} %s' % (id, title)
 
+        report_resource = Resource('report', id)
+        context = Context.from_request(req, report_resource)
         data = {'action': 'view', 'title': title,
-                'report': Resource('report', id),
+                'report': {'id': id, 'resource': report_resource},
+                'context': context,
                 'title': title, 'description': description,
                 'args': args, 'message': None}
         try:
             cols, results = self.execute_report(req, db, id, sql, args)
+            results = [list(row) for row in results]
         except Exception, e:
             data['message'] = _('Report execution failed: %(error)s',
                                 error=to_unicode(e))
@@ -324,13 +329,14 @@
             cell_groups = []
             row = {'cell_groups': cell_groups}
             realm = 'ticket'
+            email_cells = []
             for header_group in header_groups:
                 cell_group = []
                 for header in header_group:
                     value = unicode(result[col_idx])
+                    cell = {'value': value, 'header': header, 'index': col_idx}
+                    col = header['col']
                     col_idx += 1
-                    cell = {'value': value, 'header': header}
-                    col = header['col']
                     # Detect and create new group
                     if col == '__group__' and value != prev_group_value:
                         prev_group_value = value
@@ -344,8 +350,8 @@
                         row['id'] = value
                     # Special casing based on column name
                     col = col.strip('_')
-                    if col == 'reporter':
-                        cell['author'] = value
+                    if col in ('reporter', 'cc'):
+                        email_cells.append(cell)
                     elif col == 'realm':
                         realm = value
                     cell_group.append(cell)
@@ -353,6 +359,11 @@
             resource = Resource(realm, row.get('id'))
             if 'view' not in req.perm(resource):
                 continue
+            if email_cells:
+                for cell in email_cells:
+                    emails = Chrome(self.env).format_emails(context(resource),
+                                                            cell['value'])
+                    result[cell['index']] = cell['value'] = emails
             row['resource'] = resource
             if row_groups:
                 row_group = row_groups[-1][1]
@@ -378,6 +389,8 @@
             self.add_alternate_links(req, args)
 
         if format == 'rss':
+            data['context'] = Context.from_request(req, report_resource,
+                                                   absurls=True)
             return 'report.rss', data, 'application/rss+xml'
         elif format == 'csv':
             filename = id and 'report_%s.csv' % id or 'report.csv'
Index: trac/ticket/query.py
===================================================================
--- trac/ticket/query.py	(revision 6139)
+++ trac/ticket/query.py	(working copy)
@@ -787,13 +787,22 @@
         content = StringIO()
         cols = query.get_columns()
         writer = csv.writer(content, delimiter=sep)
+        writer = csv.writer(content, delimiter=sep, quoting=csv.QUOTE_MINIMAL)
         writer.writerow([unicode(c).encode('utf-8') for c in cols])
 
+        context = Context.from_request(req)
         results = query.execute(req, self.env.get_db_cnx())
         for result in results:
-            if 'TICKET_VIEW' in req.perm('ticket', result['id']):
-                writer.writerow([unicode(result[col]).encode('utf-8')
-                                 for col in cols])
+            ticket = Resource('ticket', result['id'])
+            if 'TICKET_VIEW' in req.perm(ticket):
+                values = []
+                for col in cols:
+                    value = result[col]
+                    if col in ('cc', 'reporter'):
+                        value = Chrome(self.env).format_emails(context(ticket),
+                                                               value)
+                    values.append(unicode(value).encode('utf-8'))
+                writer.writerow(values)
         return (content.getvalue(), '%s;charset=utf-8' % mimetype)
 
     def export_rss(self, req, query):
@@ -806,7 +815,6 @@
                                                    or None),
                                         row=query.rows, 
                                         **query.constraints)
-
         data = {
             'context': Context.from_request(req, 'query', absurls=True),
             'results': results,
Index: trac/ticket/templates/report.rss
===================================================================
--- trac/ticket/templates/report.rss	(revision 6139)
+++ trac/ticket/templates/report.rss	(working copy)
@@ -30,7 +30,7 @@
               <title>#$row.id: $cell.value</title>
             </py:when>
             <py:when test="col == 'description'">
-              <description>${unicode(wiki_to_html(row.context, cell.value, href=abs_href))}</description>
+              <description>${unicode(wiki_to_html(context(row.resource), cell.value))}</description>
             </py:when>
           </py:choose>
         </py:with>
Index: trac/ticket/templates/report_view.html
===================================================================
--- trac/ticket/templates/report_view.html	(revision 6139)
+++ trac/ticket/templates/report_view.html	(working copy)
@@ -27,23 +27,23 @@
       </h1>
 
       <div py:if="description" id="description" xml:space="preserve">
-        ${wiki_to_html(context(report), description)}
+        ${wiki_to_html(context, description)}
       </div>
 
       <div py:if="report.id != -1" class="buttons">
-        <form py:if="'REPORT_MODIFY' in perm(report)" action="" method="get">
+        <form py:if="'REPORT_MODIFY' in perm(report.resource)" action="" method="get">
           <div>
             <input type="hidden" name="action" value="edit" />
             <input type="submit" value="Edit report" accesskey="e" />
           </div>
         </form>
-        <form py:if="'REPORT_CREATE' in perm(report)" action="" method="get">
+        <form py:if="'REPORT_CREATE' in perm(report.resource)" action="" method="get">
           <div>
             <input type="hidden" name="action" value="copy" />
             <input type="submit" value="Copy report" />
           </div>
         </form>
-        <form py:if="'REPORT_DELETE' in perm(report)" action="" method="get">
+        <form py:if="'REPORT_DELETE' in perm(report.resource)" action="" method="get">
           <div>
             <input type="hidden" name="action" value="delete" />
             <input type="submit" value="Delete report" />
@@ -156,7 +156,7 @@
         </table>
       </py:for>
 
-      <div py:if="report.id == -1 and 'REPORT_CREATE' in perm(report)" class="buttons">
+      <div py:if="report.id == -1 and 'REPORT_CREATE' in perm(report.resource)" class="buttons">
         <form action="" method="get">
           <div>
             <input type="hidden" name="action" value="new" />
Index: trac/ticket/templates/query_results.html
===================================================================
--- trac/ticket/templates/query_results.html	(revision 6139)
+++ trac/ticket/templates/query_results.html	(working copy)
@@ -53,6 +53,7 @@
                       <a py:when="name == 'summary'" href="$result.href" title="View ticket">$value</a>
                       <span py:when="isinstance(value, datetime)">${format_datetime(value)}</span>
                       <span py:when="name == 'reporter'">${authorinfo(value)}</span>
+                      <span py:when="name == 'cc'">${format_emails(ticket_context, value)}</span>
                       <span py:when="name == 'owner' and value">${authorinfo(value)}</span>
                       <span py:otherwise="">$value</span>
                     </td>
Index: trac/wiki/tests/wiki-tests.txt
===================================================================
--- trac/wiki/tests/wiki-tests.txt	(revision 6139)
+++ trac/wiki/tests/wiki-tests.txt	(working copy)
@@ -306,6 +306,22 @@
 <i>RFCs von <a class="ext-link" href="ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt"><span class="icon">ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt</span></a></i>
 </p>
 ------------------------------
+==============================  mailto: links
+Author: mailto:cboos@neuf.fr, 
+i.e. [mailto:cboos@neuf.fr me]
+------------------------------
+<p>
+Author: <a class="mail-link" href="mailto:cboos@neuf.fr"><span class="icon">mailto:cboos@neuf.fr</span></a>, 
+i.e. <a class="mail-link" href="mailto:cboos@neuf.fr"><span class="icon">me</span></a>
+</p>
+------------------------------
+============================== Arbitrary protocol Link
+''RFCs von ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt''
+------------------------------
+<p>
+<i>RFCs von <a class="ext-link" href="ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt"><span class="icon">ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt</span></a></i>
+</p>
+------------------------------
 ============================== Generic InterTrac links
 th:roadmap
 th:roadmap:
@@ -724,7 +740,7 @@
  * item 2
    item 2 line 2
 Paragraph
-============================== Changelog sample
+============================== Changelog sample (and e-mail link)
 2003-09-18 23:26  Joe Bar <joeb@gloogle.gom>
 
 	* src/code.py: Fix problem with obsolete use of
@@ -735,7 +751,7 @@
 Paragraph
 ------------------------------
 <p>
-2003-09-18 23:26  Joe Bar &lt;joeb@gloogle.gom&gt;
+2003-09-18 23:26  Joe Bar &lt;<a class="mail-link" href="mailto:joeb@gloogle.gom"><span class="icon">joeb@gloogle.gom</span></a>&gt;
 </p>
 <ul><li>src/code.py: Fix problem with obsolete use of
 backslash in symbols.
Index: trac/wiki/parser.py
===================================================================
--- trac/wiki/parser.py	(revision 6139)
+++ trac/wiki/parser.py	(working copy)
@@ -72,6 +72,8 @@
     # Rules provided by IWikiSyntaxProviders will be inserted here
 
     _post_rules = [
+        # e-mails
+        r"(?P<email>\w[\w.]+@\w[\w.]+\w)",
         # > ...
         r"(?P<citation>^(?P<cdepth>>(?: *>)*))",
         # &, < and > to &amp;, &lt; and &gt;
Index: trac/wiki/formatter.py
===================================================================
--- trac/wiki/formatter.py	(revision 6139)
+++ trac/wiki/formatter.py	(working copy)
@@ -284,6 +284,16 @@
 
     # -- Post- IWikiSyntaxProvider rules
 
+    # E-mails
+
+    def _email_formatter(self, match, fullmatch):
+        from trac.web.chrome import Chrome
+        omatch = Chrome(self.env).format_emails(self.context, match)
+        if omatch == match: # not obfuscated, make a link
+            return self._make_mail_link('mailto:'+match, match)
+        else:
+            return omatch
+
     # HTML escape of &, < and >
 
     def _htmlescape_formatter(self, match, fullmatch):
@@ -341,7 +351,13 @@
         elif target.startswith('//'):
             return self._make_ext_link(ns+':'+target, label)
         elif ns == "mailto":
-            return self._make_mail_link('mailto:'+target, label)
+            from trac.web.chrome import Chrome
+            otarget = Chrome(self.env).format_emails(self.context, target)
+            olabel = Chrome(self.env).format_emails(self.context, label)
+            if (otarget, olabel) == (target, label):
+                return self._make_mail_link('mailto:'+target, label)
+            else:
+                return olabel or otarget
         else:
             return self._make_intertrac_link(ns, target, label) or \
                    self._make_interwiki_link(ns, target, label) or \
Index: trac/web/chrome.py
===================================================================
--- trac/web/chrome.py	(revision 6139)
+++ trac/web/chrome.py	(working copy)
@@ -535,6 +535,7 @@
             'authname': req and req.authname or '<trac>',
             'show_email_addresses': show_email_addresses,
             'format_author': partial(self.format_author, req),
+            'format_emails': self.format_emails,
 
             # Date/time formatting
             'dateinfo': dateinfo,
@@ -624,6 +625,27 @@
             req.chrome['scripts'] = scripts
             raise
 
+    # E-mail formatting utilities
+
+    def cc_list(self, cc_field):
+        """Split a CC: value in a list of addresses."""
+        if not cc_field:
+            return []
+        return [cc.strip() for cc in cc_field.split(',') if cc]
+
+    def format_emails(self, context, value, sep=', '):
+        """Normalize a list of e-mails and obfuscate them if needed.
+
+        :param context: the context in which the check for obfuscation should
+                        be done
+        :param value: a string containing a comma-separated list of e-mails
+        :param sep: the separator to use when rendering the list again
+        """
+        all_cc = self.cc_list(value)
+        if not (self.show_email_addresses or 'EMAIL_VIEW' in context.perm):
+            all_cc = [obfuscate_email_address(cc) for cc in all_cc]
+        return sep.join(all_cc)
+    
     def format_author(self, req, author):
         if self.show_email_addresses or not req or 'EMAIL_VIEW' in req.perm:
             return author

