Merge pull request #1128 from django-helpdesk/catch_exceptions_on_precess_email_loop

Catch exceptions on precess email loop
This commit is contained in:
Christopher Broderick 2023-10-26 22:11:30 +01:00 committed by GitHub
commit 2b6ad7a2cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 111 additions and 9 deletions

View File

@ -26,7 +26,7 @@ jobs:
python -m build python -m build
twine check --strict dist/* twine check --strict dist/*
- name: Publish distribution to PyPI - name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@master uses: pypa/gh-action-pypi-publish@release/v1
with: with:
user: __token__ user: __token__
password: ${{ secrets.PYPI_API_TOKEN }} password: ${{ secrets.PYPI_API_TOKEN }}

View File

@ -22,6 +22,13 @@ Before django-helpdesk will be much use, you need to do some basic configuration
If you wish to use `celery` instead of cron, you must add 'django_celery_beat' to `INSTALLED_APPS` and add a periodic celery task through the Django admin. If you wish to use `celery` instead of cron, you must add 'django_celery_beat' to `INSTALLED_APPS` and add a periodic celery task through the Django admin.
You will need to create a support queue, and associated login/host values, in the Django admin interface, in order for mail to be picked-up from the mail server and placed in the tickets table of your database. The values in the settings file alone, will not create the necessary values to trigger the get_email function. You will need to create a support queue, and associated login/host values, in the Django admin interface, in order for mail to be picked-up from the mail server and placed in the tickets table of your database. The values in the settings file alone, will not create the necessary values to trigger the get_email function.
DEBUGGING EMAIL EXTRACTION
==========================
You can run the management command manually from the command line with additional commands options:
**debug_to_stdout** - set this when manually running the command from a terminal so that additional debugging about which queues are being processed is written to stdout (console by default)
For example:
**/path/to/helpdesksite/manage.py get_email --debug_to_stdout**
4. If you wish to automatically escalate tickets based on their age, set up a cronjob to run the escalation command on a regular basis:: 4. If you wish to automatically escalate tickets based on their age, set up a cronjob to run the escalation command on a regular basis::

View File

