# HG changeset patch
# User Brodie Rao <brodie@bitheap.org>
# Date 1291452498 -39600
# Node ID 876fd3c1a04fb41f27fc552f0171a6d5573caa47
# Parent  e08464eca04c6a8ce8c09dc029e8b287a5291903
Added support for sending Django error emails in HTML using the debug exception template

diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py
--- a/django/core/mail/__init__.py
+++ b/django/core/mail/__init__.py
@@ -83,22 +83,30 @@ def send_mass_mail(datatuple, fail_silen
     return connection.send_messages(messages)
 
 
-def mail_admins(subject, message, fail_silently=False, connection=None):
+def mail_admins(subject, message, fail_silently=False, connection=None,
+                html_message=None):
     """Sends a message to the admins, as defined by the ADMINS setting."""
     if not settings.ADMINS:
         return
-    EmailMessage(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject), message,
-                 settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
-                 connection=connection).send(fail_silently=fail_silently)
+    mail = EmailMultiAlternatives(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject),
+                message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
+                connection=connection)
+    if html_message:
+        mail.attach_alternative(html_message, 'text/html')
+    mail.send(fail_silently=fail_silently)
 
 
-def mail_managers(subject, message, fail_silently=False, connection=None):
+def mail_managers(subject, message, fail_silently=False, connection=None,
+                  html_message=None):
     """Sends a message to the managers, as defined by the MANAGERS setting."""
     if not settings.MANAGERS:
         return
-    EmailMessage(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject), message,
-                 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS],
-                 connection=connection).send(fail_silently=fail_silently)
+    mail = EmailMultiAlternatives(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject),
+                message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
+                connection=connection)
+    if html_message:
+        mail.attach_alternative(html_message, 'text/html')
+    mail.send(fail_silently=fail_silently)
 
 
 class SMTPConnection(_SMTPConnection):
diff --git a/django/utils/log.py b/django/utils/log.py
--- a/django/utils/log.py
+++ b/django/utils/log.py
@@ -56,6 +56,7 @@ class AdminEmailHandler(logging.Handler)
     def emit(self, record):
         import traceback
         from django.conf import settings
+        from django.views.debug import ExceptionReporter
 
         try:
             if sys.version_info < (2,5):
@@ -75,12 +76,18 @@ class AdminEmailHandler(logging.Handler)
             request_repr = repr(request)
         except:
             subject = 'Error: Unknown URL'
+            request = None
             request_repr = "Request repr() unavailable"
 
         if record.exc_info:
+            exc_info = record.exc_info
             stack_trace = '\n'.join(traceback.format_exception(*record.exc_info))
         else:
+            exc_info = ()
             stack_trace = 'No stack trace available'
 
         message = "%s\n\n%s" % (stack_trace, request_repr)
-        mail.mail_admins(subject, message, fail_silently=True)
+        reporter = ExceptionReporter(request, *exc_info, is_email=True)
+        html_message = reporter.get_traceback_html()
+        mail.mail_admins(subject, message, fail_silently=True,
+                         html_message=html_message)
diff --git a/django/views/debug.py b/django/views/debug.py
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -62,11 +62,12 @@ class ExceptionReporter:
     """
     A class to organize and coordinate reporting on exceptions.
     """
-    def __init__(self, request, exc_type, exc_value, tb):
+    def __init__(self, request, exc_type, exc_value, tb, is_email=False):
         self.request = request
         self.exc_type = exc_type
         self.exc_value = exc_value
         self.tb = tb
+        self.is_email = is_email
 
         self.template_info = None
         self.template_does_not_exist = False
