Feature: OAuth2 Gmail and Outlook email support (#7866)

This commit is contained in:
shamoon
2024-10-10 13:57:32 -07:00
committed by GitHub
parent dcc8d4046a
commit 2353f7c2db
30 changed files with 1110 additions and 121 deletions

View File

@@ -18,6 +18,7 @@ from celery import shared_task
from celery.canvas import Signature
from django.conf import settings
from django.db import DatabaseError
from django.utils import timezone
from django.utils.timezone import is_naive
from django.utils.timezone import make_aware
from imap_tools import AND
@@ -42,6 +43,7 @@ from documents.tasks import consume_file
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
from paperless_mail.models import ProcessedMail
from paperless_mail.oauth import PaperlessMailOAuth2Manager
from paperless_mail.preprocessor import MailMessageDecryptor
from paperless_mail.preprocessor import MailMessagePreprocessor
@@ -530,6 +532,17 @@ class MailAccountHandler(LoggingMixin):
account.imap_port,
account.imap_security,
) as M:
if (
account.is_token
and account.expiration is not None
and account.expiration < timezone.now()
):
manager = PaperlessMailOAuth2Manager()
if manager.refresh_account_oauth_token(account):
account.refresh_from_db()
else:
return total_processed_files
supports_gmail_labels = "X-GM-EXT-1" in M.client.capabilities
supports_auth_plain = "AUTH=PLAIN" in M.client.capabilities

View File

@@ -0,0 +1,43 @@
# Generated by Django 5.1.1 on 2024-10-05 17:12
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("paperless_mail", "0026_mailrule_enabled"),
]
operations = [
migrations.AddField(
model_name="mailaccount",
name="expiration",
field=models.DateTimeField(
blank=True,
help_text="The expiration date of the refresh token. ",
null=True,
verbose_name="expiration",
),
),
migrations.AddField(
model_name="mailaccount",
name="account_type",
field=models.PositiveIntegerField(
choices=[(1, "IMAP"), (2, "Gmail OAuth"), (3, "Outlook OAuth")],
default=1,
verbose_name="account type",
),
),
migrations.AddField(
model_name="mailaccount",
name="refresh_token",
field=models.CharField(
blank=True,
help_text="The refresh token to use for token authentication e.g. with oauth2.",
max_length=2048,
null=True,
verbose_name="refresh token",
),
),
]

View File

@@ -15,6 +15,11 @@ class MailAccount(document_models.ModelWithOwner):
SSL = 2, _("Use SSL")
STARTTLS = 3, _("Use STARTTLS")
class MailAccountType(models.IntegerChoices):
IMAP = 1, _("IMAP")
GMAIL_OAUTH = 2, _("Gmail OAuth")
OUTLOOK_OAUTH = 3, _("Outlook OAuth")
name = models.CharField(_("name"), max_length=256, unique=True)
imap_server = models.CharField(_("IMAP server"), max_length=256)
@@ -51,6 +56,31 @@ class MailAccount(document_models.ModelWithOwner):
),
)
account_type = models.PositiveIntegerField(
_("account type"),
choices=MailAccountType.choices,
default=MailAccountType.IMAP,
)
refresh_token = models.CharField(
_("refresh token"),
max_length=2048,
blank=True,
null=True,
help_text=_(
"The refresh token to use for token authentication e.g. with oauth2.",
),
)
expiration = models.DateTimeField(
_("expiration"),
blank=True,
null=True,
help_text=_(
"The expiration date of the refresh token. ",
),
)
def __str__(self):
return self.name

111
src/paperless_mail/oauth.py Normal file
View File

