Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #10863 -- Added HTML support to mail_managers() and mail_admins…

…(), and used this to provide more and prettier detail in error emails. Thanks to boxed for the suggestion, and to Rob Hudson and Brodie Rao for their work on the patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14844 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 29c4a578af58f6da7c77830a0ff99260f2338d36 1 parent b407de3
Russell Keith-Magee authored December 06, 2010
24  django/core/mail/__init__.py
@@ -83,22 +83,30 @@ def send_mass_mail(datatuple, fail_silently=False, auth_user=None,
83 83
     return connection.send_messages(messages)
84 84
 
85 85
 
86  
-def mail_admins(subject, message, fail_silently=False, connection=None):
  86
+def mail_admins(subject, message, fail_silently=False, connection=None,
  87
+                html_message=None):
87 88
     """Sends a message to the admins, as defined by the ADMINS setting."""
88 89
     if not settings.ADMINS:
89 90
         return
90  
-    EmailMessage(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject), message,
91  
-                 settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
92  
-                 connection=connection).send(fail_silently=fail_silently)
  91
+    mail = EmailMultiAlternatives(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject),
  92
+                message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
  93
+                connection=connection)
  94
+    if html_message:
  95
+        mail.attach_alternative(html_message, 'text/html')
  96
+    mail.send(fail_silently=fail_silently)
93 97
 
94 98
 
95  
-def mail_managers(subject, message, fail_silently=False, connection=None):
  99
+def mail_managers(subject, message, fail_silently=False, connection=None,
  100
+                  html_message=None):
96 101
     """Sends a message to the managers, as defined by the MANAGERS setting."""
97 102
     if not settings.MANAGERS:
98 103
         return
99  
-    EmailMessage(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject), message,
100  
-                 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS],
101  
-                 connection=connection).send(fail_silently=fail_silently)
  104
+    mail = EmailMultiAlternatives(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject),
  105
+                message, settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS],
  106
+                connection=connection)
  107
+    if html_message:
  108
+        mail.attach_alternative(html_message, 'text/html')
  109
+    mail.send(fail_silently=fail_silently)
102 110
 
103 111
 
104 112
 class SMTPConnection(_SMTPConnection):
9  django/utils/log.py
@@ -56,6 +56,7 @@ class AdminEmailHandler(logging.Handler):
56 56
     def emit(self, record):
57 57
         import traceback
58 58
         from django.conf import settings
  59
+        from django.views.debug import ExceptionReporter
59 60
 
60 61
         try:
61 62
             if sys.version_info < (2,5):
@@ -75,12 +76,18 @@ def emit(self, record):
75 76
             request_repr = repr(request)
76 77
         except:
77 78
             subject = 'Error: Unknown URL'
  79
+            request = None
78 80
             request_repr = "Request repr() unavailable"
79 81
 
80 82
         if record.exc_info:
  83
+            exc_info = record.exc_info
81 84
             stack_trace = '\n'.join(traceback.format_exception(*record.exc_info))
82 85
         else:
  86
+            exc_info = ()
83 87
             stack_trace = 'No stack trace available'
84 88
 
85 89
         message = "%s\n\n%s" % (stack_trace, request_repr)
86  
-        mail.mail_admins(subject, message, fail_silently=True)
  90
+        reporter = ExceptionReporter(request, *exc_info, is_email=True)
  91
+        html_message = reporter.get_traceback_html()
  92
+        mail.mail_admins(subject, message, fail_silently=True,
  93
+                         html_message=html_message)
54  django/views/debug.py
@@ -62,11 +62,12 @@ class ExceptionReporter:
62 62
     """
63 63
     A class to organize and coordinate reporting on exceptions.
64 64
     """
65  
-    def __init__(self, request, exc_type, exc_value, tb):
  65
+    def __init__(self, request, exc_type, exc_value, tb, is_email=False):
66 66
         self.request = request
67 67
         self.exc_type = exc_type
68 68
         self.exc_value = exc_value
69 69
         self.tb = tb
  70
+        self.is_email = is_email
70 71
 
71 72
         self.template_info = None
72 73
         self.template_does_not_exist = False
@@ -118,6 +119,7 @@ def get_traceback_html(self):
118 119
         from django import get_version
119 120
         t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
