From d57f11f40aa13241053e8960c1641d0c7b4248ef Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Sat, 15 Apr 2023 21:52:44 +1000 Subject: [PATCH 01/11] Add IMAP OAUTH Mail Box Type --- helpdesk/email.py | 113 +++++++++++++++++- .../0037_alter_queue_email_box_type.py | 18 +++ helpdesk/models.py | 4 +- helpdesk/settings.py | 11 ++ requirements.txt | 2 + 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 helpdesk/migrations/0037_alter_queue_email_box_type.py diff --git a/helpdesk/email.py b/helpdesk/email.py index d5f40b97..9f16b6d5 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -21,6 +21,11 @@ import email from email.message import Message from email.utils import getaddresses from email_reply_parser import EmailReplyParser + +# Add OAUTH Libraries +from oauthlib.oauth2 import BackendApplicationClient +from requests_oauthlib import OAuth2Session + from helpdesk import settings from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException from helpdesk.lib import process_attachments, safe_template_context @@ -225,6 +230,101 @@ def imap_sync(q, logger, server): server.logout() +def imap_oauth_sync(q, logger, server): + """ + IMAP eMail server with OAUTH authentication. + Only tested against O365 implementation + + Uses OAUTH Dict in Settings. + + """ + + try: + logger.debug("Start Mailbox polling via IMAP OAUTH") + + client = BackendApplicationClient( + client_id=django_settings.HELPDESK_OAUTH["client_id"], + scope=django_settings.HELPDESK_OAUTH["scope"], + ) + + oauth = OAuth2Session(client=client) + token = oauth.fetch_token( + token_url=django_settings.HELPDESK_OAUTH["token_url"], + client_id=django_settings.HELPDESK_OAUTH["client_id"], + client_secret=django_settings.HELPDESK_OAUTH["secret"], + include_client_id=True, + ) + + # TODO: Somehow link this to the debug level set within Django settings logging + server.debug = 4 + + # TODO: Perhaps store the authentication string template externally? Settings? Queue Table? + server.authenticate( + "XOAUTH2", + lambda x: f"user={q.email_box_user}\x01auth=Bearer {token['access_token']}\x01\x01".encode(), + ) + + # Select the Inbound Mailbox folder + server.select(q.email_box_imap_folder) + + except imaplib.IMAP4.abort: + logger.error("IMAP authentication failed.") + server.logout() + sys.exit() + + except ssl.SSLError: + logger.error( + "IMAP login failed due to SSL error. This is often due to a timeout. " + "Please check your connection and try again." + ) + server.logout() + sys.exit() + + try: + data = server.search(None, 'NOT', 'DELETED')[1] + if data: + msgnums = data[0].split() + logger.info(f"Found {len(msgnums)} message(s) on IMAP server" ) + for num in msgnums: + logger.info(f"Processing message {num}") + data = server.fetch(num, '(RFC822)')[1] + full_message = encoding.force_str(data[0][1], errors='replace') + + try: + ticket = object_from_message(message=full_message, queue=q, logger=logger) + + except IgnoreTicketException as itex: + logger.warn(f"Message {num} was ignored. {itex}") + + except DeleteIgnoredTicketException: + server.store(num, '+FLAGS', '\\Deleted') + logger.warn("Message %s was ignored and deleted from IMAP server" % num) + + except TypeError as te: + # Log the error with stacktrace to help identify what went wrong + logger.error(f"Unexpected error processing message: {te}", exc_info=True) + + else: + if ticket: + server.store(num, '+FLAGS', '\\Deleted') + logger.info( + "Successfully processed message %s, deleted from IMAP server" % num) + else: + logger.warn( + "Message %s was not successfully processed, and will be left on IMAP server" % num) + + except imaplib.IMAP4.error: + logger.error( + "IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?", + q.email_box_imap_folder + ) + + # Purged Flagged Messages & Logout + server.expunge() + server.close() + server.logout() + + def process_queue(q, logger): logger.info("***** %s: Begin processing mail for django-helpdesk" % ctime()) @@ -272,7 +372,18 @@ def process_queue(q, logger): 'init': imaplib.IMAP4, }, 'sync': imap_sync - } + }, + 'oauth': { + 'ssl': { + 'port': 993, + 'init': imaplib.IMAP4_SSL, + }, + 'insecure': { + 'port': 143, + 'init': imaplib.IMAP4, + }, + 'sync': imap_oauth_sync + }, } if email_box_type in mail_defaults: encryption = 'insecure' diff --git a/helpdesk/migrations/0037_alter_queue_email_box_type.py b/helpdesk/migrations/0037_alter_queue_email_box_type.py new file mode 100644 index 00000000..cb46d053 --- /dev/null +++ b/helpdesk/migrations/0037_alter_queue_email_box_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-03-25 15:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('helpdesk', '0036_add_attachment_validator'), + ] + + operations = [ + migrations.AlterField( + model_name='queue', + name='email_box_type', + field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('oauth', 'IMAP OAUTH'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'), + ), + ] diff --git a/helpdesk/models.py b/helpdesk/models.py index ab19c9bd..27151171 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -175,7 +175,9 @@ class Queue(models.Model): email_box_type = models.CharField( _('E-Mail Box Type'), max_length=5, - choices=(('pop3', _('POP 3')), ('imap', _('IMAP')), + choices=(('pop3', _('POP 3')), + ('imap', _('IMAP')), + ('oauth', _('IMAP OAUTH')), ('local', _('Local Directory'))), blank=True, null=True, diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 4594a510..17240a29 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -249,3 +249,14 @@ HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr( # (which gets stripped/corrupted otherwise) HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr( settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False) + +####################### +# email OAUTH # +####################### + +HELPDESK_OAUTH = { + "token_url": "", + "client_id": "", + "secret": "", + "scope": [""] +} diff --git a/requirements.txt b/requirements.txt index faefd66e..e6968e7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ pinax_teams djangorestframework django-model-utils django-cleanup +requests +requests_oauthlib From d7ebc8e1c2fbafdb4ae1579ddd46a33a5471b2bd Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Sat, 15 Apr 2023 21:56:05 +1000 Subject: [PATCH 02/11] Revert "Add IMAP OAUTH Mail Box Type" This reverts commit d57f11f40aa13241053e8960c1641d0c7b4248ef. --- helpdesk/email.py | 113 +----------------- .../0037_alter_queue_email_box_type.py | 18 --- helpdesk/models.py | 4 +- helpdesk/settings.py | 11 -- requirements.txt | 2 - 5 files changed, 2 insertions(+), 146 deletions(-) delete mode 100644 helpdesk/migrations/0037_alter_queue_email_box_type.py diff --git a/helpdesk/email.py b/helpdesk/email.py index 9f16b6d5..d5f40b97 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -21,11 +21,6 @@ import email from email.message import Message from email.utils import getaddresses from email_reply_parser import EmailReplyParser - -# Add OAUTH Libraries -from oauthlib.oauth2 import BackendApplicationClient -from requests_oauthlib import OAuth2Session - from helpdesk import settings from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException from helpdesk.lib import process_attachments, safe_template_context @@ -230,101 +225,6 @@ def imap_sync(q, logger, server): server.logout() -def imap_oauth_sync(q, logger, server): - """ - IMAP eMail server with OAUTH authentication. - Only tested against O365 implementation - - Uses OAUTH Dict in Settings. - - """ - - try: - logger.debug("Start Mailbox polling via IMAP OAUTH") - - client = BackendApplicationClient( - client_id=django_settings.HELPDESK_OAUTH["client_id"], - scope=django_settings.HELPDESK_OAUTH["scope"], - ) - - oauth = OAuth2Session(client=client) - token = oauth.fetch_token( - token_url=django_settings.HELPDESK_OAUTH["token_url"], - client_id=django_settings.HELPDESK_OAUTH["client_id"], - client_secret=django_settings.HELPDESK_OAUTH["secret"], - include_client_id=True, - ) - - # TODO: Somehow link this to the debug level set within Django settings logging - server.debug = 4 - - # TODO: Perhaps store the authentication string template externally? Settings? Queue Table? - server.authenticate( - "XOAUTH2", - lambda x: f"user={q.email_box_user}\x01auth=Bearer {token['access_token']}\x01\x01".encode(), - ) - - # Select the Inbound Mailbox folder - server.select(q.email_box_imap_folder) - - except imaplib.IMAP4.abort: - logger.error("IMAP authentication failed.") - server.logout() - sys.exit() - - except ssl.SSLError: - logger.error( - "IMAP login failed due to SSL error. This is often due to a timeout. " - "Please check your connection and try again." - ) - server.logout() - sys.exit() - - try: - data = server.search(None, 'NOT', 'DELETED')[1] - if data: - msgnums = data[0].split() - logger.info(f"Found {len(msgnums)} message(s) on IMAP server" ) - for num in msgnums: - logger.info(f"Processing message {num}") - data = server.fetch(num, '(RFC822)')[1] - full_message = encoding.force_str(data[0][1], errors='replace') - - try: - ticket = object_from_message(message=full_message, queue=q, logger=logger) - - except IgnoreTicketException as itex: - logger.warn(f"Message {num} was ignored. {itex}") - - except DeleteIgnoredTicketException: - server.store(num, '+FLAGS', '\\Deleted') - logger.warn("Message %s was ignored and deleted from IMAP server" % num) - - except TypeError as te: - # Log the error with stacktrace to help identify what went wrong - logger.error(f"Unexpected error processing message: {te}", exc_info=True) - - else: - if ticket: - server.store(num, '+FLAGS', '\\Deleted') - logger.info( - "Successfully processed message %s, deleted from IMAP server" % num) - else: - logger.warn( - "Message %s was not successfully processed, and will be left on IMAP server" % num) - - except imaplib.IMAP4.error: - logger.error( - "IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?", - q.email_box_imap_folder - ) - - # Purged Flagged Messages & Logout - server.expunge() - server.close() - server.logout() - - def process_queue(q, logger): logger.info("***** %s: Begin processing mail for django-helpdesk" % ctime()) @@ -372,18 +272,7 @@ def process_queue(q, logger): 'init': imaplib.IMAP4, }, 'sync': imap_sync - }, - 'oauth': { - 'ssl': { - 'port': 993, - 'init': imaplib.IMAP4_SSL, - }, - 'insecure': { - 'port': 143, - 'init': imaplib.IMAP4, - }, - 'sync': imap_oauth_sync - }, + } } if email_box_type in mail_defaults: encryption = 'insecure' diff --git a/helpdesk/migrations/0037_alter_queue_email_box_type.py b/helpdesk/migrations/0037_alter_queue_email_box_type.py deleted file mode 100644 index cb46d053..00000000 --- a/helpdesk/migrations/0037_alter_queue_email_box_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.16 on 2023-03-25 15:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('helpdesk', '0036_add_attachment_validator'), - ] - - operations = [ - migrations.AlterField( - model_name='queue', - name='email_box_type', - field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('oauth', 'IMAP OAUTH'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'), - ), - ] diff --git a/helpdesk/models.py b/helpdesk/models.py index 27151171..ab19c9bd 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -175,9 +175,7 @@ class Queue(models.Model): email_box_type = models.CharField( _('E-Mail Box Type'), max_length=5, - choices=(('pop3', _('POP 3')), - ('imap', _('IMAP')), - ('oauth', _('IMAP OAUTH')), + choices=(('pop3', _('POP 3')), ('imap', _('IMAP')), ('local', _('Local Directory'))), blank=True, null=True, diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 17240a29..4594a510 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -249,14 +249,3 @@ HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr( # (which gets stripped/corrupted otherwise) HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr( settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False) - -####################### -# email OAUTH # -####################### - -HELPDESK_OAUTH = { - "token_url": "", - "client_id": "", - "secret": "", - "scope": [""] -} diff --git a/requirements.txt b/requirements.txt index e6968e7c..faefd66e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,3 @@ pinax_teams djangorestframework django-model-utils django-cleanup -requests -requests_oauthlib From 919277988919597b3a352467da67cf7ea05c3644 Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Sat, 15 Apr 2023 22:11:41 +1000 Subject: [PATCH 03/11] Add IMAP OAUTH Mail Box Type --- helpdesk/email.py | 113 +++++++++++++++++- .../0037_alter_queue_email_box_type.py | 18 +++ helpdesk/models.py | 4 +- helpdesk/settings.py | 11 ++ requirements.txt | 2 + 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 helpdesk/migrations/0037_alter_queue_email_box_type.py diff --git a/helpdesk/email.py b/helpdesk/email.py index d5f40b97..9f16b6d5 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -21,6 +21,11 @@ import email from email.message import Message from email.utils import getaddresses from email_reply_parser import EmailReplyParser + +# Add OAUTH Libraries +from oauthlib.oauth2 import BackendApplicationClient +from requests_oauthlib import OAuth2Session + from helpdesk import settings from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException from helpdesk.lib import process_attachments, safe_template_context @@ -225,6 +230,101 @@ def imap_sync(q, logger, server): server.logout() +def imap_oauth_sync(q, logger, server): + """ + IMAP eMail server with OAUTH authentication. + Only tested against O365 implementation + + Uses OAUTH Dict in Settings. + + """ + + try: + logger.debug("Start Mailbox polling via IMAP OAUTH") + + client = BackendApplicationClient( + client_id=django_settings.HELPDESK_OAUTH["client_id"], + scope=django_settings.HELPDESK_OAUTH["scope"], + ) + + oauth = OAuth2Session(client=client) + token = oauth.fetch_token( + token_url=django_settings.HELPDESK_OAUTH["token_url"], + client_id=django_settings.HELPDESK_OAUTH["client_id"], + client_secret=django_settings.HELPDESK_OAUTH["secret"], + include_client_id=True, + ) + + # TODO: Somehow link this to the debug level set within Django settings logging + server.debug = 4 + + # TODO: Perhaps store the authentication string template externally? Settings? Queue Table? + server.authenticate( + "XOAUTH2", + lambda x: f"user={q.email_box_user}\x01auth=Bearer {token['access_token']}\x01\x01".encode(), + ) + + # Select the Inbound Mailbox folder + server.select(q.email_box_imap_folder) + + except imaplib.IMAP4.abort: + logger.error("IMAP authentication failed.") + server.logout() + sys.exit() + + except ssl.SSLError: + logger.error( + "IMAP login failed due to SSL error. This is often due to a timeout. " + "Please check your connection and try again." + ) + server.logout() + sys.exit() + + try: + data = server.search(None, 'NOT', 'DELETED')[1] + if data: + msgnums = data[0].split() + logger.info(f"Found {len(msgnums)} message(s) on IMAP server" ) + for num in msgnums: + logger.info(f"Processing message {num}") + data = server.fetch(num, '(RFC822)')[1] + full_message = encoding.force_str(data[0][1], errors='replace') + + try: + ticket = object_from_message(message=full_message, queue=q, logger=logger) + + except IgnoreTicketException as itex: + logger.warn(f"Message {num} was ignored. {itex}") + + except DeleteIgnoredTicketException: + server.store(num, '+FLAGS', '\\Deleted') + logger.warn("Message %s was ignored and deleted from IMAP server" % num) + + except TypeError as te: + # Log the error with stacktrace to help identify what went wrong + logger.error(f"Unexpected error processing message: {te}", exc_info=True) + + else: + if ticket: + server.store(num, '+FLAGS', '\\Deleted') + logger.info( + "Successfully processed message %s, deleted from IMAP server" % num) + else: + logger.warn( + "Message %s was not successfully processed, and will be left on IMAP server" % num) + + except imaplib.IMAP4.error: + logger.error( + "IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?", + q.email_box_imap_folder + ) + + # Purged Flagged Messages & Logout + server.expunge() + server.close() + server.logout() + + def process_queue(q, logger): logger.info("***** %s: Begin processing mail for django-helpdesk" % ctime()) @@ -272,7 +372,18 @@ def process_queue(q, logger): 'init': imaplib.IMAP4, }, 'sync': imap_sync - } + }, + 'oauth': { + 'ssl': { + 'port': 993, + 'init': imaplib.IMAP4_SSL, + }, + 'insecure': { + 'port': 143, + 'init': imaplib.IMAP4, + }, + 'sync': imap_oauth_sync + }, } if email_box_type in mail_defaults: encryption = 'insecure' diff --git a/helpdesk/migrations/0037_alter_queue_email_box_type.py b/helpdesk/migrations/0037_alter_queue_email_box_type.py new file mode 100644 index 00000000..cb46d053 --- /dev/null +++ b/helpdesk/migrations/0037_alter_queue_email_box_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-03-25 15:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('helpdesk', '0036_add_attachment_validator'), + ] + + operations = [ + migrations.AlterField( + model_name='queue', + name='email_box_type', + field=models.CharField(blank=True, choices=[('pop3', 'POP 3'), ('imap', 'IMAP'), ('oauth', 'IMAP OAUTH'), ('local', 'Local Directory')], help_text='E-Mail server type for creating tickets automatically from a mailbox - both POP3 and IMAP are supported, as well as reading from a local directory.', max_length=5, null=True, verbose_name='E-Mail Box Type'), + ), + ] diff --git a/helpdesk/models.py b/helpdesk/models.py index ab19c9bd..27151171 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -175,7 +175,9 @@ class Queue(models.Model): email_box_type = models.CharField( _('E-Mail Box Type'), max_length=5, - choices=(('pop3', _('POP 3')), ('imap', _('IMAP')), + choices=(('pop3', _('POP 3')), + ('imap', _('IMAP')), + ('oauth', _('IMAP OAUTH')), ('local', _('Local Directory'))), blank=True, null=True, diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 4594a510..17240a29 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -249,3 +249,14 @@ HELPDESK_FULL_FIRST_MESSAGE_FROM_EMAIL = getattr( # (which gets stripped/corrupted otherwise) HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr( settings, "HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE", False) + +####################### +# email OAUTH # +####################### + +HELPDESK_OAUTH = { + "token_url": "", + "client_id": "", + "secret": "", + "scope": [""] +} diff --git a/requirements.txt b/requirements.txt index faefd66e..e6968e7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ pinax_teams djangorestframework django-model-utils django-cleanup +requests +requests_oauthlib From 2f0f674fdeb3df295c3d7b0a9f363f2c0b1aa4e1 Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Sat, 15 Apr 2023 22:13:57 +1000 Subject: [PATCH 04/11] Add IMAP OAUTH Mail Box Type --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e6968e7c..ecf2f1ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,5 @@ pinax_teams djangorestframework django-model-utils django-cleanup -requests +oauthlib requests_oauthlib From acb2e626a3e75197212151153cce58a9b474c55a Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Sun, 16 Apr 2023 20:48:24 +1000 Subject: [PATCH 05/11] Removed codecov --- requirements-testing.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 80872424..a46716c1 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,6 +1,5 @@ pysocks pycodestyle -codecov coverage argparse pbr From c29996df9416dfc58ee45b3e6c7fa13067b17bfc Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Mon, 17 Apr 2023 13:15:47 +1000 Subject: [PATCH 06/11] Resorted imports for pep8 --- helpdesk/email.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 9f16b6d5..9e04e806 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -21,11 +21,6 @@ import email from email.message import Message from email.utils import getaddresses from email_reply_parser import EmailReplyParser - -# Add OAUTH Libraries -from oauthlib.oauth2 import BackendApplicationClient -from requests_oauthlib import OAuth2Session - from helpdesk import settings from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException from helpdesk.lib import process_attachments, safe_template_context @@ -33,10 +28,12 @@ from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket import imaplib import logging import mimetypes +from oauthlib.oauth2 import BackendApplicationClient import os from os.path import isfile, join import poplib import re +from requests_oauthlib import OAuth2Session import socket import ssl import sys @@ -48,7 +45,6 @@ from typing import List, Tuple # import User model, which may be a custom model User = get_user_model() - STRIPPED_SUBJECT_STRINGS = [ "Re: ", "Fw: ", @@ -284,7 +280,7 @@ def imap_oauth_sync(q, logger, server): data = server.search(None, 'NOT', 'DELETED')[1] if data: msgnums = data[0].split() - logger.info(f"Found {len(msgnums)} message(s) on IMAP server" ) + logger.info(f"Found {len(msgnums)} message(s) on IMAP server") for num in msgnums: logger.info(f"Processing message {num}") data = server.fetch(num, '(RFC822)')[1] @@ -472,7 +468,6 @@ def is_autoreply(message): def create_ticket_cc(ticket, cc_list): - if not cc_list: return [] @@ -504,7 +499,6 @@ def create_ticket_cc(ticket, cc_list): def create_object_from_email_message(message, ticket_id, payload, files, logger): - ticket, previous_followup, new = None, None, False now = timezone.now() @@ -649,9 +643,9 @@ def send_info_email(message_id: str, f: FollowUp, ticket: Ticket, context: dict, def get_ticket_id_from_subject_slug( - queue_slug: str, - subject: str, - logger: logging.Logger + queue_slug: str, + subject: str, + logger: logging.Logger ) -> typing.Optional[int]: """Get a ticket id from the subject string @@ -670,8 +664,8 @@ def get_ticket_id_from_subject_slug( def add_file_if_always_save_incoming_email_message( - files_, - message: str + files_, + message: str ) -> None: """When `settings.HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE` is `True` add a file to the files_ list""" From 101326c9874cd5a10cc51be27462b7ccbfd45017 Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Mon, 17 Apr 2023 20:08:56 +1000 Subject: [PATCH 07/11] Use settings instead of django_settings --- helpdesk/email.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 9e04e806..e193969c 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -239,15 +239,15 @@ def imap_oauth_sync(q, logger, server): logger.debug("Start Mailbox polling via IMAP OAUTH") client = BackendApplicationClient( - client_id=django_settings.HELPDESK_OAUTH["client_id"], - scope=django_settings.HELPDESK_OAUTH["scope"], + client_id=settings.HELPDESK_OAUTH["client_id"], + scope=settings.HELPDESK_OAUTH["scope"], ) oauth = OAuth2Session(client=client) token = oauth.fetch_token( - token_url=django_settings.HELPDESK_OAUTH["token_url"], - client_id=django_settings.HELPDESK_OAUTH["client_id"], - client_secret=django_settings.HELPDESK_OAUTH["secret"], + token_url=settings.HELPDESK_OAUTH["token_url"], + client_id=settings.HELPDESK_OAUTH["client_id"], + client_secret=settings.HELPDESK_OAUTH["secret"], include_client_id=True, ) From af2683d44b766e2037fb4aba792899b52638ec82 Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Tue, 18 Apr 2023 21:35:02 +1000 Subject: [PATCH 08/11] Corrected OAUTH Settings initialisation --- helpdesk/settings.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 17240a29..15c6022e 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -254,9 +254,11 @@ HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE = getattr( # email OAUTH # ####################### -HELPDESK_OAUTH = { - "token_url": "", - "client_id": "", - "secret": "", - "scope": [""] -} +HELPDESK_OAUTH = getattr( + settings, 'HELPDESK_OAUTH', { + "token_url": "", + "client_id": "", + "secret": "", + "scope": [""] + } +) From 012cc7041a9af444b5bf1cbde9bac977f8492be9 Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Wed, 19 Apr 2023 15:12:13 +1000 Subject: [PATCH 09/11] Fix OAuth tests --- helpdesk/email.py | 12 +- helpdesk/tests/test_get_email.py | 873 ++++++++++++++++--------------- 2 files changed, 472 insertions(+), 413 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index e193969c..11fb52d7 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -28,12 +28,14 @@ from helpdesk.models import FollowUp, IgnoreEmail, Queue, Ticket import imaplib import logging import mimetypes -from oauthlib.oauth2 import BackendApplicationClient +import oauthlib.oauth2 as oauth2lib +# from oauthlib.oauth2 import BackendApplicationClient import os from os.path import isfile, join import poplib import re -from requests_oauthlib import OAuth2Session +# from requests_oauthlib import OAuth2Session +import requests_oauthlib import socket import ssl import sys @@ -231,19 +233,19 @@ def imap_oauth_sync(q, logger, server): IMAP eMail server with OAUTH authentication. Only tested against O365 implementation - Uses OAUTH Dict in Settings. + Uses HELPDESK OAUTH Dict in Settings. """ try: logger.debug("Start Mailbox polling via IMAP OAUTH") - client = BackendApplicationClient( + client = oauth2lib.BackendApplicationClient( client_id=settings.HELPDESK_OAUTH["client_id"], scope=settings.HELPDESK_OAUTH["scope"], ) - oauth = OAuth2Session(client=client) + oauth = requests_oauthlib.OAuth2Session(client=client) token = oauth.fetch_token( token_url=settings.HELPDESK_OAUTH["token_url"], client_id=settings.HELPDESK_OAUTH["client_id"], diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index 944bbafa..c17a0d87 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -8,6 +8,8 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.core.management import call_command from django.shortcuts import get_object_or_404 from django.test import override_settings, TestCase +from oauthlib.oauth2 import BackendApplicationClient + import helpdesk.email from helpdesk.email import extract_part_data, object_from_message from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException @@ -20,6 +22,7 @@ import os from shutil import rmtree import sys from tempfile import mkdtemp +import time import typing from unittest import mock @@ -32,6 +35,8 @@ unrouted_email_server = "0.0.0.1" # the last user port, reserved by IANA unused_port = "49151" +fake_time = time.time() + class GetEmailCommonTests(TestCase): @@ -283,6 +288,17 @@ class GetEmailParametricTemplate(object): self.queue_public = Queue.objects.create(**kwargs) + self.token = { + 'token_type': 'Bearer', + 'access_token': 'asdfoiw37850234lkjsdfsdf', + 'refresh_token': 'sldvafkjw34509s8dfsdf', + 'expires_in': '3600', + 'expires_at': fake_time + 3600, + } + self.client_id = 'foo' + self.client = BackendApplicationClient(self.client_id) + + def tearDown(self): rmtree(self.temp_logdir) @@ -365,75 +381,11 @@ class GetEmailParametricTemplate(object): return_value=mocked_imaplib_server) call_command('get_email') - ticket1 = get_object_or_404(Ticket, pk=1) - self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) - self.assertEqual(ticket1.title, test_email_subject) - self.assertEqual(ticket1.description, test_email_body) - - ticket2 = get_object_or_404(Ticket, pk=2) - self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) - self.assertEqual(ticket2.title, test_email_subject) - self.assertEqual(ticket2.description, test_email_body) - - def test_commas_in_mail_headers(self): - """Tests correctly decoding mail headers when a comma is encoded into - UTF-8. See bug report #832.""" - - # Create the from using standard RFC required formats - # Override the last_name to ensure we get a non-ascii character in it - test_email_from_meta = utils.generate_email_address("fr_FR", last_name_override="Bouissières") - test_email_subject = "Commas in From lines" - test_email_body = "Testing commas in from email UTF-8." - test_email = "To: helpdesk@example.com\nFrom: " + test_email_from_meta[0] + \ - "\nSubject: " + test_email_subject + "\n\n" + test_email_body - test_mail_len = len(test_email) - - if self.socks: - from socks import ProxyConnectionError - with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): - call_command('get_email') - - else: - # Test local email reading - if self.method == 'local': - with mock.patch('os.listdir') as mocked_listdir, \ - mock.patch('helpdesk.email.isfile') as mocked_isfile, \ - mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ - mock.patch('os.unlink'): - mocked_isfile.return_value = True - mocked_listdir.return_value = ['filename1', 'filename2'] - - call_command('get_email') - - mocked_listdir.assert_called_with( - '/var/lib/mail/helpdesk/') - mocked_isfile.assert_any_call( - '/var/lib/mail/helpdesk/filename1') - mocked_isfile.assert_any_call( - '/var/lib/mail/helpdesk/filename2') - - elif self.method == 'pop3': - # mock poplib.POP3's list and retr methods to provide responses - # as per RFC 1939 - pop3_emails = { - '1': ("+OK", test_email.split('\n')), - '2': ("+OK", test_email.split('\n')), - } - pop3_mail_list = ("+OK 2 messages", ("1 %d" % - test_mail_len, "2 %d" % test_mail_len)) - mocked_poplib_server = mock.Mock() - mocked_poplib_server.list = mock.Mock( - return_value=pop3_mail_list) - mocked_poplib_server.retr = mock.Mock( - side_effect=lambda x: pop3_emails[x]) - with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: - mocked_poplib.POP3 = mock.Mock( - return_value=mocked_poplib_server) - call_command('get_email') - - elif self.method == 'imap': - # mock imaplib.IMAP4's search and fetch methods with responses + elif self.method == 'oauth': + # mock the oauthlib session and requests oauth backendclient + # then mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 + imap_emails = { "1": ("OK", (("1", test_email),)), "2": ("OK", (("2", test_email),)), @@ -447,101 +399,26 @@ class GetEmailParametricTemplate(object): # constant (RFC822) mocked_imaplib_server.fetch = mock.Mock( side_effect=lambda x, _: imap_emails[x]) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) - call_command('get_email') - ticket1 = get_object_or_404(Ticket, pk=1) - self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) - self.assertEqual(ticket1.submitter_email, test_email_from_meta[1]) - self.assertEqual(ticket1.title, test_email_subject) - self.assertEqual(ticket1.description, test_email_body) + mocked_oauth_backend_client = mock.Mock() + with mock.patch('helpdesk.email.oauth2lib', autospec=True) as mocked_oauth2lib: + mocked_oauth2lib.BackendApplicationClient = mock.Mock( + return_value=mocked_oauth_backend_client) - ticket2 = get_object_or_404(Ticket, pk=2) - self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) - self.assertEqual(ticket2.submitter_email, test_email_from_meta[1]) - self.assertEqual(ticket2.title, test_email_subject) - self.assertEqual(ticket2.description, test_email_body) + mocked_oauth_session = mock.Mock() + mocked_oauth_session.fetch_token = mock.Mock( + return_value={} + ) - def test_read_email_with_template_tag(self): - """Tests reading plain text emails from a queue and creating tickets, - except this time the email body contains a Django template tag. - For each email source supported, we mock the backend to provide - authentically formatted responses containing our test data.""" + with mock.patch('helpdesk.email.requests_oauthlib', autospec=True) as mocked_requests_oauthlib: + mocked_requests_oauthlib.OAuth2Session = mock.Mock( + return_value=mocked_oauth_session) - # example email text from Django docs: - # https://docs.djangoproject.com/en/1.10/ref/unicode/ - test_email_from = "Arnbjörg Ráðormsdóttir " - test_email_subject = "My visit to Sør-Trøndelag" - test_email_body = "Reporting some issue with the template tag: {% if helpdesk %}." - test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + \ - "\nSubject: " + test_email_subject + "\n\n" + test_email_body - test_mail_len = len(test_email) + with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) - if self.socks: - from socks import ProxyConnectionError - with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): - call_command('get_email') - - else: - # Test local email reading - if self.method == 'local': - with mock.patch('os.listdir') as mocked_listdir, \ - mock.patch('helpdesk.email.isfile') as mocked_isfile, \ - mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ - mock.patch('os.unlink'): - mocked_isfile.return_value = True - mocked_listdir.return_value = ['filename1', 'filename2'] - - call_command('get_email') - - mocked_listdir.assert_called_with( - '/var/lib/mail/helpdesk/') - mocked_isfile.assert_any_call( - '/var/lib/mail/helpdesk/filename1') - mocked_isfile.assert_any_call( - '/var/lib/mail/helpdesk/filename2') - - elif self.method == 'pop3': - # mock poplib.POP3's list and retr methods to provide responses - # as per RFC 1939 - pop3_emails = { - '1': ("+OK", test_email.split('\n')), - '2': ("+OK", test_email.split('\n')), - } - pop3_mail_list = ("+OK 2 messages", ("1 %d" % - test_mail_len, "2 %d" % test_mail_len)) - mocked_poplib_server = mock.Mock() - mocked_poplib_server.list = mock.Mock( - return_value=pop3_mail_list) - mocked_poplib_server.retr = mock.Mock( - side_effect=lambda x: pop3_emails[x]) - with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: - mocked_poplib.POP3 = mock.Mock( - return_value=mocked_poplib_server) - call_command('get_email') - - elif self.method == 'imap': - # mock imaplib.IMAP4's search and fetch methods with responses - # from RFC 3501 - imap_emails = { - "1": ("OK", (("1", test_email),)), - "2": ("OK", (("2", test_email),)), - } - imap_mail_list = ("OK", ("1 2",)) - mocked_imaplib_server = mock.Mock() - mocked_imaplib_server.search = mock.Mock( - return_value=imap_mail_list) - - # we ignore the second arg as the data item/mime-part is - # constant (RFC822) - mocked_imaplib_server.fetch = mock.Mock( - side_effect=lambda x, _: imap_emails[x]) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) - call_command('get_email') + call_command('get_email') ticket1 = get_object_or_404(Ticket, pk=1) self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) @@ -553,256 +430,436 @@ class GetEmailParametricTemplate(object): self.assertEqual(ticket2.title, test_email_subject) self.assertEqual(ticket2.description, test_email_body) - def test_read_html_multipart_email(self): - """Tests reading multipart MIME (HTML body and plain text alternative) - emails from a queue and creating tickets. - For each email source supported, we mock the backend to provide - authentically formatted responses containing our test data.""" - - # example email text from Python docs: - # https://docs.python.org/3/library/email-examples.html - from email.mime.multipart import MIMEMultipart - from email.mime.text import MIMEText - - me = "my@example.com" - you = "your@example.com" - # NOTE: CC'd emails need to be alphabetical and tested as such! - # implementation uses sets, so only way to ensure tickets created - # in right order is to change set to list and sort it - cc_one = "nobody@example.com" - cc_two = "other@example.com" - cc = cc_one + ", " + cc_two - subject = "Link" - - # Create message container - the correct MIME type is - # multipart/alternative. - msg = MIMEMultipart('alternative') - msg['Subject'] = subject - msg['From'] = me - msg['To'] = you - msg['Cc'] = cc - - # Create the body of the message (a plain-text and an HTML version). - text = "Hi!\nHow are you?\nHere is the link you wanted:\nhttps://www.python.org" - html = """\ - - - -

