mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Allows users to use OAuth tokens instead of passwords
This commit is contained in:
		@@ -15,6 +15,7 @@
 | 
				
			|||||||
      <div class="col">
 | 
					      <div class="col">
 | 
				
			||||||
        <app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text>
 | 
					        <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-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>
 | 
					        <app-input-text i18n-title title="Character Set" formControlName="character_set" [error]="error?.character_set"></app-input-text>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,6 +45,7 @@ export class MailAccountEditDialogComponent extends EditDialogComponent<Paperles
 | 
				
			|||||||
      imap_security: new FormControl(IMAPSecurity.SSL),
 | 
					      imap_security: new FormControl(IMAPSecurity.SSL),
 | 
				
			||||||
      username: new FormControl(null),
 | 
					      username: new FormControl(null),
 | 
				
			||||||
      password: new FormControl(null),
 | 
					      password: new FormControl(null),
 | 
				
			||||||
 | 
					      is_token: new FormControl(false),
 | 
				
			||||||
      character_set: new FormControl('UTF-8'),
 | 
					      character_set: new FormControl('UTF-8'),
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,4 +20,6 @@ export interface PaperlessMailAccount extends ObjectWithId {
 | 
				
			|||||||
  password: string
 | 
					  password: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  character_set?: string
 | 
					  character_set?: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  is_token: boolean
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -202,20 +202,21 @@ def mailbox_login(mailbox: MailBox, account: MailAccount):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try:
 | 
					    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:
 | 
					            if use_ascii_login:
 | 
				
			||||||
        logger.debug("Falling back to AUTH=PLAIN")
 | 
					                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:
 | 
					    except Exception as e:
 | 
				
			||||||
        logger.error(
 | 
					        logger.error(
 | 
				
			||||||
            f"Error while authenticating account {account}: {e}",
 | 
					            f"Error while authenticating account {account}: {e}",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										19
									
								
								src/paperless_mail/migrations/0020_mailaccount_is_token.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/paperless_mail/migrations/0020_mailaccount_is_token.py
									
									
									
									
									
										Normal file
									
								
							@@ -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"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -38,6 +38,8 @@ class MailAccount(document_models.ModelWithOwner):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    password = models.CharField(_("password"), max_length=256)
 | 
					    password = models.CharField(_("password"), max_length=256)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    is_token = models.BooleanField(_("Is token authentication"), default=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    character_set = models.CharField(
 | 
					    character_set = models.CharField(
 | 
				
			||||||
        _("character set"),
 | 
					        _("character set"),
 | 
				
			||||||
        max_length=256,
 | 
					        max_length=256,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,6 +34,7 @@ class MailAccountSerializer(OwnedObjectSerializer):
 | 
				
			|||||||
            "username",
 | 
					            "username",
 | 
				
			||||||
            "password",
 | 
					            "password",
 | 
				
			||||||
            "character_set",
 | 
					            "character_set",
 | 
				
			||||||
 | 
					            "is_token",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update(self, instance, validated_data):
 | 
					    def update(self, instance, validated_data):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -83,6 +83,8 @@ class BogusMailBox(ContextManager):
 | 
				
			|||||||
    ASCII_PASSWORD: str = "secret"
 | 
					    ASCII_PASSWORD: str = "secret"
 | 
				
			||||||
    # Note the non-ascii characters here
 | 
					    # Note the non-ascii characters here
 | 
				
			||||||
    UTF_PASSWORD: str = "w57äöüw4b6huwb6nhu"
 | 
					    UTF_PASSWORD: str = "w57äöüw4b6huwb6nhu"
 | 
				
			||||||
 | 
					    # A dummy access token
 | 
				
			||||||
 | 
					    ACCESS_TOKEN = "ea7e075cd3acf2c54c48e600398d5d5a"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self):
 | 
					    def __init__(self):
 | 
				
			||||||
        self.messages: List[MailMessage] = []
 | 
					        self.messages: List[MailMessage] = []
 | 
				
			||||||
@@ -112,6 +114,10 @@ class BogusMailBox(ContextManager):
 | 
				
			|||||||
        if username != self.USERNAME or password != self.UTF_PASSWORD:
 | 
					        if username != self.USERNAME or password != self.UTF_PASSWORD:
 | 
				
			||||||
            raise MailboxLoginError("BAD", "OK")
 | 
					            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=""):
 | 
					    def fetch(self, criteria, mark_seen, charset=""):
 | 
				
			||||||
        msg = self.messages
 | 
					        msg = self.messages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -737,6 +743,14 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
 | 
				
			|||||||
        self.assertEqual(len(self.bogus_mailbox.messages), 3)
 | 
					        self.assertEqual(len(self.bogus_mailbox.messages), 3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_error_login(self):
 | 
					    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(
 | 
					        account = MailAccount.objects.create(
 | 
				
			||||||
            name="test",
 | 
					            name="test",
 | 
				
			||||||
            imap_server="",
 | 
					            imap_server="",
 | 
				
			||||||
@@ -1007,6 +1021,8 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        GIVEN:
 | 
					        GIVEN:
 | 
				
			||||||
            - Mail account with password containing non-ASCII characters
 | 
					            - Mail account with password containing non-ASCII characters
 | 
				
			||||||
 | 
					        WHEN:
 | 
				
			||||||
 | 
					            - Mail account is handled
 | 
				
			||||||
        THEN:
 | 
					        THEN:
 | 
				
			||||||
            - Should still authenticate to the mail account
 | 
					            - Should still authenticate to the mail account
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -1040,6 +1056,8 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
 | 
				
			|||||||
        GIVEN:
 | 
					        GIVEN:
 | 
				
			||||||
            - Mail account with password containing non-ASCII characters
 | 
					            - Mail account with password containing non-ASCII characters
 | 
				
			||||||
            - Incorrect password value
 | 
					            - Incorrect password value
 | 
				
			||||||
 | 
					        WHEN:
 | 
				
			||||||
 | 
					            - Mail account is handled
 | 
				
			||||||
        THEN:
 | 
					        THEN:
 | 
				
			||||||
            - Should raise a MailError for the account
 | 
					            - Should raise a MailError for the account
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -1064,6 +1082,41 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
 | 
				
			|||||||
            account,
 | 
					            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):
 | 
					    def assert_queue_consumption_tasks_call_args(self, expected_call_args: List):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Verifies that queue_consumption_tasks has been called with the expected arguments.
 | 
					        Verifies that queue_consumption_tasks has been called with the expected arguments.
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user