@@ -0,0 +1,111 @@
import asyncio
import logging
from datetime import timedelta
from django.conf import settings
from django.utils import timezone
from httpx_oauth.clients.google import GoogleOAuth2
from httpx_oauth.clients.microsoft import MicrosoftGraphOAuth2
from httpx_oauth.oauth2 import OAuth2Token
from httpx_oauth.oauth2 import RefreshTokenError
from paperless_mail.models import MailAccount
class PaperlessMailOAuth2Manager:
def __init__(self):
self._gmail_client = None
self._outlook_client = None
@property
def gmail_client(self) -> GoogleOAuth2:
if self._gmail_client is None:
self._gmail_client = GoogleOAuth2(
settings.GMAIL_OAUTH_CLIENT_ID,
settings.GMAIL_OAUTH_CLIENT_SECRET,
)
return self._gmail_client
@property
def outlook_client(self) -> MicrosoftGraphOAuth2:
if self._outlook_client is None:
self._outlook_client = MicrosoftGraphOAuth2(
settings.OUTLOOK_OAUTH_CLIENT_ID,
settings.OUTLOOK_OAUTH_CLIENT_SECRET,
)
return self._outlook_client
@property
def oauth_callback_url(self) -> str:
return f"{settings.OAUTH_CALLBACK_BASE_URL if settings.OAUTH_CALLBACK_BASE_URL is not None else settings.PAPERLESS_URL}{settings.BASE_URL}api/oauth/callback/"
@property
def oauth_redirect_url(self) -> str:
return f"{'http://localhost:4200/' if settings.DEBUG else settings.BASE_URL}mail" # e.g. "http://localhost:4200/mail" or "/mail"
def get_gmail_authorization_url(self) -> str:
return asyncio.run(
self.gmail_client.get_authorization_url(
redirect_uri=self.oauth_callback_url,
scope=["https://mail.google.com/"],
extras_params={"prompt": "consent", "access_type": "offline"},
),
)
def get_outlook_authorization_url(self) -> str:
return asyncio.run(
self.outlook_client.get_authorization_url(
redirect_uri=self.oauth_callback_url,
scope=[
"offline_access",
"https://outlook.office.com/IMAP.AccessAsUser.All",
],
),
)
def get_gmail_access_token(self, code: str) -> OAuth2Token:
return asyncio.run(
self.gmail_client.get_access_token(
code=code,
redirect_uri=self.oauth_callback_url,
),
)
def get_outlook_access_token(self, code: str) -> OAuth2Token:
return asyncio.run(
self.outlook_client.get_access_token(
code=code,
redirect_uri=self.oauth_callback_url,
),
)
def refresh_account_oauth_token(self, account: MailAccount) -> bool:
"""
Refreshes the oauth token for the given mail account.
"""
logger = logging.getLogger("paperless_mail")
logger.debug(f"Attempting to refresh oauth token for account {account}")
try:
result: OAuth2Token
if account.account_type == MailAccount.MailAccountType.GMAIL_OAUTH:
result = asyncio.run(
self.gmail_client.refresh_token(
refresh_token=account.refresh_token,
),
)
elif account.account_type == MailAccount.MailAccountType.OUTLOOK_OAUTH:
result = asyncio.run(
self.outlook_client.refresh_token(
refresh_token=account.refresh_token,
),
)
account.password = result["access_token"]
account.expiration = timezone.now() + timedelta(
seconds=result["expires_in"],
)
account.save()
logger.debug(f"Successfully refreshed oauth token for account {account}")
return True
except RefreshTokenError as e:
logger.error(f"Failed to refresh oauth token for account {account}: {e}")
return False

View File

@@ -39,6 +39,8 @@ class MailAccountSerializer(OwnedObjectSerializer):
"user_can_change",
"permissions",
"set_permissions",
"account_type",
"expiration",
]
def update(self, instance, validated_data):

View File