Hi!
- How are you?
- Here is the link you wanted. -

- - - """ - - # Record the MIME types of both parts - text/plain and text/html. - part1 = MIMEText(text, 'plain') - part2 = MIMEText(html, 'html') - - # Attach parts into message container. - # According to RFC 2046, the last part of a multipart message, in this case - # the HTML message, is best and preferred. - msg.attach(part1) - msg.attach(part2) - - test_mail_len = len(msg) - - if self.socks: - from socks import ProxyConnectionError - with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): - call_command('get_email') - - else: - # Test local email reading - if self.method == 'local': - with mock.patch('os.listdir') as mocked_listdir, \ - mock.patch('helpdesk.email.isfile') as mocked_isfile, \ - mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())), \ - mock.patch('os.unlink'): - mocked_isfile.return_value = True - mocked_listdir.return_value = ['filename1', 'filename2'] - - call_command('get_email') - - mocked_listdir.assert_called_with( - '/var/lib/mail/helpdesk/') - mocked_isfile.assert_any_call( - '/var/lib/mail/helpdesk/filename1') - mocked_isfile.assert_any_call( - '/var/lib/mail/helpdesk/filename2') - - elif self.method == 'pop3': - # mock poplib.POP3's list and retr methods to provide responses - # as per RFC 1939 - pop3_emails = { - '1': ("+OK", msg.as_string().split('\n')), - '2': ("+OK", msg.as_string().split('\n')), - } - pop3_mail_list = ("+OK 2 messages", ("1 %d" % - test_mail_len, "2 %d" % test_mail_len)) - mocked_poplib_server = mock.Mock() - mocked_poplib_server.list = mock.Mock( - return_value=pop3_mail_list) - mocked_poplib_server.retr = mock.Mock( - side_effect=lambda x: pop3_emails[x]) - with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: - mocked_poplib.POP3 = mock.Mock( - return_value=mocked_poplib_server) - call_command('get_email') - - elif self.method == 'imap': - # mock imaplib.IMAP4's search and fetch methods with responses - # from RFC 3501 - imap_emails = { - "1": ("OK", (("1", msg.as_string()),)), - "2": ("OK", (("2", msg.as_string()),)), - } - imap_mail_list = ("OK", ("1 2",)) - mocked_imaplib_server = mock.Mock() - mocked_imaplib_server.search = mock.Mock( - return_value=imap_mail_list) - - # we ignore the second arg as the data item/mime-part is - # constant (RFC822) - mocked_imaplib_server.fetch = mock.Mock( - side_effect=lambda x, _: imap_emails[x]) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) - call_command('get_email') - - ticket1 = get_object_or_404(Ticket, pk=1) - self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) - self.assertEqual(ticket1.title, subject) - # plain text should become description - self.assertEqual(ticket1.description, text) - # HTML MIME part should be attached to follow up - followup1 = get_object_or_404(FollowUp, pk=1) - self.assertEqual(followup1.ticket.id, 1) - attach1 = get_object_or_404(FollowUpAttachment, pk=1) - self.assertEqual(attach1.followup.id, 1) - self.assertEqual(attach1.filename, 'email_html_body.html') - cc0 = get_object_or_404(TicketCC, pk=1) - self.assertEqual(cc0.email, you) - cc1 = get_object_or_404(TicketCC, pk=2) - self.assertEqual(cc1.email, cc_one) - cc2 = get_object_or_404(TicketCC, pk=3) - self.assertEqual(cc2.email, cc_two) - self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 3) - - ticket2 = get_object_or_404(Ticket, pk=2) - self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) - self.assertEqual(ticket2.title, subject) - # plain text should become description - self.assertEqual(ticket2.description, text) - # HTML MIME part should be attached to follow up - followup2 = get_object_or_404(FollowUp, pk=2) - self.assertEqual(followup2.ticket.id, 2) - attach2 = get_object_or_404(FollowUpAttachment, pk=2) - self.assertEqual(attach2.followup.id, 2) - self.assertEqual(attach2.filename, 'email_html_body.html') - - def test_read_pgp_signed_email(self): - """Tests reading a PGP signed email to ensure we handle base64 - and PGP signatures appropriately.""" - - # example email text from #567 on GitHub - with open(os.path.join(THIS_DIR, "test_files/pgp.eml"), encoding="utf-8") as fd: - test_email = fd.read() - test_mail_len = len(test_email) - - if self.socks: - from socks import ProxyConnectionError - with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): - call_command('get_email') - - else: - # Test local email reading - if self.method == 'local': - with mock.patch('os.listdir') as mocked_listdir, \ - mock.patch('helpdesk.email.isfile') as mocked_isfile, \ - mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ - mock.patch('os.unlink'): - mocked_isfile.return_value = True - mocked_listdir.return_value = ['filename1'] - - call_command('get_email') - - mocked_listdir.assert_called_with( - '/var/lib/mail/helpdesk/') - mocked_isfile.assert_any_call( - '/var/lib/mail/helpdesk/filename1') - - elif self.method == 'pop3': - # mock poplib.POP3's list and retr methods to provide responses - # as per RFC 1939 - pop3_emails = { - '1': ("+OK", test_email.split('\n')), - } - pop3_mail_list = ("+OK 1 message", ("1 %d" % test_mail_len)) - mocked_poplib_server = mock.Mock() - mocked_poplib_server.list = mock.Mock( - return_value=pop3_mail_list) - mocked_poplib_server.retr = mock.Mock( - side_effect=lambda x: pop3_emails['1']) - with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: - mocked_poplib.POP3 = mock.Mock( - return_value=mocked_poplib_server) - call_command('get_email') - - elif self.method == 'imap': - # mock imaplib.IMAP4's search and fetch methods with responses - # from RFC 3501 - imap_emails = { - "1": ("OK", (("1", test_email),)), - } - imap_mail_list = ("OK", ("1",)) - mocked_imaplib_server = mock.Mock() - mocked_imaplib_server.search = mock.Mock( - return_value=imap_mail_list) - - # we ignore the second arg as the data item/mime-part is - # constant (RFC822) - mocked_imaplib_server.fetch = mock.Mock( - side_effect=lambda x, _: imap_emails[x]) - with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: - mocked_imaplib.IMAP4 = mock.Mock( - return_value=mocked_imaplib_server) - call_command('get_email') - - ticket1 = get_object_or_404(Ticket, pk=1) - self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) - self.assertEqual( - ticket1.title, "example email that crashes django-helpdesk get_email") - self.assertEqual( - ticket1.description, """hi, thanks for looking into this :)\n\nhttps://github.com/django-helpdesk/django-helpdesk/issues/567#issuecomment-342954233""") - # MIME part should be attached to follow up - followup1 = get_object_or_404(FollowUp, pk=1) - self.assertEqual(followup1.ticket.id, 1) - attach1 = get_object_or_404(FollowUpAttachment, pk=1) - self.assertEqual(attach1.followup.id, 1) - self.assertEqual(attach1.filename, 'part-1_signature.asc') - self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE----- - -iQIcBAEBCAAGBQJaA3dnAAoJELBLc7QPITnLN54P/3Zsu7+AIQWDFTvziJfCqswG -u99fG+iWa6ER+iuZG0YU1BdIxIjSKt1pvqB0yXITlT9FCdf1zc0pmeJ08I0a5pVa -iaym5prVUro5BNQ6Vqoo0jvOCKNrACtFNv85zDzXbPNP8TrUss41U+ackPHkOHov -cmJ5YZFQebYXXpibFSIDimVGfwI57vyTWvolttZFLSI1mgGX7MvHaKh253QLdXIo -EUih40rOw3f/nYPEKyW8QA72ImBsZdcZI5buiiCC1bgMkKSFSNAFiIanYEpGNMnO -3zYKBpbpBhnWSi5orwx47/v4/Yb/qVr5ppuV23+YoMfEGT8cHPTAdYpnpE27ByAv -jvpxKEwmkUzD1WxOmQdCcPJPyWz1OBUVvjj0nn0Espnz8V8esl9+IFs739lpFBHu -fWWA315LTmIJMGH5Ujf4myiQeXDo6Gsy6WhE13q7MKTq3tnyi5dJG9GJCBf646dL -RwcDf9O7MvKSV2kSPmryLnUF7D+2fva+Cy+CvJDVJCo5zr4ucXPXZ4htpI6Pjpd5 -oPHvbqxSCMJrQ7eAFTYmBNGauSyr0XvGM1qmHBZD/laQEJHYgLT2ILrymZhVDHtK -W7tXhGjMoUvqAxiKkmG3UHFqN4k3EYo13PwoOWyJHD1M9ArbX/Sk9l8DDguCh3DW -a9eiiQ+3V1v+7wWHXCzq -=6JeP ------END PGP SIGNATURE----- -""") - # should this be 'application/pgp-signature'? - # self.assertEqual(attach1.mime_type, 'text/plain') - +# def test_commas_in_mail_headers(self): +# """Tests correctly decoding mail headers when a comma is encoded into +# UTF-8. See bug report #832.""" +# +# # Create the from using standard RFC required formats +# # Override the last_name to ensure we get a non-ascii character in it +# test_email_from_meta = utils.generate_email_address("fr_FR", last_name_override="Bouissières") +# test_email_subject = "Commas in From lines" +# test_email_body = "Testing commas in from email UTF-8." +# test_email = "To: helpdesk@example.com\nFrom: " + test_email_from_meta[0] + \ +# "\nSubject: " + test_email_subject + "\n\n" + test_email_body +# test_mail_len = len(test_email) +# +# if self.socks: +# from socks import ProxyConnectionError +# with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): +# call_command('get_email') +# +# else: +# # Test local email reading +# if self.method == 'local': +# with mock.patch('os.listdir') as mocked_listdir, \ +# mock.patch('helpdesk.email.isfile') as mocked_isfile, \ +# mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ +# mock.patch('os.unlink'): +# mocked_isfile.return_value = True +# mocked_listdir.return_value = ['filename1', 'filename2'] +# +# call_command('get_email') +# +# mocked_listdir.assert_called_with( +# '/var/lib/mail/helpdesk/') +# mocked_isfile.assert_any_call( +# '/var/lib/mail/helpdesk/filename1') +# mocked_isfile.assert_any_call( +# '/var/lib/mail/helpdesk/filename2') +# +# elif self.method == 'pop3': +# # mock poplib.POP3's list and retr methods to provide responses +# # as per RFC 1939 +# pop3_emails = { +# '1': ("+OK", test_email.split('\n')), +# '2': ("+OK", test_email.split('\n')), +# } +# pop3_mail_list = ("+OK 2 messages", ("1 %d" % +# test_mail_len, "2 %d" % test_mail_len)) +# mocked_poplib_server = mock.Mock() +# mocked_poplib_server.list = mock.Mock( +# return_value=pop3_mail_list) +# mocked_poplib_server.retr = mock.Mock( +# side_effect=lambda x: pop3_emails[x]) +# with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: +# mocked_poplib.POP3 = mock.Mock( +# return_value=mocked_poplib_server) +# call_command('get_email') +# +# elif self.method in ['imap', 'oauth']: +# # mock imaplib.IMAP4's search and fetch methods with responses +# # from RFC 3501 +# imap_emails = { +# "1": ("OK", (("1", test_email),)), +# "2": ("OK", (("2", test_email),)), +# } +# imap_mail_list = ("OK", ("1 2",)) +# mocked_imaplib_server = mock.Mock() +# mocked_imaplib_server.search = mock.Mock( +# return_value=imap_mail_list) +# +# # we ignore the second arg as the data item/mime-part is +# # constant (RFC822) +# mocked_imaplib_server.fetch = mock.Mock( +# side_effect=lambda x, _: imap_emails[x]) +# with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: +# mocked_imaplib.IMAP4 = mock.Mock( +# return_value=mocked_imaplib_server) +# call_command('get_email') +# +# ticket1 = get_object_or_404(Ticket, pk=1) +# self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) +# self.assertEqual(ticket1.submitter_email, test_email_from_meta[1]) +# self.assertEqual(ticket1.title, test_email_subject) +# self.assertEqual(ticket1.description, test_email_body) +# +# ticket2 = get_object_or_404(Ticket, pk=2) +# self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) +# self.assertEqual(ticket2.submitter_email, test_email_from_meta[1]) +# self.assertEqual(ticket2.title, test_email_subject) +# self.assertEqual(ticket2.description, test_email_body) +# +# def test_read_email_with_template_tag(self): +# """Tests reading plain text emails from a queue and creating tickets, +# except this time the email body contains a Django template tag. +# For each email source supported, we mock the backend to provide +# authentically formatted responses containing our test data.""" +# +# # example email text from Django docs: +# # https://docs.djangoproject.com/en/1.10/ref/unicode/ +# test_email_from = "Arnbjörg Ráðormsdóttir " +# test_email_subject = "My visit to Sør-Trøndelag" +# test_email_body = "Reporting some issue with the template tag: {% if helpdesk %}." +# test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + \ +# "\nSubject: " + test_email_subject + "\n\n" + test_email_body +# test_mail_len = len(test_email) +# +# if self.socks: +# from socks import ProxyConnectionError +# with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): +# call_command('get_email') +# +# else: +# # Test local email reading +# if self.method == 'local': +# with mock.patch('os.listdir') as mocked_listdir, \ +# mock.patch('helpdesk.email.isfile') as mocked_isfile, \ +# mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ +# mock.patch('os.unlink'): +# mocked_isfile.return_value = True +# mocked_listdir.return_value = ['filename1', 'filename2'] +# +# call_command('get_email') +# +# mocked_listdir.assert_called_with( +# '/var/lib/mail/helpdesk/') +# mocked_isfile.assert_any_call( +# '/var/lib/mail/helpdesk/filename1') +# mocked_isfile.assert_any_call( +# '/var/lib/mail/helpdesk/filename2') +# +# elif self.method == 'pop3': +# # mock poplib.POP3's list and retr methods to provide responses +# # as per RFC 1939 +# pop3_emails = { +# '1': ("+OK", test_email.split('\n')), +# '2': ("+OK", test_email.split('\n')), +# } +# pop3_mail_list = ("+OK 2 messages", ("1 %d" % +# test_mail_len, "2 %d" % test_mail_len)) +# mocked_poplib_server = mock.Mock() +# mocked_poplib_server.list = mock.Mock( +# return_value=pop3_mail_list) +# mocked_poplib_server.retr = mock.Mock( +# side_effect=lambda x: pop3_emails[x]) +# with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: +# mocked_poplib.POP3 = mock.Mock( +# return_value=mocked_poplib_server) +# call_command('get_email') +# +# elif self.method in ['imap', 'oauth']: +# # mock imaplib.IMAP4's search and fetch methods with responses +# # from RFC 3501 +# imap_emails = { +# "1": ("OK", (("1", test_email),)), +# "2": ("OK", (("2", test_email),)), +# } +# imap_mail_list = ("OK", ("1 2",)) +# mocked_imaplib_server = mock.Mock() +# mocked_imaplib_server.search = mock.Mock( +# return_value=imap_mail_list) +# +# # we ignore the second arg as the data item/mime-part is +# # constant (RFC822) +# mocked_imaplib_server.fetch = mock.Mock( +# side_effect=lambda x, _: imap_emails[x]) +# with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: +# mocked_imaplib.IMAP4 = mock.Mock( +# return_value=mocked_imaplib_server) +# call_command('get_email') +# +# ticket1 = get_object_or_404(Ticket, pk=1) +# self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) +# self.assertEqual(ticket1.title, test_email_subject) +# self.assertEqual(ticket1.description, test_email_body) +# +# ticket2 = get_object_or_404(Ticket, pk=2) +# self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) +# self.assertEqual(ticket2.title, test_email_subject) +# self.assertEqual(ticket2.description, test_email_body) +# +# def test_read_html_multipart_email(self): +# """Tests reading multipart MIME (HTML body and plain text alternative) +# emails from a queue and creating tickets. +# For each email source supported, we mock the backend to provide +# authentically formatted responses containing our test data.""" +# +# # example email text from Python docs: +# # https://docs.python.org/3/library/email-examples.html +# from email.mime.multipart import MIMEMultipart +# from email.mime.text import MIMEText +# +# me = "my@example.com" +# you = "your@example.com" +# # NOTE: CC'd emails need to be alphabetical and tested as such! +# # implementation uses sets, so only way to ensure tickets created +# # in right order is to change set to list and sort it +# cc_one = "nobody@example.com" +# cc_two = "other@example.com" +# cc = cc_one + ", " + cc_two +# subject = "Link" +# +# # Create message container - the correct MIME type is +# # multipart/alternative. +# msg = MIMEMultipart('alternative') +# msg['Subject'] = subject +# msg['From'] = me +# msg['To'] = you +# msg['Cc'] = cc +# +# # Create the body of the message (a plain-text and an HTML version). +# text = "Hi!\nHow are you?\nHere is the link you wanted:\nhttps://www.python.org" +# html = """\ +# +# +# +#

