From cefbcdebd23ce91d6e8d400d49c2d5391a4f0f68 Mon Sep 17 00:00:00 2001
From: Pietro Mingo <50447537+skar395@users.noreply.github.com>
Date: Mon, 16 Jan 2023 22:23:38 +0100
Subject: [PATCH 1/2] Office365 API mailbox support (#251)
* Update models.py, __init__.py, and office365.py
* Update setup.py
* Office365Transport properties
* Update .gitignore
* wrong folder stage
* Update models.py
* Update office365.py and models.py
* office365 implementation
* Update office365.py
* clear code
* fix import
* Update readme.rst
* archive and delete
* fix and documentation
* removed build folder
* missing archive
Co-authored-by: Pietro Mingo
Co-authored-by: Serafim Bordei
---
.gitignore | 1 +
django_mailbox/models.py | 34 ++++++++++++-
django_mailbox/transports/__init__.py | 1 +
django_mailbox/transports/base.py | 1 +
django_mailbox/transports/office365.py | 66 ++++++++++++++++++++++++++
docs/topics/mailbox_types.rst | 20 ++++++++
readme.rst | 2 +-
setup.py | 7 ++-
8 files changed, 129 insertions(+), 3 deletions(-)
create mode 100644 django_mailbox/transports/office365.py
diff --git a/.gitignore b/.gitignore
index 804fef4..94fab77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ dummy_project/*
.tox/
messages
*.sqlite3
+.idea/
diff --git a/django_mailbox/models.py b/django_mailbox/models.py
index 0f0ac9d..42296e9 100644
--- a/django_mailbox/models.py
+++ b/django_mailbox/models.py
@@ -31,7 +31,7 @@ from django_mailbox import utils
from django_mailbox.signals import message_received
from django_mailbox.transports import Pop3Transport, ImapTransport, \
MaildirTransport, MboxTransport, BabylTransport, MHTransport, \
- MMDFTransport, GmailImapTransport
+ MMDFTransport, GmailImapTransport, Office365Transport
logger = logging.getLogger(__name__)
@@ -189,6 +189,30 @@ class Mailbox(models.Model):
return None
return folder[0]
+ @property
+ def client_id(self):
+ """Returns (if specified) the client id for Office365."""
+ client_id = self._query_string.get('client_id', None)
+ if not client_id:
+ return None
+ return client_id[0]
+
+ @property
+ def client_secret(self):
+ """Returns (if specified) the client secret for Office365."""
+ client_secret = self._query_string.get('client_secret', None)
+ if not client_secret:
+ return None
+ return client_secret[0]
+
+ @property
+ def tenant_id(self):
+ """Returns (if specified) the tenant id for Office365."""
+ tenant_id = self._query_string.get('tenant_id', None)
+ if not tenant_id:
+ return None
+ return tenant_id[0]
+
def get_connection(self):
"""Returns the transport instance for this mailbox.
@@ -223,6 +247,14 @@ class Mailbox(models.Model):
ssl=self.use_ssl
)
conn.connect(self.username, self.password)
+ elif self.type == 'office365':
+ conn = Office365Transport(
+ self.location,
+ self.username,
+ folder=self.folder,
+ archive=self.archive
+ )
+ conn.connect(self.client_id, self.client_secret, self.tenant_id)
elif self.type == 'maildir':
conn = MaildirTransport(self.location)
elif self.type == 'mbox':
diff --git a/django_mailbox/transports/__init__.py b/django_mailbox/transports/__init__.py
index 7aeef95..9f733e3 100644
--- a/django_mailbox/transports/__init__.py
+++ b/django_mailbox/transports/__init__.py
@@ -8,3 +8,4 @@ from django_mailbox.transports.babyl import BabylTransport
from django_mailbox.transports.mh import MHTransport
from django_mailbox.transports.mmdf import MMDFTransport
from django_mailbox.transports.gmail import GmailImapTransport
+from django_mailbox.transports.office365 import Office365Transport
diff --git a/django_mailbox/transports/base.py b/django_mailbox/transports/base.py
index a90af9f..a3a2bc0 100644
--- a/django_mailbox/transports/base.py
+++ b/django_mailbox/transports/base.py
@@ -9,3 +9,4 @@ class EmailTransport:
message = email.message_from_bytes(contents)
return message
+
diff --git a/django_mailbox/transports/office365.py b/django_mailbox/transports/office365.py
new file mode 100644
index 0000000..4f748b2
--- /dev/null
+++ b/django_mailbox/transports/office365.py
@@ -0,0 +1,66 @@
+import logging
+
+from django.conf import settings
+
+from .base import EmailTransport, MessageParseError
+
+logger = logging.getLogger(__name__)
+
+
+class Office365Transport(EmailTransport):
+ def __init__(
+ self, hostname, username, archive='', folder=None
+ ):
+ self.integration_testing_subject = getattr(
+ settings,
+ 'DJANGO_MAILBOX_INTEGRATION_TESTING_SUBJECT',
+ None
+ )
+ self.hostname = hostname
+ self.username = username
+ self.archive = archive
+ self.folder = folder
+
+ def connect(self, client_id, client_secret, tenant_id):
+ try:
+ import O365
+ except ImportError:
+ raise ValueError(
+ "Install o365 to use oauth2 auth for office365"
+ )
+
+ credentials = (client_id, client_secret)
+
+ self.account = O365.Account(credentials, auth_flow_type='credentials', tenant_id=tenant_id)
+ self.account.authenticate()
+
+ self.mailbox = self.account.mailbox(resource=self.username)
+ self.mailbox_folder = self.mailbox.inbox_folder()
+ if self.folder:
+ self.mailbox_folder = self.mailbox.get_folder(folder_name=self.folder)
+
+ def get_message(self, condition=None):
+ archive_folder = None
+ if self.archive:
+ archive_folder = self.mailbox.get_folder(folder_name=self.archive)
+ if not archive_folder:
+ archive_folder = self.mailbox.create_child_folder(self.archive)
+
+ for o365message in self.mailbox_folder.get_messages(order_by='receivedDateTime'):
+ try:
+ mime_content = o365message.get_mime_content()
+ message = self.get_email_from_bytes(mime_content)
+
+ if condition and not condition(message):
+ continue
+
+ yield message
+ except MessageParseError:
+ continue
+
+ if self.archive and archive_folder:
+ o365message.copy(archive_folder)
+
+ o365message.delete()
+ return
+
diff --git a/docs/topics/mailbox_types.rst b/docs/topics/mailbox_types.rst
index ad8c9ff..2d5a457 100644
--- a/docs/topics/mailbox_types.rst
+++ b/docs/topics/mailbox_types.rst
@@ -13,6 +13,7 @@ POP3 and IMAP as well as local file-based mailboxes.
POP3 ``pop3://`` Can also specify SSL with ``pop3+ssl://``
IMAP ``imap://`` Can also specify SSL with ``imap+ssl://`` or STARTTLS with ``imap+tls``; additional configuration is also possible: see :ref:`pop3-and-imap-mailboxes` for details.
Gmail IMAP ``gmail+ssl://`` Uses OAuth authentication for Gmail's IMAP transport. See :ref:`gmail-oauth` for details.
+ Office365 API``office365://`` Uses OAuth authentication for Office365 API transport. See :ref:`office365-oauth` for details.
Maildir ``maildir://``
Mbox ``mbox://``
Babyl ``babyl://``
@@ -115,6 +116,25 @@ Build your URI accordingly::
gmail+ssl://youremailaddress%40gmail.com:oauth2@imap.gmail.com?archive=Archived
+.. _office365-oauth:
+Office 365 API
+-------------------------------------
+
+Office 365 allows through the API to read a mailbox with Oauth.
+The O365_ library is used.
+
+.. _O365: https://github.com/O365/python-o365
+.. _configuration: https://github.com/O365/python-o365#authentication
+
+For the configuration_ you need to register an application and get a client_id, client_secret and tenant_id.
+
+This implementation uses the client credentials grant flow and the password you specify will be ignored.
+
+Build your URI accordingly::
+
+ office365://youremailaddress%40yourdomain.com:oauth2@outlook.office365.com?client_id=client_id&client_secret=client_secret&tenant_id=tenant_id&archive=Archived
+
+
Local File-based Mailboxes
--------------------------
diff --git a/readme.rst b/readme.rst
index 0aac06b..f03459e 100644
--- a/readme.rst
+++ b/readme.rst
@@ -8,7 +8,7 @@
:target: https://pypi.python.org/pypi/django-mailbox
-Easily ingest messages from POP3, IMAP, or local mailboxes into your Django application.
+Easily ingest messages from POP3, IMAP, Office365 API or local mailboxes into your Django application.
This app allows you to either ingest e-mail content from common e-mail services (as long as the service provides POP3 or IMAP support),
or directly receive e-mail messages from ``stdin`` (for locally processing messages from Postfix or Exim4).
diff --git a/setup.py b/setup.py
index 2460dc0..a6ade7d 100755
--- a/setup.py
+++ b/setup.py
@@ -13,6 +13,10 @@ gmail_oauth2_require = [
'python-social-auth',
]
+office365_oauth2_require = [
+ 'O365',
+]
+
setup(
name='django-mailbox',
version=version_string,
@@ -25,7 +29,8 @@ setup(
author='Adam Coddington',
author_email='me@adamcoddington.net',
extras_require={
- 'gmail-oauth2': gmail_oauth2_require
+ 'gmail-oauth2': gmail_oauth2_require,
+ 'office365-oauth2': office365_oauth2_require
},
python_requires=">=3",
classifiers=[
From 462fdd3e496e28cc7c9d99a4f18ce80cb8b6aba6 Mon Sep 17 00:00:00 2001
From: Jace Manshadi
Date: Tue, 24 Jan 2023 06:08:47 -0800
Subject: [PATCH 2/2] updating location of logic for getmail command (#264)
* moving logic for processing all the new emails from all mailboxes to a method under Mailbox so it can be called from other modules
* moving logger lines into handle method as requested
---
django_mailbox/management/commands/getmail.py | 23 ++-----------------
django_mailbox/models.py | 21 +++++++++++++++++
2 files changed, 23 insertions(+), 21 deletions(-)
diff --git a/django_mailbox/management/commands/getmail.py b/django_mailbox/management/commands/getmail.py
index 31b2dbd..f657509 100644
--- a/django_mailbox/management/commands/getmail.py
+++ b/django_mailbox/management/commands/getmail.py
@@ -5,26 +5,7 @@ from django.core.management.base import BaseCommand
from django_mailbox.models import Mailbox
-logger = logging.getLogger(__name__)
-logging.basicConfig(level=logging.INFO)
-
-
class Command(BaseCommand):
def handle(self, *args, **options):
- mailboxes = Mailbox.active_mailboxes.all()
- if args:
- mailboxes = mailboxes.filter(
- name=' '.join(args)
- )
- for mailbox in mailboxes:
- logger.info(
- 'Gathering messages for %s',
- mailbox.name
- )
- messages = mailbox.get_new_mail()
- for message in messages:
- logger.info(
- 'Received %s (from %s)',
- message.subject,
- message.from_address
- )
+ logging.basicConfig(level=logging.INFO)
+ Mailbox.get_new_mail_all_mailboxes(args)
diff --git a/django_mailbox/models.py b/django_mailbox/models.py
index 42296e9..871b0cc 100644
--- a/django_mailbox/models.py
+++ b/django_mailbox/models.py
@@ -462,6 +462,27 @@ class Mailbox(models.Model):
else:
self.save()
+ @staticmethod
+ def get_new_mail_all_mailboxes(args=None):
+ mailboxes = Mailbox.active_mailboxes.all()
+ if args:
+ mailboxes = mailboxes.filter(
+ name=' '.join(args)
+ )
+ for mailbox in mailboxes:
+ logger.info(
+ 'Gathering messages for %s',
+ mailbox.name
+ )
+ messages = mailbox.get_new_mail()
+ for message in messages:
+ logger.info(
+ 'Received %s (from %s)',
+ message.subject,
+ message.from_address
+ )
+
+
def __str__(self):
return self.name