@@ -4,9 +4,11 @@ import random
import uuid
from collections import namedtuple
from contextlib import AbstractContextManager
from datetime import timedelta
from unittest import mock
import pytest
from django.contrib.auth.models import User
from django.core.management import call_command
from django.db import DatabaseError
from django.test import TestCase
@@ -19,6 +21,8 @@ from imap_tools import MailboxLoginError
from imap_tools import MailMessage
from imap_tools import MailMessageFlags
from imap_tools import errors
from rest_framework import status
from rest_framework.test import APITestCase
from documents.models import Correspondent
from documents.tests.utils import DirectoriesMixin
@@ -1590,3 +1594,128 @@ class TestTasks(TestCase):
tasks.process_mail_accounts()
self.assertEqual(m.call_count, 0)
class TestMailAccountTestView(APITestCase):
def setUp(self):
self.mailMocker = MailMocker()
self.mailMocker.setUp()
self.user = User.objects.create_user(
username="testuser",
password="testpassword",
)
self.client.force_authenticate(user=self.user)
self.url = "/api/mail_accounts/test/"
def test_mail_account_test_view_success(self):
data = {
"imap_server": "imap.example.com",
"imap_port": 993,
"imap_security": MailAccount.ImapSecurity.SSL,
"username": "admin",
"password": "secret",
"account_type": MailAccount.MailAccountType.IMAP,
"is_token": False,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {"success": True})
def test_mail_account_test_view_mail_error(self):
data = {
"imap_server": "imap.example.com",
"imap_port": 993,
"imap_security": MailAccount.ImapSecurity.SSL,
"username": "admin",
"password": "wrong",
"account_type": MailAccount.MailAccountType.IMAP,
"is_token": False,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.content.decode(), "Unable to connect to server")
@mock.patch(
"paperless_mail.oauth.PaperlessMailOAuth2Manager.refresh_account_oauth_token",
)
def test_mail_account_test_view_refresh_token(
self,
mock_refresh_account_oauth_token,
):
"""
GIVEN:
- Mail account with expired token
WHEN:
- Mail account is tested
THEN:
- Should refresh the token
"""
existing_account = MailAccount.objects.create(
imap_server="imap.example.com",
imap_port=993,
imap_security=MailAccount.ImapSecurity.SSL,
username="admin",
password="secret",
account_type=MailAccount.MailAccountType.GMAIL_OAUTH,
refresh_token="oldtoken",
expiration=timezone.now() - timedelta(days=1),
is_token=True,
)
mock_refresh_account_oauth_token.return_value = True
data = {
"id": existing_account.id,
"imap_server": "imap.example.com",
"imap_port": 993,
"imap_security": MailAccount.ImapSecurity.SSL,
"username": "admin",
"password": "****",
"is_token": True,
}
self.client.post(self.url, data, format="json")
self.assertEqual(mock_refresh_account_oauth_token.call_count, 1)
@mock.patch(
"paperless_mail.oauth.PaperlessMailOAuth2Manager.refresh_account_oauth_token",
)
def test_mail_account_test_view_refresh_token_fails(
self,
mock_mock_refresh_account_oauth_token,
):
"""
GIVEN:
- Mail account with expired token
WHEN:
- Mail account is tested
- Token refresh fails
THEN:
- Should log an error
"""
existing_account = MailAccount.objects.create(
imap_server="imap.example.com",
imap_port=993,
imap_security=MailAccount.ImapSecurity.SSL,
username="admin",
password="secret",
account_type=MailAccount.MailAccountType.GMAIL_OAUTH,
refresh_token="oldtoken",
expiration=timezone.now() - timedelta(days=1),
is_token=True,
)
mock_mock_refresh_account_oauth_token.return_value = False
data = {
"id": existing_account.id,
"imap_server": "imap.example.com",
"imap_port": 993,
"imap_security": MailAccount.ImapSecurity.SSL,
"username": "admin",
"password": "****",
"is_token": True,
}
with self.assertLogs("paperless_mail", level="ERROR") as cm:
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
error_str = cm.output[0]
expected_str = "Unable to refresh oauth token"
self.assertIn(expected_str, error_str)

View File