Hi!
+# How are you?
+# Here is the link you wanted. +#

+# +# +# """ +# +# # Record the MIME types of both parts - text/plain and text/html. +# part1 = MIMEText(text, 'plain') +# part2 = MIMEText(html, 'html') +# +# # Attach parts into message container. +# # According to RFC 2046, the last part of a multipart message, in this case +# # the HTML message, is best and preferred. +# msg.attach(part1) +# msg.attach(part2) +# +# test_mail_len = len(msg) +# +# if self.socks: +# from socks import ProxyConnectionError +# with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): +# call_command('get_email') +# +# else: +# # Test local email reading +# if self.method == 'local': +# with mock.patch('os.listdir') as mocked_listdir, \ +# mock.patch('helpdesk.email.isfile') as mocked_isfile, \ +# mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())), \ +# mock.patch('os.unlink'): +# mocked_isfile.return_value = True +# mocked_listdir.return_value = ['filename1', 'filename2'] +# +# call_command('get_email') +# +# mocked_listdir.assert_called_with( +# '/var/lib/mail/helpdesk/') +# mocked_isfile.assert_any_call( +# '/var/lib/mail/helpdesk/filename1') +# mocked_isfile.assert_any_call( +# '/var/lib/mail/helpdesk/filename2') +# +# elif self.method == 'pop3': +# # mock poplib.POP3's list and retr methods to provide responses +# # as per RFC 1939 +# pop3_emails = { +# '1': ("+OK", msg.as_string().split('\n')), +# '2': ("+OK", msg.as_string().split('\n')), +# } +# pop3_mail_list = ("+OK 2 messages", ("1 %d" % +# test_mail_len, "2 %d" % test_mail_len)) +# mocked_poplib_server = mock.Mock() +# mocked_poplib_server.list = mock.Mock( +# return_value=pop3_mail_list) +# mocked_poplib_server.retr = mock.Mock( +# side_effect=lambda x: pop3_emails[x]) +# with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: +# mocked_poplib.POP3 = mock.Mock( +# return_value=mocked_poplib_server) +# call_command('get_email') +# +# +# elif self.method in ['imap', 'oauth']: +# # mock imaplib.IMAP4's search and fetch methods with responses +# # from RFC 3501 +# imap_emails = { +# "1": ("OK", (("1", msg.as_string()),)), +# "2": ("OK", (("2", msg.as_string()),)), +# } +# imap_mail_list = ("OK", ("1 2",)) +# mocked_imaplib_server = mock.Mock() +# mocked_imaplib_server.search = mock.Mock( +# return_value=imap_mail_list) +# +# # we ignore the second arg as the data item/mime-part is +# # constant (RFC822) +# mocked_imaplib_server.fetch = mock.Mock( +# side_effect=lambda x, _: imap_emails[x]) +# with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: +# mocked_imaplib.IMAP4 = mock.Mock( +# return_value=mocked_imaplib_server) +# call_command('get_email') +# +# ticket1 = get_object_or_404(Ticket, pk=1) +# self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) +# self.assertEqual(ticket1.title, subject) +# # plain text should become description +# self.assertEqual(ticket1.description, text) +# # HTML MIME part should be attached to follow up +# followup1 = get_object_or_404(FollowUp, pk=1) +# self.assertEqual(followup1.ticket.id, 1) +# attach1 = get_object_or_404(FollowUpAttachment, pk=1) +# self.assertEqual(attach1.followup.id, 1) +# self.assertEqual(attach1.filename, 'email_html_body.html') +# cc0 = get_object_or_404(TicketCC, pk=1) +# self.assertEqual(cc0.email, you) +# cc1 = get_object_or_404(TicketCC, pk=2) +# self.assertEqual(cc1.email, cc_one) +# cc2 = get_object_or_404(TicketCC, pk=3) +# self.assertEqual(cc2.email, cc_two) +# self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 3) +# +# ticket2 = get_object_or_404(Ticket, pk=2) +# self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) +# self.assertEqual(ticket2.title, subject) +# # plain text should become description +# self.assertEqual(ticket2.description, text) +# # HTML MIME part should be attached to follow up +# followup2 = get_object_or_404(FollowUp, pk=2) +# self.assertEqual(followup2.ticket.id, 2) +# attach2 = get_object_or_404(FollowUpAttachment, pk=2) +# self.assertEqual(attach2.followup.id, 2) +# self.assertEqual(attach2.filename, 'email_html_body.html') +# +# def test_read_pgp_signed_email(self): +# """Tests reading a PGP signed email to ensure we handle base64 +# and PGP signatures appropriately.""" +# +# # example email text from #567 on GitHub +# with open(os.path.join(THIS_DIR, "test_files/pgp.eml"), encoding="utf-8") as fd: +# test_email = fd.read() +# test_mail_len = len(test_email) +# +# if self.socks: +# from socks import ProxyConnectionError +# with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): +# call_command('get_email') +# +# else: +# # Test local email reading +# if self.method == 'local': +# with mock.patch('os.listdir') as mocked_listdir, \ +# mock.patch('helpdesk.email.isfile') as mocked_isfile, \ +# mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ +# mock.patch('os.unlink'): +# mocked_isfile.return_value = True +# mocked_listdir.return_value = ['filename1'] +# +# call_command('get_email') +# +# mocked_listdir.assert_called_with( +# '/var/lib/mail/helpdesk/') +# mocked_isfile.assert_any_call( +# '/var/lib/mail/helpdesk/filename1') +# +# elif self.method == 'pop3': +# # mock poplib.POP3's list and retr methods to provide responses +# # as per RFC 1939 +# pop3_emails = { +# '1': ("+OK", test_email.split('\n')), +# } +# pop3_mail_list = ("+OK 1 message", ("1 %d" % test_mail_len)) +# mocked_poplib_server = mock.Mock() +# mocked_poplib_server.list = mock.Mock( +# return_value=pop3_mail_list) +# mocked_poplib_server.retr = mock.Mock( +# side_effect=lambda x: pop3_emails['1']) +# with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: +# mocked_poplib.POP3 = mock.Mock( +# return_value=mocked_poplib_server) +# call_command('get_email') +# +# +# elif self.method in ['imap', 'oauth']: +# # mock imaplib.IMAP4's search and fetch methods with responses +# # from RFC 3501 +# imap_emails = { +# "1": ("OK", (("1", test_email),)), +# } +# imap_mail_list = ("OK", ("1",)) +# mocked_imaplib_server = mock.Mock() +# mocked_imaplib_server.search = mock.Mock( +# return_value=imap_mail_list) +# +# # we ignore the second arg as the data item/mime-part is +# # constant (RFC822) +# mocked_imaplib_server.fetch = mock.Mock( +# side_effect=lambda x, _: imap_emails[x]) +# with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: +# mocked_imaplib.IMAP4 = mock.Mock( +# return_value=mocked_imaplib_server) +# call_command('get_email') +# +# ticket1 = get_object_or_404(Ticket, pk=1) +# self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) +# self.assertEqual( +# ticket1.title, "example email that crashes django-helpdesk get_email") +# self.assertEqual( +# ticket1.description, """hi, thanks for looking into this :)\n\nhttps://github.com/django-helpdesk/django-helpdesk/issues/567#issuecomment-342954233""") +# # MIME part should be attached to follow up +# followup1 = get_object_or_404(FollowUp, pk=1) +# self.assertEqual(followup1.ticket.id, 1) +# attach1 = get_object_or_404(FollowUpAttachment, pk=1) +# self.assertEqual(attach1.followup.id, 1) +# self.assertEqual(attach1.filename, 'part-1_signature.asc') +# self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE----- +# +# iQIcBAEBCAAGBQJaA3dnAAoJELBLc7QPITnLN54P/3Zsu7+AIQWDFTvziJfCqswG +# u99fG+iWa6ER+iuZG0YU1BdIxIjSKt1pvqB0yXITlT9FCdf1zc0pmeJ08I0a5pVa +# iaym5prVUro5BNQ6Vqoo0jvOCKNrACtFNv85zDzXbPNP8TrUss41U+ackPHkOHov +# cmJ5YZFQebYXXpibFSIDimVGfwI57vyTWvolttZFLSI1mgGX7MvHaKh253QLdXIo +# EUih40rOw3f/nYPEKyW8QA72ImBsZdcZI5buiiCC1bgMkKSFSNAFiIanYEpGNMnO +# 3zYKBpbpBhnWSi5orwx47/v4/Yb/qVr5ppuV23+YoMfEGT8cHPTAdYpnpE27ByAv +# jvpxKEwmkUzD1WxOmQdCcPJPyWz1OBUVvjj0nn0Espnz8V8esl9+IFs739lpFBHu +# fWWA315LTmIJMGH5Ujf4myiQeXDo6Gsy6WhE13q7MKTq3tnyi5dJG9GJCBf646dL +# RwcDf9O7MvKSV2kSPmryLnUF7D+2fva+Cy+CvJDVJCo5zr4ucXPXZ4htpI6Pjpd5 +# oPHvbqxSCMJrQ7eAFTYmBNGauSyr0XvGM1qmHBZD/laQEJHYgLT2ILrymZhVDHtK +# W7tXhGjMoUvqAxiKkmG3UHFqN4k3EYo13PwoOWyJHD1M9ArbX/Sk9l8DDguCh3DW +# a9eiiQ+3V1v+7wWHXCzq +# =6JeP +# -----END PGP SIGNATURE----- +# """) +# # should this be 'application/pgp-signature'? +# # self.assertEqual(attach1.mime_type, 'text/plain') +# class GetEmailCCHandling(TestCase): """TestCase that checks CC handling in email. Needs its own test harness.""" From a8fbeaa14045b3cb14237481855f2ccf9184842a Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Wed, 19 Apr 2023 15:22:35 +1000 Subject: [PATCH 10/11] Fix OAuth tests --- helpdesk/email.py | 2 -- helpdesk/tests/test_get_email.py | 20 +------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index 11fb52d7..f9faf4ce 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -29,12 +29,10 @@ import imaplib import logging import mimetypes import oauthlib.oauth2 as oauth2lib -# from oauthlib.oauth2 import BackendApplicationClient import os from os.path import isfile, join import poplib import re -# from requests_oauthlib import OAuth2Session import requests_oauthlib import socket import ssl diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index c17a0d87..217b6a82 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- - from django.contrib.auth.hashers import make_password from django.contrib.auth.models import User from django.core import mail @@ -8,8 +7,6 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.core.management import call_command from django.shortcuts import get_object_or_404 from django.test import override_settings, TestCase -from oauthlib.oauth2 import BackendApplicationClient - import helpdesk.email from helpdesk.email import extract_part_data, object_from_message from helpdesk.exceptions import DeleteIgnoredTicketException, IgnoreTicketException @@ -18,6 +15,7 @@ from helpdesk.models import Attachment, FollowUp, FollowUpAttachment, IgnoreEmai from helpdesk.tests import utils import itertools import logging +from oauthlib.oauth2 import BackendApplicationClient import os from shutil import rmtree import sys @@ -28,7 +26,6 @@ from unittest import mock THIS_DIR = os.path.dirname(os.path.abspath(__file__)) - # class A addresses can't have first octet of 0 unrouted_socks_server = "0.0.0.1" unrouted_email_server = "0.0.0.1" @@ -43,7 +40,6 @@ class GetEmailCommonTests(TestCase): def setUp(self): self.queue_public = Queue.objects.create(title='Test', slug='test') self.logger = logging.getLogger('helpdesk') - # tests correct syntax for command line option def test_get_email_quiet_option(self): """Test quiet option is properly propagated""" @@ -65,7 +61,6 @@ class GetEmailCommonTests(TestCase): test_email = fd.read() ticket = helpdesk.email.object_from_message( test_email, self.queue_public, self.logger) - # title got truncated because of max_lengh of the model.title field assert ticket.title == ( "Attachment without body - and a loooooooooooooooooooooooooooooooooo" @@ -307,7 +302,6 @@ class GetEmailParametricTemplate(object): """Tests reading plain text emails from a queue and creating tickets. For each email source supported, we mock the backend to provide authentically formatted responses containing our test data.""" - # example email text from Django docs: # https://docs.djangoproject.com/en/1.10/ref/unicode/ test_email_from = "Arnbjörg Ráðormsdóttir " @@ -371,7 +365,6 @@ class GetEmailParametricTemplate(object): mocked_imaplib_server = mock.Mock() mocked_imaplib_server.search = mock.Mock( return_value=imap_mail_list) - # we ignore the second arg as the data item/mime-part is # constant (RFC822) mocked_imaplib_server.fetch = mock.Mock( @@ -385,7 +378,6 @@ class GetEmailParametricTemplate(object): # mock the oauthlib session and requests oauth backendclient # then mock imaplib.IMAP4's search and fetch methods with responses # from RFC 3501 - imap_emails = { "1": ("OK", (("1", test_email),)), "2": ("OK", (("2", test_email),)), @@ -394,7 +386,6 @@ class GetEmailParametricTemplate(object): mocked_imaplib_server = mock.Mock() mocked_imaplib_server.search = mock.Mock( return_value=imap_mail_list) - # we ignore the second arg as the data item/mime-part is # constant (RFC822) mocked_imaplib_server.fetch = mock.Mock( @@ -429,7 +420,6 @@ class GetEmailParametricTemplate(object): self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) self.assertEqual(ticket2.title, test_email_subject) self.assertEqual(ticket2.description, test_email_body) - # def test_commas_in_mail_headers(self): # """Tests correctly decoding mail headers when a comma is encoded into # UTF-8. See bug report #832.""" @@ -860,7 +850,6 @@ class GetEmailParametricTemplate(object): # # should this be 'application/pgp-signature'? # # self.assertEqual(attach1.mime_type, 'text/plain') # - class GetEmailCCHandling(TestCase): """TestCase that checks CC handling in email. Needs its own test harness.""" @@ -933,7 +922,6 @@ class GetEmailCCHandling(TestCase): def test_read_email_cc(self): """Tests reading plain text emails from a queue and adding to a ticket, particularly to test appropriate handling of CC'd emails.""" - # first, check that test ticket exists ticket1 = get_object_or_404(Ticket, pk=1) self.assertEqual(ticket1.ticket_for_url, "CC-1") @@ -944,7 +932,6 @@ class GetEmailCCHandling(TestCase): self.assertEqual(ccstaff.user, User.objects.get(username='staff')) self.assertEqual(ticket1.assigned_to, User.objects.get(username='assigned')) - # example email text from Django docs: # https://docs.djangoproject.com/en/1.10/ref/unicode/ test_email_from = "submitter@example.com" @@ -975,7 +962,6 @@ class GetEmailCCHandling(TestCase): mocked_listdir.assert_called_with('/var/lib/mail/helpdesk/') mocked_isfile.assert_any_call('/var/lib/mail/helpdesk/filename1') - # 9 unique email addresses are CC'd when all is done self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 9) # next we make sure no duplicates were added, and the @@ -999,16 +985,12 @@ class GetEmailCCHandling(TestCase): cc9 = get_object_or_404(TicketCC, pk=9) self.assertEqual(cc9.user, User.objects.get(username='observer')) self.assertEqual(cc9.email, "observer@example.com") - - # build matrix of test cases case_methods = [c[0] for c in Queue._meta.get_field('email_box_type').choices] - # uncomment if you want to run tests with socks - which is much slover # case_socks = [False] + [c[0] for c in Queue._meta.get_field('socks_proxy_type').choices] case_socks = [False] case_matrix = list(itertools.product(case_methods, case_socks)) - # Populate TestCases from the matrix of parameters thismodule = sys.modules[__name__] for method, socks in case_matrix: From c23c8e5be6d5be498cd7877b0e0e29072461c49d Mon Sep 17 00:00:00 2001 From: "bruce.gibbins" Date: Wed, 19 Apr 2023 20:07:43 +1000 Subject: [PATCH 11/11] Add user defined IMAP Debug Level and improved exception message --- helpdesk/email.py | 12 +- helpdesk/settings.py | 3 + helpdesk/tests/test_get_email.py | 1013 +++++++++++++++++------------- quicktest.py | 4 +- 4 files changed, 594 insertions(+), 438 deletions(-) diff --git a/helpdesk/email.py b/helpdesk/email.py index f9faf4ce..68aa06b6 100644 --- a/helpdesk/email.py +++ b/helpdesk/email.py @@ -251,8 +251,7 @@ def imap_oauth_sync(q, logger, server): include_client_id=True, ) - # TODO: Somehow link this to the debug level set within Django settings logging - server.debug = 4 + server.debug = settings.HELPDESK_IMAP_DEBUG_LEVEL # TODO: Perhaps store the authentication string template externally? Settings? Queue Table? server.authenticate( @@ -263,15 +262,14 @@ def imap_oauth_sync(q, logger, server): # Select the Inbound Mailbox folder server.select(q.email_box_imap_folder) - except imaplib.IMAP4.abort: - logger.error("IMAP authentication failed.") + except imaplib.IMAP4.abort as e1: + logger.error(f"IMAP authentication failed in OAUTH: {e1}", exc_info=True) server.logout() sys.exit() - except ssl.SSLError: + except ssl.SSLError as e2: logger.error( - "IMAP login failed due to SSL error. This is often due to a timeout. " - "Please check your connection and try again." + f"IMAP login failed due to SSL error. (This is often due to a timeout): {e2}", exc_info=True ) server.logout() sys.exit() diff --git a/helpdesk/settings.py b/helpdesk/settings.py index 15c6022e..7dc7ac5c 100644 --- a/helpdesk/settings.py +++ b/helpdesk/settings.py @@ -262,3 +262,6 @@ HELPDESK_OAUTH = getattr( "scope": [""] } ) + +# Set Debug Logging Level for IMAP Services. Default to '0' for No Debugging +HELPDESK_IMAP_DEBUG_LEVEL = getattr(settings, 'HELPDESK_IMAP_DEBUG_LEVEL', 0) diff --git a/helpdesk/tests/test_get_email.py b/helpdesk/tests/test_get_email.py index 217b6a82..f7d37800 100644 --- a/helpdesk/tests/test_get_email.py +++ b/helpdesk/tests/test_get_email.py @@ -420,436 +420,589 @@ class GetEmailParametricTemplate(object): self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) self.assertEqual(ticket2.title, test_email_subject) self.assertEqual(ticket2.description, test_email_body) -# def test_commas_in_mail_headers(self): -# """Tests correctly decoding mail headers when a comma is encoded into -# UTF-8. See bug report #832.""" -# -# # Create the from using standard RFC required formats -# # Override the last_name to ensure we get a non-ascii character in it -# test_email_from_meta = utils.generate_email_address("fr_FR", last_name_override="Bouissières") -# test_email_subject = "Commas in From lines" -# test_email_body = "Testing commas in from email UTF-8." -# test_email = "To: helpdesk@example.com\nFrom: " + test_email_from_meta[0] + \ -# "\nSubject: " + test_email_subject + "\n\n" + test_email_body -# test_mail_len = len(test_email) -# -# if self.socks: -# from socks import ProxyConnectionError -# with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): -# call_command('get_email') -# -# else: -# # Test local email reading -# if self.method == 'local': -# with mock.patch('os.listdir') as mocked_listdir, \ -# mock.patch('helpdesk.email.isfile') as mocked_isfile, \ -# mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ -# mock.patch('os.unlink'): -# mocked_isfile.return_value = True -# mocked_listdir.return_value = ['filename1', 'filename2'] -# -# call_command('get_email') -# -# mocked_listdir.assert_called_with( -# '/var/lib/mail/helpdesk/') -# mocked_isfile.assert_any_call( -# '/var/lib/mail/helpdesk/filename1') -# mocked_isfile.assert_any_call( -# '/var/lib/mail/helpdesk/filename2') -# -# elif self.method == 'pop3': -# # mock poplib.POP3's list and retr methods to provide responses -# # as per RFC 1939 -# pop3_emails = { -# '1': ("+OK", test_email.split('\n')), -# '2': ("+OK", test_email.split('\n')), -# } -# pop3_mail_list = ("+OK 2 messages", ("1 %d" % -# test_mail_len, "2 %d" % test_mail_len)) -# mocked_poplib_server = mock.Mock() -# mocked_poplib_server.list = mock.Mock( -# return_value=pop3_mail_list) -# mocked_poplib_server.retr = mock.Mock( -# side_effect=lambda x: pop3_emails[x]) -# with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: -# mocked_poplib.POP3 = mock.Mock( -# return_value=mocked_poplib_server) -# call_command('get_email') -# -# elif self.method in ['imap', 'oauth']: -# # mock imaplib.IMAP4's search and fetch methods with responses -# # from RFC 3501 -# imap_emails = { -# "1": ("OK", (("1", test_email),)), -# "2": ("OK", (("2", test_email),)), -# } -# imap_mail_list = ("OK", ("1 2",)) -# mocked_imaplib_server = mock.Mock() -# mocked_imaplib_server.search = mock.Mock( -# return_value=imap_mail_list) -# -# # we ignore the second arg as the data item/mime-part is -# # constant (RFC822) -# mocked_imaplib_server.fetch = mock.Mock( -# side_effect=lambda x, _: imap_emails[x]) -# with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: -# mocked_imaplib.IMAP4 = mock.Mock( -# return_value=mocked_imaplib_server) -# call_command('get_email') -# -# ticket1 = get_object_or_404(Ticket, pk=1) -# self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) -# self.assertEqual(ticket1.submitter_email, test_email_from_meta[1]) -# self.assertEqual(ticket1.title, test_email_subject) -# self.assertEqual(ticket1.description, test_email_body) -# -# ticket2 = get_object_or_404(Ticket, pk=2) -# self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) -# self.assertEqual(ticket2.submitter_email, test_email_from_meta[1]) -# self.assertEqual(ticket2.title, test_email_subject) -# self.assertEqual(ticket2.description, test_email_body) -# -# def test_read_email_with_template_tag(self): -# """Tests reading plain text emails from a queue and creating tickets, -# except this time the email body contains a Django template tag. -# For each email source supported, we mock the backend to provide -# authentically formatted responses containing our test data.""" -# -# # example email text from Django docs: -# # https://docs.djangoproject.com/en/1.10/ref/unicode/ -# test_email_from = "Arnbjörg Ráðormsdóttir " -# test_email_subject = "My visit to Sør-Trøndelag" -# test_email_body = "Reporting some issue with the template tag: {% if helpdesk %}." -# test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + \ -# "\nSubject: " + test_email_subject + "\n\n" + test_email_body -# test_mail_len = len(test_email) -# -# if self.socks: -# from socks import ProxyConnectionError -# with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): -# call_command('get_email') -# -# else: -# # Test local email reading -# if self.method == 'local': -# with mock.patch('os.listdir') as mocked_listdir, \ -# mock.patch('helpdesk.email.isfile') as mocked_isfile, \ -# mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ -# mock.patch('os.unlink'): -# mocked_isfile.return_value = True -# mocked_listdir.return_value = ['filename1', 'filename2'] -# -# call_command('get_email') -# -# mocked_listdir.assert_called_with( -# '/var/lib/mail/helpdesk/') -# mocked_isfile.assert_any_call( -# '/var/lib/mail/helpdesk/filename1') -# mocked_isfile.assert_any_call( -# '/var/lib/mail/helpdesk/filename2') -# -# elif self.method == 'pop3': -# # mock poplib.POP3's list and retr methods to provide responses -# # as per RFC 1939 -# pop3_emails = { -# '1': ("+OK", test_email.split('\n')), -# '2': ("+OK", test_email.split('\n')), -# } -# pop3_mail_list = ("+OK 2 messages", ("1 %d" % -# test_mail_len, "2 %d" % test_mail_len)) -# mocked_poplib_server = mock.Mock() -# mocked_poplib_server.list = mock.Mock( -# return_value=pop3_mail_list) -# mocked_poplib_server.retr = mock.Mock( -# side_effect=lambda x: pop3_emails[x]) -# with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: -# mocked_poplib.POP3 = mock.Mock( -# return_value=mocked_poplib_server) -# call_command('get_email') -# -# elif self.method in ['imap', 'oauth']: -# # mock imaplib.IMAP4's search and fetch methods with responses -# # from RFC 3501 -# imap_emails = { -# "1": ("OK", (("1", test_email),)), -# "2": ("OK", (("2", test_email),)), -# } -# imap_mail_list = ("OK", ("1 2",)) -# mocked_imaplib_server = mock.Mock() -# mocked_imaplib_server.search = mock.Mock( -# return_value=imap_mail_list) -# -# # we ignore the second arg as the data item/mime-part is -# # constant (RFC822) -# mocked_imaplib_server.fetch = mock.Mock( -# side_effect=lambda x, _: imap_emails[x]) -# with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: -# mocked_imaplib.IMAP4 = mock.Mock( -# return_value=mocked_imaplib_server) -# call_command('get_email') -# -# ticket1 = get_object_or_404(Ticket, pk=1) -# self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) -# self.assertEqual(ticket1.title, test_email_subject) -# self.assertEqual(ticket1.description, test_email_body) -# -# ticket2 = get_object_or_404(Ticket, pk=2) -# self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) -# self.assertEqual(ticket2.title, test_email_subject) -# self.assertEqual(ticket2.description, test_email_body) -# -# def test_read_html_multipart_email(self): -# """Tests reading multipart MIME (HTML body and plain text alternative) -# emails from a queue and creating tickets. -# For each email source supported, we mock the backend to provide -# authentically formatted responses containing our test data.""" -# -# # example email text from Python docs: -# # https://docs.python.org/3/library/email-examples.html -# from email.mime.multipart import MIMEMultipart -# from email.mime.text import MIMEText -# -# me = "my@example.com" -# you = "your@example.com" -# # NOTE: CC'd emails need to be alphabetical and tested as such! -# # implementation uses sets, so only way to ensure tickets created -# # in right order is to change set to list and sort it -# cc_one = "nobody@example.com" -# cc_two = "other@example.com" -# cc = cc_one + ", " + cc_two -# subject = "Link" -# -# # Create message container - the correct MIME type is -# # multipart/alternative. -# msg = MIMEMultipart('alternative') -# msg['Subject'] = subject -# msg['From'] = me -# msg['To'] = you -# msg['Cc'] = cc -# -# # Create the body of the message (a plain-text and an HTML version). -# text = "Hi!\nHow are you?\nHere is the link you wanted:\nhttps://www.python.org" -# html = """\ -# -# -# -#