120 121
         c = Context({
  122
+            'is_email': self.is_email,
121 123
             'exception_type': self.exc_type.__name__,
122 124
             'exception_value': smart_unicode(self.exc_value, errors='replace'),
123 125
             'unicode_hint': unicode_hint,
@@ -324,7 +326,7 @@ def empty_urlconf(request):
324 326
     table.vars { margin:5px 0 2px 40px; }
325 327
     table.vars td, table.req td { font-family:monospace; }
326 328
     table td.code { width:100%; }
327  
-    table td.code div { overflow:hidden; }
  329
+    table td.code pre { overflow:hidden; }
328 330
     table.source th { color:#666; }
329 331
     table.source td { font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
330 332
     ul.traceback { list-style-type:none; }
@@ -353,6 +355,7 @@ def empty_urlconf(request):
353 355
     span.commands a:link {color:#5E5694;}
354 356
     pre.exception_value { font-family: sans-serif; color: #666; font-size: 1.5em; margin: 10px 0 10px 0; }
355 357
   </style>
  358
+  {% if not is_email %}
356 359
   <script type="text/javascript">
357 360
   //<!--
358 361
     function getElementsByClassName(oElm, strTagName, strClassName){
@@ -408,10 +411,11 @@ def empty_urlconf(request):
408 411
     }
409 412
     //-->
410 413
   </script>
  414
+  {% endif %}
411 415
 </head>
412 416
 <body>
413 417
 <div id="summary">
414  
-  <h1>{{ exception_type }} at {{ request.path_info|escape }}</h1>
  418
+  <h1>{{ exception_type }}{% if request %} at {{ request.path_info|escape }}{% endif %}</h1>
415 419
   <pre class="exception_value">{{ exception_value|force_escape }}</pre>
416 420
   <table class="meta">
417 421
     <tr>
@@ -448,7 +452,7 @@ def empty_urlconf(request):
448 452
     </tr>
449 453
     <tr>
450 454
       <th>Python Path:</th>
451  
-      <td>{{ sys_path }}</td>
  455
+      <td><pre>{{ sys_path|pprint }}</pre></td>
452 456
     </tr>
453 457
     <tr>
454 458
       <th>Server time:</th>
@@ -498,7 +502,7 @@ def empty_urlconf(request):
498 502
 </div>
499 503
 {% endif %}
500 504
 <div id="traceback">
501  
-  <h2>Traceback <span class="commands"><a href="#" onclick="return switchPastebinFriendly(this);">Switch to copy-and-paste view</a></span></h2>
  505
+  <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>
502 506
   {% autoescape off %}
503 507
   <div id="browserTraceback">
504 508
     <ul class="traceback">
@@ -508,19 +512,23 @@ def empty_urlconf(request):
508 512
 
509 513
           {% if frame.context_line %}
510 514
             <div class="context" id="c{{ frame.id }}">
511  
-              {% if frame.pre_context %}
512  
-                <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}">{% for line in frame.pre_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line|escape }}</li>{% endfor %}</ol>
  515
+              {% if frame.pre_context and not is_email %}
  516
+                <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}">{% for line in frame.pre_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ line|escape }}</pre></li>{% endfor %}</ol>
513 517
               {% endif %}
514  
-              <ol start="{{ frame.lineno }}" class="context-line"><li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ frame.context_line|escape }} <span>...</span></li></ol>
515  
-              {% if frame.post_context %}
516  
-                <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">{% for line in frame.post_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line|escape }}</li>{% endfor %}</ol>
  518
+              <ol start="{{ frame.lineno }}" class="context-line"><li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ frame.context_line|escape }}</pre>{% if not is_email %} <span>...</span>{% endif %}</li></ol>
  519
+              {% if frame.post_context and not is_email  %}
  520
+                <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">{% for line in frame.post_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')"><pre>{{ line|escape }}</pre></li>{% endfor %}</ol>
517 521
               {% endif %}
518 522
             </div>
519 523
           {% endif %}
520 524
 
521 525
           {% if frame.vars %}
522 526
             <div class="commands">
523  
-                <a href="#" onclick="return varToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Local vars</a>
  527
+                {% if is_email %}
  528
+                    <h2>Local Vars</h2>
  529
+                {% else %}
  530
+                    <a href="#" onclick="return varToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Local vars</a>
  531
+                {% endif %}
524 532
             </div>
525 533
             <table class="vars" id="v{{ frame.id }}">
526 534
               <thead>
@@ -533,7 +541,7 @@ def empty_urlconf(request):
533 541
                 {% for var in frame.vars|dictsort:"0" %}
534 542
                   <tr>
535 543
                     <td>{{ var.0|force_escape }}</td>
536  
-                    <td class="code"><div>{{ var.1|pprint|force_escape }}</div></td>
  544
+                    <td class="code"><pre>{{ var.1|pprint|force_escape }}</pre></td>
537 545
                   </tr>
538 546
                 {% endfor %}
539 547
               </tbody>
@@ -545,16 +553,19 @@ def empty_urlconf(request):
545 553
   </div>
546 554
   {% endautoescape %}
547 555
   <form action="http://dpaste.com/" name="pasteform" id="pasteform" method="post">
  556
