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