@@ -118,6 +119,7 @@ class ExceptionReporter:
         from django import get_version
         t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
         c = Context({
+            'is_email': self.is_email,
             'exception_type': self.exc_type.__name__,
             'exception_value': smart_unicode(self.exc_value, errors='replace'),
             'unicode_hint': unicode_hint,
@@ -353,6 +355,7 @@ TECHNICAL_500_TEMPLATE = """
     span.commands a:link {color:#5E5694;}
     pre.exception_value { font-family: sans-serif; color: #666; font-size: 1.5em; margin: 10px 0 10px 0; }
   </style>
+  {% if not is_email %}
   <script type="text/javascript">
   //<!--
     function getElementsByClassName(oElm, strTagName, strClassName){
@@ -408,10 +411,11 @@ TECHNICAL_500_TEMPLATE = """
     }
     //-->
   </script>
+  {% endif %}
 </head>
 <body>
 <div id="summary">
-  <h1>{{ exception_type }} at {{ request.path_info|escape }}</h1>
+  <h1>{{ exception_type }}{% if request %} at {{ request.path_info|escape }}{% endif %}</h1>
   <pre class="exception_value">{{ exception_value|force_escape }}</pre>
   <table class="meta">
     <tr>
@@ -498,7 +502,7 @@ TECHNICAL_500_TEMPLATE = """
 </div>
 {% endif %}
 <div id="traceback">
-  <h2>Traceback <span class="commands"><a href="#" onclick="return switchPastebinFriendly(this);">Switch to copy-and-paste view</a></span></h2>
+  <h2>Traceback <span class="commands">{% if not is_email %}<a href="#" onclick="return switchPastebinFriendly(this);">Switch to copy-and-paste view</a></span>{% endif %}</h2>
   {% autoescape off %}
   <div id="browserTraceback">
     <ul class="traceback">
@@ -545,16 +549,19 @@ TECHNICAL_500_TEMPLATE = """
   </div>
   {% endautoescape %}
   <form action="http://dpaste.com/" name="pasteform" id="pasteform" method="post">
+{% if not is_email %}
   <div id="pastebinTraceback" class="pastebin">
     <input type="hidden" name="language" value="PythonConsole">
-    <input type="hidden" name="title" value="{{ exception_type|escape }} at {{ request.path_info|escape }}">
+    <input type="hidden" name="title" value="{{ exception_type|escape }}{% if request %} at {{ request.path_info|escape }}{% endif %}">
     <input type="hidden" name="source" value="Django Dpaste Agent">
     <input type="hidden" name="poster" value="Django">
     <textarea name="content" id="traceback_area" cols="140" rows="25">
 Environment:
 
+{% if request %}
 Request Method: {{ request.META.REQUEST_METHOD }}
 Request URL: {{ request.build_absolute_uri|escape }}
+{% endif %}
 Django Version: {{ django_version_info }}
 Python Version: {{ sys_version_info }}
 Installed Applications:
@@ -581,7 +588,7 @@ Traceback:
 {% for frame in frames %}File "{{ frame.filename|escape }}" in {{ frame.function|escape }}
 {% if frame.context_line %}  {{ frame.lineno }}. {{ frame.context_line|escape }}{% endif %}
 {% endfor %}
-Exception Type: {{ exception_type|escape }} at {{ request.path_info|escape }}
+Exception Type: {{ exception_type|escape }}{% if request %} at {{ request.path_info|escape }}{% endif %}
 Exception Value: {{ exception_value|force_escape }}
 </textarea>
   <br><br>
@@ -589,10 +596,12 @@ Exception Value: {{ exception_value|forc
   </div>
 </form>
 </div>
+{% endif %}
 
 <div id="requestinfo">
   <h2>Request information</h2>
 
+{% if request %}
   <h3 id="get-info">GET</h3>
   {% if request.GET %}
     <table class="req">
@@ -698,6 +707,7 @@ Exception Value: {{ exception_value|forc
       {% endfor %}
     </tbody>
   </table>
+{% endif %}
 
   <h3 id="settings-info">Settings</h3>
   <h4>Using settings module <code>{{ settings.SETTINGS_MODULE }}</code></h4>
diff --git a/docs/topics/email.txt b/docs/topics/email.txt
--- a/docs/topics/email.txt
+++ b/docs/topics/email.txt
@@ -109,7 +109,7 @@ a single connection for all of its messa
 mail_admins()
 =============
 
-.. function:: mail_admins(subject, message, fail_silently=False, connection=None)
+.. function:: mail_admins(subject, message, fail_silently=False, connection=None, html_message=None)
 
 ``django.core.mail.mail_admins()`` is a shortcut for sending an e-mail to the
 site admins, as defined in the :setting:`ADMINS` setting.
@@ -122,10 +122,16 @@ The "From:" header of the e-mail will be
 
 This method exists for convenience and readability.
 
+.. versionchanged:: 1.3
+
+If ``html_message`` is provided, the resulting e-mail will be a
+multipart/alternative e-mail with ``message`` as the "text/plain"
+content type and ``html_message`` as the "text/html" content type.
+
 mail_managers()
 ===============
 
-.. function:: mail_managers(subject, message, fail_silently=False, connection=None)
+.. function:: mail_managers(subject, message, fail_silently=False, connection=None, html_message=None)
 
 ``django.core.mail.mail_managers()`` is just like ``mail_admins()``, except it
 sends an e-mail to the site managers, as defined in the :setting:`MANAGERS`
diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py
--- a/tests/regressiontests/mail/tests.py
+++ b/tests/regressiontests/mail/tests.py
@@ -364,6 +364,31 @@ class MailTests(TestCase):
         settings.ADMINS = old_admins
         settings.MANAGERS = old_managers
 
+    def test_html_mail(self):
+        """Test html_message argument to mail_admins and mail_managers"""
+        old_admins = settings.ADMINS
+        old_managers = settings.MANAGERS
+        settings.ADMINS = settings.MANAGERS = [('nobody','nobody@example.com')]
+
+        mail.outbox = []
+        mail_managers('Subject', 'Content', html_message='HTML Content')
+        self.assertEqual(len(mail.outbox), 1)
+        message = mail.outbox[0]
+        self.assertEqual(message.subject, '[Django] Subject')
+        self.assertEqual(message.body, 'Content')
+        self.assertEqual(message.alternatives, [('HTML Content', 'text/html')])
+
+        mail.outbox = []
+        mail_admins('Subject', 'Content', html_message='HTML Content')
+        self.assertEqual(len(mail.outbox), 1)
+        message = mail.outbox[0]
+        self.assertEqual(message.subject, '[Django] Subject')
+        self.assertEqual(message.body, 'Content')
+        self.assertEqual(message.alternatives, [('HTML Content', 'text/html')])
+
+        settings.ADMINS = old_admins
+        settings.MANAGERS = old_managers
+
     def test_idn_validation(self):
         """Test internationalized email adresses"""
         # Regression for #14301.