@ -37,6 +37,7 @@ import socket
import ssl import ssl
import sys import sys
from time import ctime from time import ctime
import traceback
import typing import typing
from typing import List from typing import List
@ -56,11 +57,16 @@ STRIPPED_SUBJECT_STRINGS = [
HTML_EMAIL_ATTACHMENT_FILENAME = _("email_html_body.html") HTML_EMAIL_ATTACHMENT_FILENAME = _("email_html_body.html")
def process_email(quiet=False): def process_email(quiet: bool = False, debug_to_stdout: bool = False):
if debug_to_stdout:
print("Extracting email into queues...")
q: Queue() # Typing ahead of time for loop to make it more useful in an IDE
for q in Queue.objects.filter( for q in Queue.objects.filter(
email_box_type__isnull=False, email_box_type__isnull=False,
allow_email_submission=True): allow_email_submission=True):
log_msg = f"Processing queue: {q.slug} Email address: {q.email_address}..."
if debug_to_stdout:
print(log_msg)
logger = logging.getLogger('django.helpdesk.queue.' + q.slug) logger = logging.getLogger('django.helpdesk.queue.' + q.slug)
logging_types = { logging_types = {
'info': logging.INFO, 'info': logging.INFO,
@ -84,16 +90,24 @@ def process_email(quiet=False):
logger.addHandler(log_file_handler) logger.addHandler(log_file_handler)
else: else:
log_file_handler = None log_file_handler = None
try:
if not q.email_box_last_check: if not q.email_box_last_check:
q.email_box_last_check = timezone.now() - timedelta(minutes=30) q.email_box_last_check = timezone.now() - timedelta(minutes=30)
try:
queue_time_delta = timedelta(minutes=q.email_box_interval or 0) queue_time_delta = timedelta(minutes=q.email_box_interval or 0)
if (q.email_box_last_check + queue_time_delta) < timezone.now(): if (q.email_box_last_check + queue_time_delta) < timezone.now():
process_queue(q, logger=logger) process_queue(q, logger=logger)
q.email_box_last_check = timezone.now() q.email_box_last_check = timezone.now()
q.save() q.save()
log_msg: str = f"Queue successfully processed: {q.slug}"
logger.info(log_msg)
if debug_to_stdout:
print(log_msg)
except Exception as e:
logger.error(f"Queue processing failed: {q.slug} -- {e}", exc_info=True)
if debug_to_stdout:
print(f"Queue processing failed: {q.slug}")
print("-"*60)
traceback.print_exc(file=sys.stdout)
finally: finally:
# we must close the file handler correctly if it's created # we must close the file handler correctly if it's created
try: try:
@ -106,6 +120,8 @@ def process_email(quiet=False):
logger.removeHandler(log_file_handler) logger.removeHandler(log_file_handler)
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
if debug_to_stdout:
print("Email extraction into queues completed.")
def pop3_sync(q, logger, server): def pop3_sync(q, logger, server):
@ -320,7 +336,7 @@ def imap_oauth_sync(q, logger, server):
def process_queue(q, logger): def process_queue(q, logger):
logger.info("***** %s: Begin processing mail for django-helpdesk" % ctime()) logger.info(f"***** {ctime()}: Begin processing mail for django-helpdesk queue: {q.title}")
if q.socks_proxy_type and q.socks_proxy_host and q.socks_proxy_port: if q.socks_proxy_type and q.socks_proxy_host and q.socks_proxy_port:
try: try:

View File

@ -30,10 +30,18 @@ class Command(BaseCommand):
default=False, default=False,
help='Hide details about each queue/message as they are processed', help='Hide details about each queue/message as they are processed',
) )
parser.add_argument(
'--debug_to_stdout',
action='store_true',
dest='debug_to_stdout',
default=False,
help='Log additional messaging to stdout.',
)
def handle(self, *args, **options): def handle(self, *args, **options):
quiet = options.get('quiet', False) quiet = options.get('quiet', False)
process_email(quiet=quiet) debug_to_stdout = options.get('debug_to_stdout', False)
process_email(quiet=quiet, debug_to_stdout=debug_to_stdout)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -18,6 +18,7 @@ from helpdesk.models import FollowUp, FollowUpAttachment, IgnoreEmail, Queue, Ti
from helpdesk.tests import utils from helpdesk.tests import utils
import itertools import itertools
import logging import logging
from mock.mock import patch
from oauthlib.oauth2 import BackendApplicationClient from oauthlib.oauth2 import BackendApplicationClient
import os import os
from shutil import rmtree from shutil import rmtree
@ -335,12 +336,82 @@ class GetEmailCommonTests(TestCase):
elif att_retrieved.filename.endswith(email_att_filename): elif att_retrieved.filename.endswith(email_att_filename):
email_attachment_found = True email_attachment_found = True
else: else:
print(f"\n\n%%%%%% {att_retrieved}")
self.assertTrue(False, "Unexpected file in ticket attachments: %s" % att_retrieved.filename) self.assertTrue(False, "Unexpected file in ticket attachments: %s" % att_retrieved.filename)
self.assertTrue(email_attachment_found, "Email attachment file not found ticket attachments: %s" % (email_att_filename)) self.assertTrue(email_attachment_found, "Email attachment file not found ticket attachments: %s" % (email_att_filename))
self.assertTrue(inline_found, "Inline file not found in email: %s" % (inline_att_filename)) self.assertTrue(inline_found, "Inline file not found in email: %s" % (inline_att_filename))
def test_email_with_txt_as_attachment(self):
"""
Test an email with an txt extension email attachment to the email
"""
email_message, _, _ = utils.generate_multipart_email(type_list=['plain'])
email_att_filename = 'test.txt'
file_part = utils.generate_file_mime_part("en_US", email_att_filename, "Testing a simple txt attachment.")
email_message.attach(file_part)
# Now send the part to the email workflow
extract_email_metadata(email_message.as_string(), self.queue_public, self.logger)
self.assertEqual(len(mail.outbox), 1) # @UndefinedVariable
ticket = Ticket.objects.get()
followup = ticket.followup_set.get()
attachments = FollowUpAttachment.objects.filter(followup=followup)
self.assertEqual(len(attachments), 1)
attachment = attachments[0]
self.assertTrue(attachment.filename.endswith(email_att_filename), "The txt file not found in email: %s" % (email_att_filename))
class EmailTaskTests(TestCase):
def setUp(self):
self.num_queues = 5
self.exception_queues = [2, 4]
self.q_ids = []
for i in range(self.num_queues):
q = Queue.objects.create(
title=f'Test{i+1}',
slug=f'test{i+1}',
email_box_type='local',
allow_email_submission=True
)
self.q_ids.append(q.id)
self.logger = logging.getLogger('helpdesk')
def test_get_email_with_debug_to_stdout_option(self):
"""Test debug_to_stdout option """
# Test get_email with debug_to_stdout set to True and also False, and verify
# handle receives debug_to_stdout option set properly
for debug_to_stdout in [True, False]:
with mock.patch.object(Command, 'handle', return_value=None) as mocked_handle:
call_command('get_email', "--debug_to_stdout") if debug_to_stdout else call_command('get_email')
mocked_handle.assert_called_once()
for _, kwargs in mocked_handle.call_args_list:
self.assertEqual(debug_to_stdout, (kwargs['debug_to_stdout']))
@patch('helpdesk.email.process_queue')
def test_get_email_with_queue_failure(self, mocked_process_queue):
"""Test all queues are processed if specified queues have exceptions"""
ret_values = [
Exception(f"Error Q{i};") if (i in self.exception_queues) else None for i in range(1, self.num_queues+1)
]
mocked_process_queue.side_effect = ret_values
call_command(
'get_email',
'--debug_to_stdout',
)
self.assertEqual(mocked_process_queue.call_count, self.num_queues)
not_processed_count = Queue.objects.filter(
email_box_type__isnull=False,
allow_email_submission=True,
email_box_last_check__isnull=True).count()
self.assertEqual(
not_processed_count,
len(self.exception_queues),
"Incorrect number of queues that did not get processed due to a forced exception."
)
class GetEmailParametricTemplate(object): class GetEmailParametricTemplate(object):
"""TestCase that checks basic email functionality across methods and socks configs.""" """TestCase that checks basic email functionality across methods and socks configs."""