+{% if not is_email %}
548 557
   <div id="pastebinTraceback" class="pastebin">
549 558
     <input type="hidden" name="language" value="PythonConsole">
550  
-    <input type="hidden" name="title" value="{{ exception_type|escape }} at {{ request.path_info|escape }}">
  559
+    <input type="hidden" name="title" value="{{ exception_type|escape }}{% if request %} at {{ request.path_info|escape }}{% endif %}">
551 560
     <input type="hidden" name="source" value="Django Dpaste Agent">
552 561
     <input type="hidden" name="poster" value="Django">
553 562
     <textarea name="content" id="traceback_area" cols="140" rows="25">
554 563
 Environment:
555 564
 
  565
+{% if request %}
556 566
 Request Method: {{ request.META.REQUEST_METHOD }}
557 567
 Request URL: {{ request.build_absolute_uri|escape }}
  568
+{% endif %}
558 569
 Django Version: {{ django_version_info }}
559 570
 Python Version: {{ sys_version_info }}
560 571
 Installed Applications:
@@ -581,7 +592,7 @@ def empty_urlconf(request):
581 592
 {% for frame in frames %}File "{{ frame.filename|escape }}" in {{ frame.function|escape }}
582 593
 {% if frame.context_line %}  {{ frame.lineno }}. {{ frame.context_line|escape }}{% endif %}
583 594
 {% endfor %}
584  
-Exception Type: {{ exception_type|escape }} at {{ request.path_info|escape }}
  595
+Exception Type: {{ exception_type|escape }}{% if request %} at {{ request.path_info|escape }}{% endif %}
585 596
 Exception Value: {{ exception_value|force_escape }}
586 597
 </textarea>
587 598
   <br><br>
@@ -589,10 +600,12 @@ def empty_urlconf(request):
589 600
   </div>
590 601
 </form>
591 602
 </div>
  603
+{% endif %}
592 604
 
593 605
 <div id="requestinfo">
594 606
   <h2>Request information</h2>
595 607
 
  608
+{% if request %}
596 609
   <h3 id="get-info">GET</h3>
597 610
   {% if request.GET %}
598 611
     <table class="req">
@@ -606,7 +619,7 @@ def empty_urlconf(request):
606 619
         {% for var in request.GET.items %}
607 620
           <tr>
608 621
             <td>{{ var.0 }}</td>
609  
-            <td class="code"><div>{{ var.1|pprint }}</div></td>
  622
+            <td class="code"><pre>{{ var.1|pprint }}</pre></td>
610 623
           </tr>
611 624
         {% endfor %}
612 625
       </tbody>
@@ -628,7 +641,7 @@ def empty_urlconf(request):
628 641
         {% for var in request.POST.items %}
629 642
           <tr>
630 643
             <td>{{ var.0 }}</td>
631  
-            <td class="code"><div>{{ var.1|pprint }}</div></td>
  644
+            <td class="code"><pre>{{ var.1|pprint }}</pre></td>
632 645
           </tr>
633 646
         {% endfor %}
634 647
       </tbody>
@@ -649,7 +662,7 @@ def empty_urlconf(request):
649 662
             {% for var in request.FILES.items %}
650 663
                 <tr>
651 664
                     <td>{{ var.0 }}</td>
652  
-                    <td class="code"><div>{{ var.1|pprint }}</div></td>
  665
+                    <td class="code"><pre>{{ var.1|pprint }}</pre></td>
653 666
                 </tr>
654 667
             {% endfor %}
655 668
         </tbody>
@@ -672,7 +685,7 @@ def empty_urlconf(request):
672 685
         {% for var in request.COOKIES.items %}
673 686
           <tr>
674 687
             <td>{{ var.0 }}</td>
675  
-            <td class="code"><div>{{ var.1|pprint }}</div></td>
  688
+            <td class="code"><pre>{{ var.1|pprint }}</pre></td>
676 689
           </tr>
677 690
         {% endfor %}
678 691
       </tbody>
@@ -693,11 +706,12 @@ def empty_urlconf(request):
693 706
       {% for var in request.META.items|dictsort:"0" %}
694 707
         <tr>
695 708
           <td>{{ var.0 }}</td>
696  
-          <td class="code"><div>{{ var.1|pprint }}</div></td>
  709
+          <td class="code"><pre>{{ var.1|pprint }}</pre></td>
697 710
         </tr>
698 711
       {% endfor %}
699 712
     </tbody>
700 713
   </table>
  714
+{% endif %}
701 715
 
702 716
   <h3 id="settings-info">Settings</h3>
703 717
   <h4>Using settings module <code>{{ settings.SETTINGS_MODULE }}</code></h4>
@@ -712,7 +726,7 @@ def empty_urlconf(request):
712 726
       {% for var in settings.items|dictsort:"0" %}
