]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Allows users to use OAuth tokens instead of passwords
authorTrenton H <797416+stumpylog@users.noreply.github.com>
Wed, 22 Mar 2023 18:54:20 +0000 (11:54 -0700)
committerTrenton H <797416+stumpylog@users.noreply.github.com>
Thu, 23 Mar 2023 15:52:12 +0000 (08:52 -0700)
src-ui/src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.ts
src-ui/src/app/data/paperless-mail-account.ts
src/paperless_mail/mail.py
src/paperless_mail/migrations/0020_mailaccount_is_token.py [new file with mode: 0644]
src/paperless_mail/models.py
src/paperless_mail/serialisers.py
src/paperless_mail/tests/test_mail.py

index 8164fca9a1b8c4daccfd44683d0dd0f1b16957b9..356c0a300f415854249c7988151cb922a5c385a3 100644 (file)
@@ -15,6 +15,7 @@
       <div class="col">
         <app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
         <app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password>
+        <app-input-check i18n-title title="Is Token?" formControlName="is_token" [error]="error?.is_token"></app-input-check>
         <app-input-text i18n-title title="Character Set" formControlName="character_set" [error]="error?.character_set"></app-input-text>
       </div>
     </div>
index 1c42ffeb072b10f2b39ec37c6bf6a9a441ebe36e..558ec02293a82d86e4d47b09a0b246ec689627e3 100644 (file)
@@ -45,6 +45,7 @@ export class MailAccountEditDialogComponent extends EditDialogComponent<Paperles
       imap_security: new FormControl(IMAPSecurity.SSL),
       username: new FormControl(null),
       password: new FormControl(null),
+      is_token: new FormControl(false),
       character_set: new FormControl('UTF-8'),
     })
   }
index 9f875e78346a72eaf72900e576bff5791bffc770..484997213d9d6734a369828323e4034d3d42b270 100644 (file)
@@ -20,4 +20,6 @@ export interface PaperlessMailAccount extends ObjectWithId {
   password: string
 
   character_set?: string
+
+  is_token: boolean
 }
index 5d06db7334ed5b11feabe8fcdd5c4df064467de2..50a5785632d050619b54e936cdf978df55cc3529 100644 (file)
@@ -202,20 +202,21 @@ def mailbox_login(mailbox: MailBox, account: MailAccount):
 
     try:
 
-        mailbox.login(account.username, account.password)
+        if account.is_token:
+            mailbox.xoauth2(account.username, account.password)
+        else:
+            try:
+                _ = account.password.encode("ascii")
+                use_ascii_login = True
+            except UnicodeEncodeError:
+                use_ascii_login = False
 
-    except UnicodeEncodeError:
-        logger.debug("Falling back to AUTH=PLAIN")
+            if use_ascii_login:
+                mailbox.login(account.username, account.password)
+            else:
+                logger.debug("Falling back to AUTH=PLAIN")
+                mailbox.login_utf8(account.username, account.password)
 
-        try:
-            mailbox.login_utf8(account.username, account.password)
-        except Exception as e:
-            logger.error(
-                "Unable to authenticate with mail server using AUTH=PLAIN",
-            )
-            raise MailError(
-                f"Error while authenticating account {account}",
-            ) from e
     except Exception as e:
         logger.error(
             f"Error while authenticating account {account}: {e}",
diff --git a/src/paperless_mail/migrations/0020_mailaccount_is_token.py b/src/paperless_mail/migrations/0020_mailaccount_is_token.py
new file mode 100644 (file)
index 0000000..5a2f8c5
--- /dev/null
@@ -0,0 +1,19 @@
+# Generated by Django 4.1.7 on 2023-03-22 17:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("paperless_mail", "0019_mailrule_filter_to"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="mailaccount",
+            name="is_token",
+            field=models.BooleanField(
+                default=False, verbose_name="Is token authentication"
+            ),
+        ),
+    ]
index d0289ef31f8d0d136147669ce58c3b7bc1c35b98..2b263166440d0f7b4cea237c2b58d12c88e46852 100644 (file)
@@ -38,6 +38,8 @@ class MailAccount(document_models.ModelWithOwner):
 
     password = models.CharField(_("password"), max_length=256)
 
