mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Allows users to use OAuth tokens instead of passwords
This commit is contained in:
		| @@ -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> | ||||
|   | ||||
| @@ -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'), | ||||
|     }) | ||||
|   } | ||||
|   | ||||
| @@ -20,4 +20,6 @@ export interface PaperlessMailAccount extends ObjectWithId { | ||||
|   password: string | ||||
|  | ||||
|   character_set?: string | ||||
|  | ||||
|   is_token: boolean | ||||
| } | ||||
|   | ||||
| @@ -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}", | ||||
|   | ||||
							
								
								
									
										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) | ||||
|  | ||||
|     is_token = models.BooleanField(_("Is token authentication"), default=False) | ||||
|  | ||||
|     character_set = models.CharField( | ||||
|         _("character set"), | ||||
|         max_length=256, | ||||
|   | ||||
| @@ -34,6 +34,7 @@ class MailAccountSerializer(OwnedObjectSerializer): | ||||
|             "username", | ||||
|             "password", | ||||
|             "character_set", | ||||
|             "is_token", | ||||
|         ] | ||||
|  | ||||
|     def update(self, instance, validated_data): | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Trenton H
					Trenton H