From c0de72edf537a4a1fd1481de4efaa94755802f48 Mon Sep 17 00:00:00 2001 From: Stephan Deibel Date: Wed, 8 Apr 2026 22:21:56 -0400 Subject: [PATCH] Add optional email send logging for monitoring and auditing Adds a lightweight _log_email_sent() helper that appends timestamped entries (recipient, subject) to a file when EMAIL_LOG_FILE is set in Django settings. Called after each successful send_mail(). Errors in logging are silently caught to never break email delivery. Opt-in: does nothing unless EMAIL_LOG_FILE is configured. --- askbot/mail/__init__.py | 16 +++++++++ askbot/tests/test_email_logging.py | 53 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 askbot/tests/test_email_logging.py diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py index cc7674fae3..1778011e4c 100644 --- a/askbot/mail/__init__.py +++ b/askbot/mail/__init__.py @@ -23,6 +23,20 @@ DEBUG_EMAIL = django_settings.ASKBOT_DEBUG_INCOMING_EMAIL +def _log_email_sent(subject, recipient_list): + """Append a line to the email log for monitoring/daily summaries.""" + log_path = getattr(django_settings, 'EMAIL_LOG_FILE', None) + if not log_path: + return + try: + import datetime + ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + recipients = ','.join(str(r) for r in recipient_list) + with open(log_path, 'a') as f: + f.write(f'[{ts}] to={recipients} subject={subject}\n') + except Exception: + pass # never break email sending over logging + #or the future API def prefix_the_subject_line(subject): """prefixes the subject line with the @@ -110,6 +124,8 @@ def send_mail( # pylint: disable=too-many-arguments attachments=attachments ) logging.debug('sent update to %s' % ','.join(map(str, recipient_list))) + # Log email activity for monitoring + _log_email_sent(subject_line, recipient_list) except Exception as error: # pylint: disable=broad-except sys.stderr.write('\n' + str(error) + '\n') if raise_on_failure: diff --git a/askbot/tests/test_email_logging.py b/askbot/tests/test_email_logging.py new file mode 100644 index 0000000000..be349a5b58 --- /dev/null +++ b/askbot/tests/test_email_logging.py @@ -0,0 +1,53 @@ +"""Tests for email send logging.""" +import os +import tempfile +from unittest.mock import patch + +from django.test import TestCase, override_settings + +from askbot.mail import _log_email_sent + + +class EmailLoggingTests(TestCase): + """Tests for _log_email_sent().""" + + def test_no_setting_noop(self): + """No EMAIL_LOG_FILE setting means no file I/O.""" + with override_settings(**{'EMAIL_LOG_FILE': None}): + # Should not raise and not create any files + _log_email_sent('Test subject', ['user@example.com']) + + def test_writes_log_entry(self): + """Log file should contain subject and recipient.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.log', + delete=False) as f: + log_path = f.name + try: + with self.settings(EMAIL_LOG_FILE=log_path): + _log_email_sent('Hello world', ['alice@example.com']) + with open(log_path) as f: + contents = f.read() + self.assertIn('subject=Hello world', contents) + self.assertIn('to=alice@example.com', contents) + finally: + os.unlink(log_path) + + def test_multiple_recipients(self): + """Multiple recipients should be comma-separated.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.log', + delete=False) as f: + log_path = f.name + try: + with self.settings(EMAIL_LOG_FILE=log_path): + _log_email_sent('Multi', ['a@example.com', 'b@example.com']) + with open(log_path) as f: + contents = f.read() + self.assertIn('to=a@example.com,b@example.com', contents) + finally: + os.unlink(log_path) + + def test_file_error_silenced(self): + """IOError when writing log should not propagate.""" + with self.settings(EMAIL_LOG_FILE='/nonexistent/dir/email.log'): + # Should not raise + _log_email_sent('Test', ['user@example.com'])