+    is_token = models.BooleanField(_("Is token authentication"), default=False)
+
     character_set = models.CharField(
         _("character set"),
         max_length=256,
index 28511fcb8b827e45cefb8ec32c5881dd148d8cf2..133227468e5094d37b3fc30e23cc65a9c86a5bbf 100644 (file)
@@ -34,6 +34,7 @@ class MailAccountSerializer(OwnedObjectSerializer):
             "username",
             "password",
             "character_set",
+            "is_token",
         ]
 
     def update(self, instance, validated_data):
index b1d8218243cebd5424048f13489157d6b71b5225..c0bfccba5e592d3d99b8e4c08dc4998712ac5fec 100644 (file)
@@ -83,6 +83,8 @@ class BogusMailBox(ContextManager):
     ASCII_PASSWORD: str = "secret"
     # Note the non-ascii characters here
     UTF_PASSWORD: str = "w57äöüw4b6huwb6nhu"
+    # A dummy access token
+    ACCESS_TOKEN = "ea7e075cd3acf2c54c48e600398d5d5a"
 
     def __init__(self):
         self.messages: List[MailMessage] = []
@@ -112,6 +114,10 @@ class BogusMailBox(ContextManager):
         if username != self.USERNAME or password != self.UTF_PASSWORD:
             raise MailboxLoginError("BAD", "OK")
 
+    def xoauth2(self, username: str, access_token: str):
+        if username != self.USERNAME or access_token != self.ACCESS_TOKEN:
+            raise MailboxLoginError("BAD", "OK")
+
     def fetch(self, criteria, mark_seen, charset=""):
         msg = self.messages
 
@@ -737,6 +743,14 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
         self.assertEqual(len(self.bogus_mailbox.messages), 3)
 
     def test_error_login(self):
+        """
+        GIVEN:
+            - Account configured with incorrect password
+        WHEN:
+            - Account tried to login
+        THEN:
+            - MailError with correct message raised
+        """
         account = MailAccount.objects.create(
             name="test",
             imap_server="",
@@ -1007,6 +1021,8 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
         """
         GIVEN:
             - Mail account with password containing non-ASCII characters
+        WHEN:
+            - Mail account is handled
         THEN:
             - Should still authenticate to the mail account
         """
@@ -1040,6 +1056,8 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
         GIVEN:
             - Mail account with password containing non-ASCII characters
             - Incorrect password value
+        WHEN:
+            - Mail account is handled
         THEN:
             - Should raise a MailError for the account
         """
@@ -1064,6 +1082,41 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
             account,
         )
 
+    def test_auth_with_valid_token(self):
+        """
+        GIVEN:
+            - Mail account configured with access token
+        WHEN:
+            - Mail account is handled
+        THEN:
+            - Should still authenticate to the mail account
+        """
+        account = MailAccount.objects.create(
+            name="test",
+            imap_server="",
+            username=BogusMailBox.USERNAME,
+            # Note the non-ascii characters here
+            password=BogusMailBox.ACCESS_TOKEN,
+            is_token=True,
+        )
+
+        _ = MailRule.objects.create(
+            name="testrule",
+            account=account,
+            action=MailRule.MailAction.MARK_READ,
+        )
+
+        self.assertEqual(len(self.bogus_mailbox.messages), 3)
+        self.assertEqual(self._queue_consumption_tasks_mock.call_count, 0)
+        self.assertEqual(len(self.bogus_mailbox.fetch("UNSEEN", False)), 2)
+
+        self.mail_account_handler.handle_mail_account(account)
+        self.apply_mail_actions()
+
+        self.assertEqual(self._queue_consumption_tasks_mock.call_count, 2)
+        self.assertEqual(len(self.bogus_mailbox.fetch("UNSEEN", False)), 0)
+        self.assertEqual(len(self.bogus_mailbox.messages), 3)
+
     def assert_queue_consumption_tasks_call_args(self, expected_call_args: List):
         """
         Verifies that queue_consumption_tasks has been called with the expected arguments.