713 727
         <tr>
714 728
           <td>{{ var.0 }}</td>
715  
-          <td class="code"><div>{{ var.1|pprint }}</div></td>
  729
+          <td class="code"><pre>{{ var.1|pprint }}</pre></td>
716 730
         </tr>
717 731
       {% endfor %}
718 732
     </tbody>
6  docs/releases/1.3.txt
@@ -163,6 +163,12 @@ requests. These include:
163 163
 
164 164
     * Support for _HTTPOnly cookies.
165 165
 
  166
+    * mail_admins() and mail_managers() now support easily attaching
  167
+      HTML content to messages.
  168
+
  169
+    * Error emails now include more of the detail and formatting of
  170
+      the debug server error page.
  171
+
166 172
 .. _HTTPOnly: http://www.owasp.org/index.php/HTTPOnly
167 173
 
168 174
 .. _backwards-incompatible-changes-1.3:
10  docs/topics/email.txt
@@ -109,7 +109,7 @@ a single connection for all of its messages. This makes
109 109
 mail_admins()
110 110
 =============
111 111
 
112  
-.. function:: mail_admins(subject, message, fail_silently=False, connection=None)
  112
+.. function:: mail_admins(subject, message, fail_silently=False, connection=None, html_message=None)
113 113
 
114 114
 ``django.core.mail.mail_admins()`` is a shortcut for sending an e-mail to the
115 115
 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
122 122
 
123 123
 This method exists for convenience and readability.
124 124
 
  125
+.. versionchanged:: 1.3
  126
+
  127
+If ``html_message`` is provided, the resulting e-mail will be a
  128
+multipart/alternative e-mail with ``message`` as the "text/plain"
  129
+content type and ``html_message`` as the "text/html" content type.
  130
+
125 131
 mail_managers()
126 132
 ===============
127 133
 
128  
-.. function:: mail_managers(subject, message, fail_silently=False, connection=None)
  134
+.. function:: mail_managers(subject, message, fail_silently=False, connection=None, html_message=None)
129 135
 
130 136
 ``django.core.mail.mail_managers()`` is just like ``mail_admins()``, except it
131 137
 sends an e-mail to the site managers, as defined in the :setting:`MANAGERS`
32  tests/regressiontests/mail/tests.py
@@ -232,7 +232,7 @@ def test_locmem(self):
232 232
         self.assertEqual(len(mail.outbox), 2)
233 233
         self.assertEqual(mail.outbox[0].subject, 'Subject')
234 234
         self.assertEqual(mail.outbox[1].subject, 'Subject 2')
235  
-        
  235
+
236 236
         # Make sure that multiple locmem connections share mail.outbox
237 237
         mail.outbox = []
238 238
         connection2 = locmem.EmailBackend()
@@ -364,6 +364,36 @@ def test_mail_prefix(self):
364 364
         settings.ADMINS = old_admins
365 365
         settings.MANAGERS = old_managers
366 366
 
  367
+    def test_html_mail_admins(self):
  368
+        """Test html_message argument to mail_admins and mail_managers"""
  369
+        old_admins = settings.ADMINS
  370
+        settings.ADMINS = [('nobody','nobody@example.com')]
  371
+
  372
+        mail.outbox = []
  373
+        mail_admins('Subject', 'Content', html_message='HTML Content')
  374
+        self.assertEqual(len(mail.outbox), 1)
  375
+        message = mail.outbox[0]
  376
+        self.assertEqual(message.subject, '[Django] Subject')
  377
+        self.assertEqual(message.body, 'Content')
  378
+        self.assertEqual(message.alternatives, [('HTML Content', 'text/html')])
  379
+
  380
+        settings.ADMINS = old_admins
  381
+
  382
+    def test_html_mail_managers(self):
  383
+        """Test html_message argument to mail_admins and mail_managers"""
  384
+        old_managers = settings.MANAGERS
  385
+        settings.MANAGERS = [('nobody','nobody@example.com')]
  386
+
  387
+        mail.outbox = []
  388
+        mail_managers('Subject', 'Content', html_message='HTML Content')
  389
+        self.assertEqual(len(mail.outbox), 1)
  390
+        message = mail.outbox[0]
  391
+        self.assertEqual(message.subject, '[Django] Subject')
  392
+        self.assertEqual(message.body, 'Content')
  393
+        self.assertEqual(message.alternatives, [('HTML Content', 'text/html')])
  394
+
  395
+        settings.MANAGERS = old_managers
  396
+
367 397
     def test_idn_validation(self):
368 398
         """Test internationalized email adresses"""
369 399
         # Regression for #14301.

0 notes on commit 29c4a57

Please sign in to comment.
Something went wrong with that request. Please try again.