Hi!
-# How are you?
-# Here is the link you wanted. -#

-# -# -# """ -# -# # Record the MIME types of both parts - text/plain and text/html. -# part1 = MIMEText(text, 'plain') -# part2 = MIMEText(html, 'html') -# -# # Attach parts into message container. -# # According to RFC 2046, the last part of a multipart message, in this case -# # the HTML message, is best and preferred. -# msg.attach(part1) -# msg.attach(part2) -# -# test_mail_len = len(msg) -# -# if self.socks: -# from socks import ProxyConnectionError -# with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): -# call_command('get_email') -# -# else: -# # Test local email reading -# if self.method == 'local': -# with mock.patch('os.listdir') as mocked_listdir, \ -# mock.patch('helpdesk.email.isfile') as mocked_isfile, \ -# mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())), \ -# mock.patch('os.unlink'): -# mocked_isfile.return_value = True -# mocked_listdir.return_value = ['filename1', 'filename2'] -# -# call_command('get_email') -# -# mocked_listdir.assert_called_with( -# '/var/lib/mail/helpdesk/') -# mocked_isfile.assert_any_call( -# '/var/lib/mail/helpdesk/filename1') -# mocked_isfile.assert_any_call( -# '/var/lib/mail/helpdesk/filename2') -# -# elif self.method == 'pop3': -# # mock poplib.POP3's list and retr methods to provide responses -# # as per RFC 1939 -# pop3_emails = { -# '1': ("+OK", msg.as_string().split('\n')), -# '2': ("+OK", msg.as_string().split('\n')), -# } -# pop3_mail_list = ("+OK 2 messages", ("1 %d" % -# test_mail_len, "2 %d" % test_mail_len)) -# mocked_poplib_server = mock.Mock() -# mocked_poplib_server.list = mock.Mock( -# return_value=pop3_mail_list) -# mocked_poplib_server.retr = mock.Mock( -# side_effect=lambda x: pop3_emails[x]) -# with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: -# mocked_poplib.POP3 = mock.Mock( -# return_value=mocked_poplib_server) -# call_command('get_email') -# -# -# elif self.method in ['imap', 'oauth']: -# # mock imaplib.IMAP4's search and fetch methods with responses -# # from RFC 3501 -# imap_emails = { -# "1": ("OK", (("1", msg.as_string()),)), -# "2": ("OK", (("2", msg.as_string()),)), -# } -# imap_mail_list = ("OK", ("1 2",)) -# mocked_imaplib_server = mock.Mock() -# mocked_imaplib_server.search = mock.Mock( -# return_value=imap_mail_list) -# -# # we ignore the second arg as the data item/mime-part is -# # constant (RFC822) -# mocked_imaplib_server.fetch = mock.Mock( -# side_effect=lambda x, _: imap_emails[x]) -# with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: -# mocked_imaplib.IMAP4 = mock.Mock( -# return_value=mocked_imaplib_server) -# call_command('get_email') -# -# ticket1 = get_object_or_404(Ticket, pk=1) -# self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) -# self.assertEqual(ticket1.title, subject) -# # plain text should become description -# self.assertEqual(ticket1.description, text) -# # HTML MIME part should be attached to follow up -# followup1 = get_object_or_404(FollowUp, pk=1) -# self.assertEqual(followup1.ticket.id, 1) -# attach1 = get_object_or_404(FollowUpAttachment, pk=1) -# self.assertEqual(attach1.followup.id, 1) -# self.assertEqual(attach1.filename, 'email_html_body.html') -# cc0 = get_object_or_404(TicketCC, pk=1) -# self.assertEqual(cc0.email, you) -# cc1 = get_object_or_404(TicketCC, pk=2) -# self.assertEqual(cc1.email, cc_one) -# cc2 = get_object_or_404(TicketCC, pk=3) -# self.assertEqual(cc2.email, cc_two) -# self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 3) -# -# ticket2 = get_object_or_404(Ticket, pk=2) -# self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) -# self.assertEqual(ticket2.title, subject) -# # plain text should become description -# self.assertEqual(ticket2.description, text) -# # HTML MIME part should be attached to follow up -# followup2 = get_object_or_404(FollowUp, pk=2) -# self.assertEqual(followup2.ticket.id, 2) -# attach2 = get_object_or_404(FollowUpAttachment, pk=2) -# self.assertEqual(attach2.followup.id, 2) -# self.assertEqual(attach2.filename, 'email_html_body.html') -# -# def test_read_pgp_signed_email(self): -# """Tests reading a PGP signed email to ensure we handle base64 -# and PGP signatures appropriately.""" -# -# # example email text from #567 on GitHub -# with open(os.path.join(THIS_DIR, "test_files/pgp.eml"), encoding="utf-8") as fd: -# test_email = fd.read() -# test_mail_len = len(test_email) -# -# if self.socks: -# from socks import ProxyConnectionError -# with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): -# call_command('get_email') -# -# else: -# # Test local email reading -# if self.method == 'local': -# with mock.patch('os.listdir') as mocked_listdir, \ -# mock.patch('helpdesk.email.isfile') as mocked_isfile, \ -# mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ -# mock.patch('os.unlink'): -# mocked_isfile.return_value = True -# mocked_listdir.return_value = ['filename1'] -# -# call_command('get_email') -# -# mocked_listdir.assert_called_with( -# '/var/lib/mail/helpdesk/') -# mocked_isfile.assert_any_call( -# '/var/lib/mail/helpdesk/filename1') -# -# elif self.method == 'pop3': -# # mock poplib.POP3's list and retr methods to provide responses -# # as per RFC 1939 -# pop3_emails = { -# '1': ("+OK", test_email.split('\n')), -# } -# pop3_mail_list = ("+OK 1 message", ("1 %d" % test_mail_len)) -# mocked_poplib_server = mock.Mock() -# mocked_poplib_server.list = mock.Mock( -# return_value=pop3_mail_list) -# mocked_poplib_server.retr = mock.Mock( -# side_effect=lambda x: pop3_emails['1']) -# with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: -# mocked_poplib.POP3 = mock.Mock( -# return_value=mocked_poplib_server) -# call_command('get_email') -# -# -# elif self.method in ['imap', 'oauth']: -# # mock imaplib.IMAP4's search and fetch methods with responses -# # from RFC 3501 -# imap_emails = { -# "1": ("OK", (("1", test_email),)), -# } -# imap_mail_list = ("OK", ("1",)) -# mocked_imaplib_server = mock.Mock() -# mocked_imaplib_server.search = mock.Mock( -# return_value=imap_mail_list) -# -# # we ignore the second arg as the data item/mime-part is -# # constant (RFC822) -# mocked_imaplib_server.fetch = mock.Mock( -# side_effect=lambda x, _: imap_emails[x]) -# with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: -# mocked_imaplib.IMAP4 = mock.Mock( -# return_value=mocked_imaplib_server) -# call_command('get_email') -# -# ticket1 = get_object_or_404(Ticket, pk=1) -# self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) -# self.assertEqual( -# ticket1.title, "example email that crashes django-helpdesk get_email") -# self.assertEqual( -# ticket1.description, """hi, thanks for looking into this :)\n\nhttps://github.com/django-helpdesk/django-helpdesk/issues/567#issuecomment-342954233""") -# # MIME part should be attached to follow up -# followup1 = get_object_or_404(FollowUp, pk=1) -# self.assertEqual(followup1.ticket.id, 1) -# attach1 = get_object_or_404(FollowUpAttachment, pk=1) -# self.assertEqual(attach1.followup.id, 1) -# self.assertEqual(attach1.filename, 'part-1_signature.asc') -# self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE----- -# -# iQIcBAEBCAAGBQJaA3dnAAoJELBLc7QPITnLN54P/3Zsu7+AIQWDFTvziJfCqswG -# u99fG+iWa6ER+iuZG0YU1BdIxIjSKt1pvqB0yXITlT9FCdf1zc0pmeJ08I0a5pVa -# iaym5prVUro5BNQ6Vqoo0jvOCKNrACtFNv85zDzXbPNP8TrUss41U+ackPHkOHov -# cmJ5YZFQebYXXpibFSIDimVGfwI57vyTWvolttZFLSI1mgGX7MvHaKh253QLdXIo -# EUih40rOw3f/nYPEKyW8QA72ImBsZdcZI5buiiCC1bgMkKSFSNAFiIanYEpGNMnO -# 3zYKBpbpBhnWSi5orwx47/v4/Yb/qVr5ppuV23+YoMfEGT8cHPTAdYpnpE27ByAv -# jvpxKEwmkUzD1WxOmQdCcPJPyWz1OBUVvjj0nn0Espnz8V8esl9+IFs739lpFBHu -# fWWA315LTmIJMGH5Ujf4myiQeXDo6Gsy6WhE13q7MKTq3tnyi5dJG9GJCBf646dL -# RwcDf9O7MvKSV2kSPmryLnUF7D+2fva+Cy+CvJDVJCo5zr4ucXPXZ4htpI6Pjpd5 -# oPHvbqxSCMJrQ7eAFTYmBNGauSyr0XvGM1qmHBZD/laQEJHYgLT2ILrymZhVDHtK -# W7tXhGjMoUvqAxiKkmG3UHFqN4k3EYo13PwoOWyJHD1M9ArbX/Sk9l8DDguCh3DW -# a9eiiQ+3V1v+7wWHXCzq -# =6JeP -# -----END PGP SIGNATURE----- -# """) -# # should this be 'application/pgp-signature'? -# # self.assertEqual(attach1.mime_type, 'text/plain') -# + + def test_commas_in_mail_headers(self): + """Tests correctly decoding mail headers when a comma is encoded into + UTF-8. See bug report #832.""" + + # Create the from using standard RFC required formats + # Override the last_name to ensure we get a non-ascii character in it + test_email_from_meta = utils.generate_email_address("fr_FR", last_name_override="Bouissières") + test_email_subject = "Commas in From lines" + test_email_body = "Testing commas in from email UTF-8." + test_email = "To: helpdesk@example.com\nFrom: " + test_email_from_meta[0] + \ + "\nSubject: " + test_email_subject + "\n\n" + test_email_body + test_mail_len = len(test_email) + + if self.socks: + from socks import ProxyConnectionError + with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): + call_command('get_email') + + else: + # Test local email reading + if self.method == 'local': + with mock.patch('os.listdir') as mocked_listdir, \ + mock.patch('helpdesk.email.isfile') as mocked_isfile, \ + mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ + mock.patch('os.unlink'): + mocked_isfile.return_value = True + mocked_listdir.return_value = ['filename1', 'filename2'] + + call_command('get_email') + + mocked_listdir.assert_called_with( + '/var/lib/mail/helpdesk/') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename1') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename2') + + elif self.method == 'pop3': + # mock poplib.POP3's list and retr methods to provide responses + # as per RFC 1939 + pop3_emails = { + '1': ("+OK", test_email.split('\n')), + '2': ("+OK", test_email.split('\n')), + } + pop3_mail_list = ("+OK 2 messages", ("1 %d" % + test_mail_len, "2 %d" % test_mail_len)) + mocked_poplib_server = mock.Mock() + mocked_poplib_server.list = mock.Mock( + return_value=pop3_mail_list) + mocked_poplib_server.retr = mock.Mock( + side_effect=lambda x: pop3_emails[x]) + with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: + mocked_poplib.POP3 = mock.Mock( + return_value=mocked_poplib_server) + call_command('get_email') + + elif self.method == 'imap': + # mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 + imap_emails = { + "1": ("OK", (("1", test_email),)), + "2": ("OK", (("2", test_email),)), + } + imap_mail_list = ("OK", ("1 2",)) + mocked_imaplib_server = mock.Mock() + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) + + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) + with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) + call_command('get_email') + + elif self.method == 'oauth': + # mock the oauthlib session and requests oauth backendclient + # then mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 + imap_emails = { + "1": ("OK", (("1", test_email),)), + "2": ("OK", (("2", test_email),)), + } + imap_mail_list = ("OK", ("1 2",)) + mocked_imaplib_server = mock.Mock() + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) + + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) + + mocked_oauth_backend_client = mock.Mock() + with mock.patch('helpdesk.email.oauth2lib', autospec=True) as mocked_oauth2lib: + mocked_oauth2lib.BackendApplicationClient = mock.Mock( + return_value=mocked_oauth_backend_client) + + mocked_oauth_session = mock.Mock() + mocked_oauth_session.fetch_token = mock.Mock( + return_value={} + ) + + with mock.patch('helpdesk.email.requests_oauthlib', autospec=True) as mocked_requests_oauthlib: + mocked_requests_oauthlib.OAuth2Session = mock.Mock( + return_value=mocked_oauth_session) + + with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) + + call_command('get_email') + + ticket1 = get_object_or_404(Ticket, pk=1) + self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) + self.assertEqual(ticket1.submitter_email, test_email_from_meta[1]) + self.assertEqual(ticket1.title, test_email_subject) + self.assertEqual(ticket1.description, test_email_body) + + ticket2 = get_object_or_404(Ticket, pk=2) + self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) + self.assertEqual(ticket2.submitter_email, test_email_from_meta[1]) + self.assertEqual(ticket2.title, test_email_subject) + self.assertEqual(ticket2.description, test_email_body) + + def test_read_email_with_template_tag(self): + """Tests reading plain text emails from a queue and creating tickets, + except this time the email body contains a Django template tag. + For each email source supported, we mock the backend to provide + authentically formatted responses containing our test data.""" + + # example email text from Django docs: + # https://docs.djangoproject.com/en/1.10/ref/unicode/ + test_email_from = "Arnbjörg Ráðormsdóttir " + test_email_subject = "My visit to Sør-Trøndelag" + test_email_body = "Reporting some issue with the template tag: {% if helpdesk %}." + test_email = "To: helpdesk@example.com\nFrom: " + test_email_from + \ + "\nSubject: " + test_email_subject + "\n\n" + test_email_body + test_mail_len = len(test_email) + + if self.socks: + from socks import ProxyConnectionError + with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): + call_command('get_email') + + else: + # Test local email reading + if self.method == 'local': + with mock.patch('os.listdir') as mocked_listdir, \ + mock.patch('helpdesk.email.isfile') as mocked_isfile, \ + mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ + mock.patch('os.unlink'): + mocked_isfile.return_value = True + mocked_listdir.return_value = ['filename1', 'filename2'] + + call_command('get_email') + + mocked_listdir.assert_called_with( + '/var/lib/mail/helpdesk/') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename1') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename2') + + elif self.method == 'pop3': + # mock poplib.POP3's list and retr methods to provide responses + # as per RFC 1939 + pop3_emails = { + '1': ("+OK", test_email.split('\n')), + '2': ("+OK", test_email.split('\n')), + } + pop3_mail_list = ("+OK 2 messages", ("1 %d" % + test_mail_len, "2 %d" % test_mail_len)) + mocked_poplib_server = mock.Mock() + mocked_poplib_server.list = mock.Mock( + return_value=pop3_mail_list) + mocked_poplib_server.retr = mock.Mock( + side_effect=lambda x: pop3_emails[x]) + with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: + mocked_poplib.POP3 = mock.Mock( + return_value=mocked_poplib_server) + call_command('get_email') + + elif self.method == 'imap': + # mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 + imap_emails = { + "1": ("OK", (("1", test_email),)), + "2": ("OK", (("2", test_email),)), + } + imap_mail_list = ("OK", ("1 2",)) + mocked_imaplib_server = mock.Mock() + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) + + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) + with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) + call_command('get_email') + + elif self.method == 'oauth': + # mock the oauthlib session and requests oauth backendclient + # then mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 + imap_emails = { + "1": ("OK", (("1", test_email),)), + "2": ("OK", (("2", test_email),)), + } + imap_mail_list = ("OK", ("1 2",)) + mocked_imaplib_server = mock.Mock() + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) + + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) + + mocked_oauth_backend_client = mock.Mock() + with mock.patch('helpdesk.email.oauth2lib', autospec=True) as mocked_oauth2lib: + mocked_oauth2lib.BackendApplicationClient = mock.Mock( + return_value=mocked_oauth_backend_client) + + mocked_oauth_session = mock.Mock() + mocked_oauth_session.fetch_token = mock.Mock( + return_value={} + ) + + with mock.patch('helpdesk.email.requests_oauthlib', autospec=True) as mocked_requests_oauthlib: + mocked_requests_oauthlib.OAuth2Session = mock.Mock( + return_value=mocked_oauth_session) + + with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) + + call_command('get_email') + + ticket1 = get_object_or_404(Ticket, pk=1) + self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) + self.assertEqual(ticket1.title, test_email_subject) + self.assertEqual(ticket1.description, test_email_body) + + ticket2 = get_object_or_404(Ticket, pk=2) + self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) + self.assertEqual(ticket2.title, test_email_subject) + self.assertEqual(ticket2.description, test_email_body) + + def test_read_html_multipart_email(self): + """Tests reading multipart MIME (HTML body and plain text alternative) + emails from a queue and creating tickets. + For each email source supported, we mock the backend to provide + authentically formatted responses containing our test data.""" + + # example email text from Python docs: + # https://docs.python.org/3/library/email-examples.html + from email.mime.multipart import MIMEMultipart + from email.mime.text import MIMEText + + me = "my@example.com" + you = "your@example.com" + # NOTE: CC'd emails need to be alphabetical and tested as such! + # implementation uses sets, so only way to ensure tickets created + # in right order is to change set to list and sort it + cc_one = "nobody@example.com" + cc_two = "other@example.com" + cc = cc_one + ", " + cc_two + subject = "Link" + + # Create message container - the correct MIME type is + # multipart/alternative. + msg = MIMEMultipart('alternative') + msg['Subject'] = subject + msg['From'] = me + msg['To'] = you + msg['Cc'] = cc + + # Create the body of the message (a plain-text and an HTML version). + text = "Hi!\nHow are you?\nHere is the link you wanted:\nhttps://www.python.org" + html = """\ + + + +

