diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py index 8d064223cd9..a3ad837bc9e 100644 --- a/django/core/mail/__init__.py +++ b/django/core/mail/__init__.py @@ -83,22 +83,30 @@ def send_mass_mail(datatuple, fail_silently=False, auth_user=None, 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.MANAGERS], + 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 index b9d32e339b2..d3c8e67a307 100644 --- 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 index 7050ea38fb6..5c75a49e9ac 100644 --- 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, @@ -324,7 +326,7 @@ TECHNICAL_500_TEMPLATE = """ table.vars { margin:5px 0 2px 40px; } table.vars td, table.req td { font-family:monospace; } table td.code { width:100%; } - table td.code div { overflow:hidden; } + table td.code pre { overflow:hidden; } table.source th { color:#666; } table.source td { font-family:monospace; white-space:pre; border-bottom:1px solid #eee; } ul.traceback { list-style-type:none; } @@ -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; } + {% if not is_email %} + {% endif %}
-

{{ exception_type }} at {{ request.path_info|escape }}

+

{{ exception_type }}{% if request %} at {{ request.path_info|escape }}{% endif %}

{{ exception_value|force_escape }}
@@ -448,7 +452,7 @@ TECHNICAL_500_TEMPLATE = """ - + @@ -498,7 +502,7 @@ TECHNICAL_500_TEMPLATE = """ {% endif %}
-

Traceback Switch to copy-and-paste view

+

Traceback {% if not is_email %}Switch to copy-and-paste view{% endif %}

{% autoescape off %}
    @@ -508,19 +512,23 @@ TECHNICAL_500_TEMPLATE = """ {% if frame.context_line %}
    - {% if frame.pre_context %} -
      {% for line in frame.pre_context %}
    1. {{ line|escape }}
    2. {% endfor %}
    + {% if frame.pre_context and not is_email %} +
      {% for line in frame.pre_context %}
    1. {{ line|escape }}
    2. {% endfor %}
    {% endif %} -
    1. {{ frame.context_line|escape }} ...
    - {% if frame.post_context %} -
      {% for line in frame.post_context %}
    1. {{ line|escape }}
    2. {% endfor %}
    +
    1. {{ frame.context_line|escape }}
      {% if not is_email %} ...{% endif %}
    + {% if frame.post_context and not is_email %} +
      {% for line in frame.post_context %}
    1. {{ line|escape }}
    2. {% endfor %}
    {% endif %}
    {% endif %} {% if frame.vars %}
    - Local vars + {% if is_email %} +

    Local Vars

    + {% else %} + Local vars + {% endif %}
Python Path:{{ sys_path }}
{{ sys_path|pprint }}
Server time:
@@ -533,7 +541,7 @@ TECHNICAL_500_TEMPLATE = """ {% for var in frame.vars|dictsort:"0" %} - + {% endfor %} @@ -545,16 +553,19 @@ TECHNICAL_500_TEMPLATE = """ {% endautoescape %} +{% if not is_email %}
- +

@@ -589,10 +600,12 @@ Exception Value: {{ exception_value|force_escape }}
+{% endif %}

Request information

+{% if request %}

GET

{% if request.GET %}
{{ var.0|force_escape }}
{{ var.1|pprint|force_escape }}
{{ var.1|pprint|force_escape }}
@@ -606,7 +619,7 @@ Exception Value: {{ exception_value|force_escape }} {% for var in request.GET.items %} - + {% endfor %} @@ -628,7 +641,7 @@ Exception Value: {{ exception_value|force_escape }} {% for var in request.POST.items %} - + {% endfor %} @@ -649,7 +662,7 @@ Exception Value: {{ exception_value|force_escape }} {% for var in request.FILES.items %} - + {% endfor %} @@ -672,7 +685,7 @@ Exception Value: {{ exception_value|force_escape }} {% for var in request.COOKIES.items %} - + {% endfor %} @@ -693,11 +706,12 @@ Exception Value: {{ exception_value|force_escape }} {% for var in request.META.items|dictsort:"0" %} - + {% endfor %}
{{ var.0 }}
{{ var.1|pprint }}
{{ var.1|pprint }}
{{ var.0 }}
{{ var.1|pprint }}
{{ var.1|pprint }}
{{ var.0 }}
{{ var.1|pprint }}
{{ var.1|pprint }}
{{ var.0 }}
{{ var.1|pprint }}
{{ var.1|pprint }}
{{ var.0 }}
{{ var.1|pprint }}
{{ var.1|pprint }}
+{% endif %}

Settings

Using settings module {{ settings.SETTINGS_MODULE }}

@@ -712,7 +726,7 @@ Exception Value: {{ exception_value|force_escape }} {% for var in settings.items|dictsort:"0" %} {{ var.0 }} -
{{ var.1|pprint }}
+
{{ var.1|pprint }}
{% endfor %} diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 4e01fca9722..79ab8b3f16a 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -163,6 +163,12 @@ requests. These include: * Support for _HTTPOnly cookies. + * mail_admins() and mail_managers() now support easily attaching + HTML content to messages. + + * Error emails now include more of the detail and formatting of + the debug server error page. + .. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly .. _backwards-incompatible-changes-1.3: diff --git a/docs/topics/email.txt b/docs/topics/email.txt index c5c848c67a8..ad148a5a6fc 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -109,7 +109,7 @@ a single connection for all of its messages. This makes 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 the value of the 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 index a84417e21b1..327159eb7f7 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -232,7 +232,7 @@ class MailTests(TestCase): self.assertEqual(len(mail.outbox), 2) self.assertEqual(mail.outbox[0].subject, 'Subject') self.assertEqual(mail.outbox[1].subject, 'Subject 2') - + # Make sure that multiple locmem connections share mail.outbox mail.outbox = [] connection2 = locmem.EmailBackend() @@ -364,6 +364,36 @@ class MailTests(TestCase): settings.ADMINS = old_admins settings.MANAGERS = old_managers + def test_html_mail_admins(self): + """Test html_message argument to mail_admins and mail_managers""" + old_admins = settings.ADMINS + settings.ADMINS = [('nobody','nobody@example.com')] + + 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 + + def test_html_mail_managers(self): + """Test html_message argument to mail_admins and mail_managers""" + old_managers = settings.MANAGERS + 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')]) + + settings.MANAGERS = old_managers + def test_idn_validation(self): """Test internationalized email adresses""" # Regression for #14301.