@@ -0,0 +1,334 @@
from datetime import timedelta
from unittest import mock
from django.conf import settings
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import TestCase
from django.test import override_settings
from django.utils import timezone
from httpx_oauth.oauth2 import GetAccessTokenError
from httpx_oauth.oauth2 import RefreshTokenError
from rest_framework import status
from paperless_mail.mail import MailAccountHandler
from paperless_mail.models import MailAccount
from paperless_mail.oauth import PaperlessMailOAuth2Manager
class TestMailOAuth(
TestCase,
):
def setUp(self) -> None:
self.user = User.objects.create_user("testuser")
self.user.user_permissions.add(
*Permission.objects.filter(
codename__in=[
"add_mailaccount",
],
),
)
self.user.save()
self.client.force_login(self.user)
self.mail_account_handler = MailAccountHandler()
# Mock settings
settings.OAUTH_CALLBACK_BASE_URL = "http://localhost:8000"
settings.GMAIL_OAUTH_CLIENT_ID = "test_gmail_client_id"
settings.GMAIL_OAUTH_CLIENT_SECRET = "test_gmail_client_secret"
settings.OUTLOOK_OAUTH_CLIENT_ID = "test_outlook_client_id"
settings.OUTLOOK_OAUTH_CLIENT_SECRET = "test_outlook_client_secret"
super().setUp()
def test_generate_paths(self):
"""
GIVEN:
- Mocked settings for OAuth callback and base URLs
WHEN:
- get_oauth_callback_url and get_oauth_redirect_url are called
THEN:
- Correct URLs are generated
"""
# Callback URL
oauth_manager = PaperlessMailOAuth2Manager()
with override_settings(OAUTH_CALLBACK_BASE_URL="http://paperless.example.com"):
self.assertEqual(
oauth_manager.oauth_callback_url,
"http://paperless.example.com/api/oauth/callback/",
)
with override_settings(
OAUTH_CALLBACK_BASE_URL=None,
PAPERLESS_URL="http://paperless.example.com",
):
self.assertEqual(
oauth_manager.oauth_callback_url,
"http://paperless.example.com/api/oauth/callback/",
)
with override_settings(
OAUTH_CALLBACK_BASE_URL=None,
PAPERLESS_URL="http://paperless.example.com",
BASE_URL="/paperless/",
):
self.assertEqual(
oauth_manager.oauth_callback_url,
"http://paperless.example.com/paperless/api/oauth/callback/",
)
# Redirect URL
with override_settings(DEBUG=True):
self.assertEqual(
oauth_manager.oauth_redirect_url,
"http://localhost:4200/mail",
)
with override_settings(DEBUG=False):
self.assertEqual(
oauth_manager.oauth_redirect_url,
"/mail",
)
@mock.patch(
"paperless_mail.oauth.PaperlessMailOAuth2Manager.get_gmail_access_token",
)
@mock.patch(
"paperless_mail.oauth.PaperlessMailOAuth2Manager.get_outlook_access_token",
)
def test_oauth_callback_view_success(
self,
mock_get_outlook_access_token,
mock_get_gmail_access_token,
):
"""
GIVEN:
- Mocked settings for Gmail and Outlook OAuth client IDs and secrets
WHEN:
- OAuth callback is called with a code and scope
- OAuth callback is called with a code and no scope
THEN:
- Gmail mail account is created
- Outlook mail account is created
"""
mock_get_gmail_access_token.return_value = {
"access_token": "test_access_token",
"refresh_token": "test_refresh_token",
"expires_in": 3600,
}
mock_get_outlook_access_token.return_value = {
"access_token": "test_access_token",
"refresh_token": "test_refresh_token",
"expires_in": 3600,
}
# Test Google OAuth callback
response = self.client.get(
"/api/oauth/callback/?code=test_code&scope=https://mail.google.com/",
)
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
self.assertIn("oauth_success=1", response.url)
mock_get_gmail_access_token.assert_called_once()
self.assertTrue(
MailAccount.objects.filter(imap_server="imap.gmail.com").exists(),
)
# Test Outlook OAuth callback
response = self.client.get("/api/oauth/callback/?code=test_code")
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
self.assertIn("oauth_success=1", response.url)
self.assertTrue(
MailAccount.objects.filter(imap_server="outlook.office365.com").exists(),
)
@mock.patch("httpx_oauth.oauth2.BaseOAuth2.get_access_token")
def test_oauth_callback_view_fails(self, mock_get_access_token):
"""
GIVEN:
- Mocked settings for Gmail and Outlook OAuth client IDs and secrets
WHEN:
- OAuth callback is called and get access token returns an error
THEN:
- No mail account is created
- Error is logged
"""
mock_get_access_token.side_effect = GetAccessTokenError("test_error")
with self.assertLogs("paperless_mail", level="ERROR") as cm:
# Test Google OAuth callback
response = self.client.get(
"/api/oauth/callback/?code=test_code&scope=https://mail.google.com/",
)
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
self.assertIn("oauth_success=0", response.url)
self.assertFalse(
MailAccount.objects.filter(imap_server="imap.gmail.com").exists(),
)
# Test Outlook OAuth callback
response = self.client.get("/api/oauth/callback/?code=test_code")
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
self.assertIn("oauth_success=0", response.url)
self.assertFalse(
MailAccount.objects.filter(
imap_server="outlook.office365.com",
).exists(),
)
self.assertIn("Error getting access token: test_error", cm.output[0])
def test_oauth_callback_view_insufficient_permissions(self):
"""
GIVEN:
- Mocked settings for Gmail and Outlook OAuth client IDs and secrets
- User without add_mailaccount permission
WHEN:
- OAuth callback is called
THEN:
- 400 bad request returned, no mail accounts are created
"""
self.user.user_permissions.remove(
*Permission.objects.filter(
codename__in=[
"add_mailaccount",
],
),
)
self.user.save()
response = self.client.get(
"/api/oauth/callback/?code=test_code&scope=https://mail.google.com/",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(
MailAccount.objects.filter(imap_server="imap.gmail.com").exists(),
)
self.assertFalse(
MailAccount.objects.filter(imap_server="outlook.office365.com").exists(),
)
def test_oauth_callback_view_no_code(self):
"""
GIVEN:
- Mocked settings for Gmail and Outlook OAuth client IDs and secrets
WHEN:
- OAuth callback is called without a code
THEN:
- 400 bad request returned, no mail accounts are created
"""
response = self.client.get(
"/api/oauth/callback/",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(
MailAccount.objects.filter(imap_server="imap.gmail.com").exists(),
)
self.assertFalse(
MailAccount.objects.filter(imap_server="outlook.office365.com").exists(),
)
@mock.patch("paperless_mail.mail.get_mailbox")
@mock.patch(
"httpx_oauth.oauth2.BaseOAuth2.refresh_token",
)
def test_refresh_token_on_handle_mail_account(
self,
mock_refresh_token,
mock_get_mailbox,
):
"""
GIVEN:
- Mail account with refresh token and expiration
WHEN:
- handle_mail_account is called
THEN:
- Refresh token is called
"""
mock_mailbox = mock.MagicMock()
mock_get_mailbox.return_value.__enter__.return_value = mock_mailbox
mail_account = MailAccount.objects.create(
name="Test Gmail Mail Account",
username="test_username",
imap_security=MailAccount.ImapSecurity.SSL,
imap_port=993,
account_type=MailAccount.MailAccountType.GMAIL_OAUTH,
is_token=True,
refresh_token="test_refresh_token",
expiration=timezone.now() - timedelta(days=1),
)
mock_refresh_token.return_value = {
"access_token": "test_access_token",
"refresh_token": "test_refresh_token",
"expires_in": 3600,
}
self.mail_account_handler.handle_mail_account(mail_account)
mock_refresh_token.assert_called_once()
mock_refresh_token.reset_mock()
mock_refresh_token.return_value = {
"access_token": "test_access_token",
"refresh_token": "test_refresh",
"expires_in": 3600,
}
outlook_mail_account = MailAccount.objects.create(
name="Test Outlook Mail Account",
username="test_username",
imap_security=MailAccount.ImapSecurity.SSL,
imap_port=993,
account_type=MailAccount.MailAccountType.OUTLOOK_OAUTH,
is_token=True,
refresh_token="test_refresh_token",
expiration=timezone.now() - timedelta(days=1),
)
self.mail_account_handler.handle_mail_account(outlook_mail_account)
mock_refresh_token.assert_called_once()
@mock.patch("paperless_mail.mail.get_mailbox")
@mock.patch(
"httpx_oauth.oauth2.BaseOAuth2.refresh_token",
)
def test_refresh_token_on_handle_mail_account_fails(
self,
mock_refresh_token,
mock_get_mailbox,
):
"""
GIVEN:
- Mail account with refresh token and expiration
WHEN:
- handle_mail_account is called
- Refresh token is called but fails
THEN:
- Error is logged
- 0 processed mails is returned
"""
mock_mailbox = mock.MagicMock()
mock_get_mailbox.return_value.__enter__.return_value = mock_mailbox
mail_account = MailAccount.objects.create(
name="Test Gmail Mail Account",
username="test_username",
imap_security=MailAccount.ImapSecurity.SSL,
imap_port=993,
account_type=MailAccount.MailAccountType.GMAIL_OAUTH,
is_token=True,
refresh_token="test_refresh_token",
expiration=timezone.now() - timedelta(days=1),
)
mock_refresh_token.side_effect = RefreshTokenError("test_error")
with self.assertLogs("paperless_mail", level="ERROR") as cm:
# returns 0 processed mails
self.assertEqual(
self.mail_account_handler.handle_mail_account(mail_account),
0,
)
mock_refresh_token.assert_called_once()
self.assertIn(
f"Failed to refresh oauth token for account {mail_account}: test_error",
cm.output[0],
)

View File

@@ -1,7 +1,11 @@
import datetime
import logging
from datetime import timedelta
from django.http import HttpResponseBadRequest
from django.http import HttpResponseRedirect
from django.utils import timezone
from httpx_oauth.oauth2 import GetAccessTokenError
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
@@ -16,6 +20,7 @@ from paperless_mail.mail import get_mailbox
from paperless_mail.mail import mailbox_login
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
from paperless_mail.oauth import PaperlessMailOAuth2Manager
from paperless_mail.serialisers import MailAccountSerializer
from paperless_mail.serialisers import MailRuleSerializer
@@ -50,27 +55,114 @@ class MailAccountTestView(GenericAPIView):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# account exists, use the password from there instead of ***
# account exists, use the password from there instead of *** and refresh_token / expiration
if (
len(serializer.validated_data.get("password").replace("*", "")) == 0
and request.data["id"] is not None
):
serializer.validated_data["password"] = MailAccount.objects.get(
pk=request.data["id"],
).password
existing_account = MailAccount.objects.get(pk=request.data["id"])
serializer.validated_data["password"] = existing_account.password
serializer.validated_data["account_type"] = existing_account.account_type
serializer.validated_data["refresh_token"] = existing_account.refresh_token
serializer.validated_data["expiration"] = existing_account.expiration
account = MailAccount(**serializer.validated_data)
with get_mailbox(
account.imap_server,
account.imap_port,
account.imap_security,
) as M:
try:
if (
account.is_token
and account.expiration is not None
and account.expiration < timezone.now()
):
oauth_manager = PaperlessMailOAuth2Manager()
if oauth_manager.refresh_account_oauth_token(existing_account):
# User is not changing password and token needs to be refreshed
existing_account.refresh_from_db()
account.password = existing_account.password
else:
raise MailError("Unable to refresh oauth token")
mailbox_login(M, account)
return Response({"success": True})
except MailError:
except MailError as e:
logger.error(
f"Mail account {account} test failed",
f"Mail account {account} test failed: {e}",
)
return HttpResponseBadRequest("Unable to connect to server")
class OauthCallbackView(GenericAPIView):
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
if not (
request.user and request.user.has_perms(["paperless_mail.add_mailaccount"])
):
return HttpResponseBadRequest(
"You do not have permission to add mail accounts",
)
logger = logging.getLogger("paperless_mail")
code = request.query_params.get("code")
# Gmail passes scope as a query param, Outlook does not
scope = request.query_params.get("scope")
if code is None:
logger.error(
f"Invalid oauth callback request, code: {code}, scope: {scope}",
)
return HttpResponseBadRequest("Invalid request, see logs for more detail")
oauth_manager = PaperlessMailOAuth2Manager()
try:
if scope is not None and "google" in scope:
# Google
account_type = MailAccount.MailAccountType.GMAIL_OAUTH
imap_server = "imap.gmail.com"
defaults = {
"name": f"Gmail OAuth {timezone.now()}",
"username": "",
"imap_security": MailAccount.ImapSecurity.SSL,
"imap_port": 993,
"account_type": account_type,
}
result = oauth_manager.get_gmail_access_token(code)
elif scope is None:
# Outlook
account_type = MailAccount.MailAccountType.OUTLOOK_OAUTH
imap_server = "outlook.office365.com"
defaults = {
"name": f"Outlook OAuth {timezone.now()}",
"username": "",
"imap_security": MailAccount.ImapSecurity.SSL,
"imap_port": 993,
"account_type": account_type,
}
result = oauth_manager.get_outlook_access_token(code)
access_token = result["access_token"]
refresh_token = result["refresh_token"]
expires_in = result["expires_in"]
account, _ = MailAccount.objects.update_or_create(
password=access_token,
is_token=True,
imap_server=imap_server,
refresh_token=refresh_token,
expiration=timezone.now() + timedelta(seconds=expires_in),
defaults=defaults,
)
return HttpResponseRedirect(
f"{oauth_manager.oauth_redirect_url}?oauth_success=1&account_id={account.pk}",
)
except GetAccessTokenError as e:
logger.error(f"Error getting access token: {e}")
return HttpResponseRedirect(
f"{oauth_manager.oauth_redirect_url}?oauth_success=0",
)