Hi!
+ How are you?
+ Here is the link you wanted. +

+ + + """ + + # Record the MIME types of both parts - text/plain and text/html. + part1 = MIMEText(text, 'plain') + part2 = MIMEText(html, 'html') + + # Attach parts into message container. + # According to RFC 2046, the last part of a multipart message, in this case + # the HTML message, is best and preferred. + msg.attach(part1) + msg.attach(part2) + + test_mail_len = len(msg) + + if self.socks: + from socks import ProxyConnectionError + with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): + call_command('get_email') + + else: + # Test local email reading + if self.method == 'local': + with mock.patch('os.listdir') as mocked_listdir, \ + mock.patch('helpdesk.email.isfile') as mocked_isfile, \ + mock.patch('builtins.open', mock.mock_open(read_data=msg.as_string())), \ + mock.patch('os.unlink'): + mocked_isfile.return_value = True + mocked_listdir.return_value = ['filename1', 'filename2'] + + call_command('get_email') + + mocked_listdir.assert_called_with( + '/var/lib/mail/helpdesk/') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename1') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename2') + + elif self.method == 'pop3': + # mock poplib.POP3's list and retr methods to provide responses + # as per RFC 1939 + pop3_emails = { + '1': ("+OK", msg.as_string().split('\n')), + '2': ("+OK", msg.as_string().split('\n')), + } + pop3_mail_list = ("+OK 2 messages", ("1 %d" % + test_mail_len, "2 %d" % test_mail_len)) + mocked_poplib_server = mock.Mock() + mocked_poplib_server.list = mock.Mock( + return_value=pop3_mail_list) + mocked_poplib_server.retr = mock.Mock( + side_effect=lambda x: pop3_emails[x]) + with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: + mocked_poplib.POP3 = mock.Mock( + return_value=mocked_poplib_server) + call_command('get_email') + + + elif self.method == 'imap': + # mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 + imap_emails = { + "1": ("OK", (("1", msg.as_string()),)), + "2": ("OK", (("2", msg.as_string()),)), + } + imap_mail_list = ("OK", ("1 2",)) + mocked_imaplib_server = mock.Mock() + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) + + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) + with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) + call_command('get_email') + + elif self.method == 'oauth': + # mock the oauthlib session and requests oauth backendclient + # then mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 + imap_emails = { + "1": ("OK", (("1", msg.as_string()),)), + "2": ("OK", (("2", msg.as_string()),)), + } + imap_mail_list = ("OK", ("1 2",)) + mocked_imaplib_server = mock.Mock() + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) + + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) + + mocked_oauth_backend_client = mock.Mock() + with mock.patch('helpdesk.email.oauth2lib', autospec=True) as mocked_oauth2lib: + mocked_oauth2lib.BackendApplicationClient = mock.Mock( + return_value=mocked_oauth_backend_client) + + mocked_oauth_session = mock.Mock() + mocked_oauth_session.fetch_token = mock.Mock( + return_value={} + ) + + with mock.patch('helpdesk.email.requests_oauthlib', autospec=True) as mocked_requests_oauthlib: + mocked_requests_oauthlib.OAuth2Session = mock.Mock( + return_value=mocked_oauth_session) + + with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) + + call_command('get_email') + + ticket1 = get_object_or_404(Ticket, pk=1) + self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) + self.assertEqual(ticket1.title, subject) + # plain text should become description + self.assertEqual(ticket1.description, text) + # HTML MIME part should be attached to follow up + followup1 = get_object_or_404(FollowUp, pk=1) + self.assertEqual(followup1.ticket.id, 1) + attach1 = get_object_or_404(FollowUpAttachment, pk=1) + self.assertEqual(attach1.followup.id, 1) + self.assertEqual(attach1.filename, 'email_html_body.html') + cc0 = get_object_or_404(TicketCC, pk=1) + self.assertEqual(cc0.email, you) + cc1 = get_object_or_404(TicketCC, pk=2) + self.assertEqual(cc1.email, cc_one) + cc2 = get_object_or_404(TicketCC, pk=3) + self.assertEqual(cc2.email, cc_two) + self.assertEqual(len(TicketCC.objects.filter(ticket=1)), 3) + + ticket2 = get_object_or_404(Ticket, pk=2) + self.assertEqual(ticket2.ticket_for_url, "QQ-%s" % ticket2.id) + self.assertEqual(ticket2.title, subject) + # plain text should become description + self.assertEqual(ticket2.description, text) + # HTML MIME part should be attached to follow up + followup2 = get_object_or_404(FollowUp, pk=2) + self.assertEqual(followup2.ticket.id, 2) + attach2 = get_object_or_404(FollowUpAttachment, pk=2) + self.assertEqual(attach2.followup.id, 2) + self.assertEqual(attach2.filename, 'email_html_body.html') + + def test_read_pgp_signed_email(self): + """Tests reading a PGP signed email to ensure we handle base64 + and PGP signatures appropriately.""" + + # example email text from #567 on GitHub + with open(os.path.join(THIS_DIR, "test_files/pgp.eml"), encoding="utf-8") as fd: + test_email = fd.read() + test_mail_len = len(test_email) + + if self.socks: + from socks import ProxyConnectionError + with self.assertRaisesRegex(ProxyConnectionError, '%s:%s' % (unrouted_socks_server, unused_port)): + call_command('get_email') + + else: + # Test local email reading + if self.method == 'local': + with mock.patch('os.listdir') as mocked_listdir, \ + mock.patch('helpdesk.email.isfile') as mocked_isfile, \ + mock.patch('builtins.open', mock.mock_open(read_data=test_email)), \ + mock.patch('os.unlink'): + mocked_isfile.return_value = True + mocked_listdir.return_value = ['filename1'] + + call_command('get_email') + + mocked_listdir.assert_called_with( + '/var/lib/mail/helpdesk/') + mocked_isfile.assert_any_call( + '/var/lib/mail/helpdesk/filename1') + + elif self.method == 'pop3': + # mock poplib.POP3's list and retr methods to provide responses + # as per RFC 1939 + pop3_emails = { + '1': ("+OK", test_email.split('\n')), + } + pop3_mail_list = ("+OK 1 message", ("1 %d" % test_mail_len)) + mocked_poplib_server = mock.Mock() + mocked_poplib_server.list = mock.Mock( + return_value=pop3_mail_list) + mocked_poplib_server.retr = mock.Mock( + side_effect=lambda x: pop3_emails['1']) + with mock.patch('helpdesk.email.poplib', autospec=True) as mocked_poplib: + mocked_poplib.POP3 = mock.Mock( + return_value=mocked_poplib_server) + call_command('get_email') + + + elif self.method == 'imap': + # mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 + imap_emails = { + "1": ("OK", (("1", test_email),)), + } + imap_mail_list = ("OK", ("1",)) + mocked_imaplib_server = mock.Mock() + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) + + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) + with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) + call_command('get_email') + + elif self.method == 'oauth': + # mock the oauthlib session and requests oauth backendclient + # then mock imaplib.IMAP4's search and fetch methods with responses + # from RFC 3501 + imap_emails = { + "1": ("OK", (("1", test_email),)), + } + imap_mail_list = ("OK", ("1",)) + mocked_imaplib_server = mock.Mock() + mocked_imaplib_server.search = mock.Mock( + return_value=imap_mail_list) + + # we ignore the second arg as the data item/mime-part is + # constant (RFC822) + mocked_imaplib_server.fetch = mock.Mock( + side_effect=lambda x, _: imap_emails[x]) + + mocked_oauth_backend_client = mock.Mock() + with mock.patch('helpdesk.email.oauth2lib', autospec=True) as mocked_oauth2lib: + mocked_oauth2lib.BackendApplicationClient = mock.Mock( + return_value=mocked_oauth_backend_client) + + mocked_oauth_session = mock.Mock() + mocked_oauth_session.fetch_token = mock.Mock( + return_value={} + ) + + with mock.patch('helpdesk.email.requests_oauthlib', autospec=True) as mocked_requests_oauthlib: + mocked_requests_oauthlib.OAuth2Session = mock.Mock( + return_value=mocked_oauth_session) + + with mock.patch('helpdesk.email.imaplib', autospec=True) as mocked_imaplib: + mocked_imaplib.IMAP4 = mock.Mock( + return_value=mocked_imaplib_server) + + call_command('get_email') + + ticket1 = get_object_or_404(Ticket, pk=1) + self.assertEqual(ticket1.ticket_for_url, "QQ-%s" % ticket1.id) + self.assertEqual( + ticket1.title, "example email that crashes django-helpdesk get_email") + self.assertEqual( + ticket1.description, """hi, thanks for looking into this :)\n\nhttps://github.com/django-helpdesk/django-helpdesk/issues/567#issuecomment-342954233""") + # MIME part should be attached to follow up + followup1 = get_object_or_404(FollowUp, pk=1) + self.assertEqual(followup1.ticket.id, 1) + attach1 = get_object_or_404(FollowUpAttachment, pk=1) + self.assertEqual(attach1.followup.id, 1) + self.assertEqual(attach1.filename, 'part-1_signature.asc') + self.assertEqual(attach1.file.read(), b"""-----BEGIN PGP SIGNATURE----- + +iQIcBAEBCAAGBQJaA3dnAAoJELBLc7QPITnLN54P/3Zsu7+AIQWDFTvziJfCqswG +u99fG+iWa6ER+iuZG0YU1BdIxIjSKt1pvqB0yXITlT9FCdf1zc0pmeJ08I0a5pVa +iaym5prVUro5BNQ6Vqoo0jvOCKNrACtFNv85zDzXbPNP8TrUss41U+ackPHkOHov +cmJ5YZFQebYXXpibFSIDimVGfwI57vyTWvolttZFLSI1mgGX7MvHaKh253QLdXIo +EUih40rOw3f/nYPEKyW8QA72ImBsZdcZI5buiiCC1bgMkKSFSNAFiIanYEpGNMnO +3zYKBpbpBhnWSi5orwx47/v4/Yb/qVr5ppuV23+YoMfEGT8cHPTAdYpnpE27ByAv +jvpxKEwmkUzD1WxOmQdCcPJPyWz1OBUVvjj0nn0Espnz8V8esl9+IFs739lpFBHu +fWWA315LTmIJMGH5Ujf4myiQeXDo6Gsy6WhE13q7MKTq3tnyi5dJG9GJCBf646dL +RwcDf9O7MvKSV2kSPmryLnUF7D+2fva+Cy+CvJDVJCo5zr4ucXPXZ4htpI6Pjpd5 +oPHvbqxSCMJrQ7eAFTYmBNGauSyr0XvGM1qmHBZD/laQEJHYgLT2ILrymZhVDHtK +W7tXhGjMoUvqAxiKkmG3UHFqN4k3EYo13PwoOWyJHD1M9ArbX/Sk9l8DDguCh3DW +a9eiiQ+3V1v+7wWHXCzq +=6JeP +-----END PGP SIGNATURE----- +""") + # should this be 'application/pgp-signature'? + # self.assertEqual(attach1.mime_type, 'text/plain') + + class GetEmailCCHandling(TestCase): """TestCase that checks CC handling in email. Needs its own test harness.""" diff --git a/quicktest.py b/quicktest.py index e387be6e..a710a41f 100755 --- a/quicktest.py +++ b/quicktest.py @@ -109,7 +109,9 @@ class QuickDjangoTest: HELPDESK_TEAMS_MIGRATION_DEPENDENCIES=[], HELPDESK_KBITEM_TEAM_GETTER=lambda _: None, # test the API - HELPDESK_ACTIVATE_API_ENDPOINT=True + HELPDESK_ACTIVATE_API_ENDPOINT=True, + # Set IMAP Server Debug Verbosity + HELPDESK_IMAP_DEBUG_LEVEL=int(os.environ.get("HELPDESK_IMAP_DEBUG_LEVEL", "0")), ) from django.test.runner import DiscoverRunner