diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index a9481cf409..d830587a43 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -24,6 +24,7 @@ use OCA\Mail\Service\ContextChat\ContextChatSettingsService; use OCA\Mail\Service\InternalAddressService; use OCA\Mail\Service\OutboxService; +use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager; use OCA\Mail\Service\QuickActionsService; use OCA\Mail\Service\SmimeService; use OCA\Viewer\Event\LoadViewer; @@ -94,6 +95,7 @@ public function __construct( private ContextChatSettingsService $contextChatSettingsService, private ClassificationSettingsService $classificationSettingsService, private IAppConfig $appConfig, + private ProvisioningManager $provisioningManager, ) { parent::__construct($appName, $request); @@ -226,6 +228,26 @@ public function index(): TemplateResponse { $passwordIsUnavailable, ); + // Whether any of the user's provisioned accounts use a master password. + // Frontend skips INBOX sync for provisioned accounts when the session has + // no login password *unless* this is true (#13807, #9008, #9653). + $masterPasswordEnabled = false; + foreach ($mailAccounts as $mailAccount) { + $provisioningId = $mailAccount->getMailAccount()->getProvisioningId(); + if ($provisioningId === null) { + continue; + } + $provisioning = $this->provisioningManager->getConfigById($provisioningId); + if ($provisioning !== null && $provisioning->getMasterPasswordEnabled() === true) { + $masterPasswordEnabled = true; + break; + } + } + $this->initialStateService->provideInitialState( + 'master-password-enabled', + $masterPasswordEnabled, + ); + $response = new TemplateResponse($this->appName, 'index'); $this->initialStateService->provideInitialState('preferences', [ 'attachment-size-limit' => $this->config->getSystemValue('app.mail.attachment-size-limit', 0), diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue index bc5efbd74a..cc33a3b1c1 100644 --- a/src/components/Navigation.vue +++ b/src/components/Navigation.vue @@ -233,14 +233,15 @@ export default { }, /** - * Disable provisioned accounts when no password is available. + * Disable provisioned accounts when no password is available and no master password is configured. * Loading messages of those accounts will fail and an endless spinner will be shown. * * @param {object} account Account object * @return {boolean} True if the account should be disabled */ isDisabled(account) { - return (this.passwordIsUnavailable && !!account.provisioningId) && !!this.mainStore.masterPasswordEnabled + // Disable provisioned accounts on passwordless sessions unless a master password is configured. + return this.passwordIsUnavailable && !!account.provisioningId && !this.mainStore.masterPasswordEnabled }, }, } diff --git a/src/init.js b/src/init.js index 8304089892..41344c1454 100644 --- a/src/init.js +++ b/src/init.js @@ -142,6 +142,7 @@ export default function initAfterAppCreation() { mainStore.setSnoozeDisabledMutation(disableSnooze) mainStore.setGoogleOauthUrlMutation(googleOauthUrl) mainStore.setMicrosoftOauthUrlMutation(microsoftOauthUrl) + mainStore.setMasterPasswordEnabledMutation(loadState('mail', 'master-password-enabled', false)) mainStore.setFollowUpFeatureAvailableMutation(followUpFeatureAvailable) mainStore.setContextChatFeatureAvailableMutation(contextChatFeatureAvailable) diff --git a/src/store/mainStore/actions.js b/src/store/mainStore/actions.js index 1f6164f7b2..e203f3aabf 100644 --- a/src/store/mainStore/actions.js +++ b/src/store/mainStore/actions.js @@ -902,9 +902,10 @@ export default function mainStoreActions() { const mailbox = this.getMailbox(mailboxId) - // Skip superfluous requests if using passwordless authentication. They will fail anyway. + // Skip superfluous requests if using passwordless authentication without a + // master password. With a master password these requests succeed (#9008, #9653). const passwordIsUnavailable = this.getPreference('password-is-unavailable', false) - const isDisabled = (account) => passwordIsUnavailable && !!account.provisioningId + const isDisabled = (account) => passwordIsUnavailable && !!account.provisioningId && !this.masterPasswordEnabled if (mailbox.isUnified) { return Promise.all(this.getAccounts @@ -1004,9 +1005,10 @@ export default function mainStoreActions() { }) }, async syncInboxes() { - // Skip superfluous requests if using passwordless authentication. They will fail anyway. + // Skip superfluous requests if using passwordless authentication without a + // master password. With a master password these requests succeed (#9008, #9653). const passwordIsUnavailable = this.getPreference('password-is-unavailable', false) - const isDisabled = (account) => passwordIsUnavailable && !!account.provisioningId + const isDisabled = (account) => passwordIsUnavailable && !!account.provisioningId && !this.masterPasswordEnabled return handleHttpAuthErrors(async () => { const results = await Promise.all(this.getAccounts diff --git a/tests/Unit/Controller/PageControllerTest.php b/tests/Unit/Controller/PageControllerTest.php index 9b89951eeb..00bd7b783a 100644 --- a/tests/Unit/Controller/PageControllerTest.php +++ b/tests/Unit/Controller/PageControllerTest.php @@ -17,6 +17,7 @@ use OCA\Mail\Contracts\IUserPreferences; use OCA\Mail\Controller\PageController; use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\TagMapper; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AiIntegrations\AiIntegrationsService; @@ -26,6 +27,7 @@ use OCA\Mail\Service\InternalAddressService; use OCA\Mail\Service\MailManager; use OCA\Mail\Service\OutboxService; +use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager; use OCA\Mail\Service\QuickActionsService; use OCA\Mail\Service\SmimeService; use OCP\App\IAppManager; @@ -122,6 +124,10 @@ class PageControllerTest extends TestCase { private ContextChatSettingsService $contextChatSettingsService; private ClassificationSettingsService|MockObject $classificationSettingsService; + + /** @var ProvisioningManager|MockObject */ + private $provisioningManager; + protected function setUp(): void { parent::setUp(); @@ -154,6 +160,7 @@ protected function setUp(): void { $this->contextChatSettingsService->method('isIndexingEnabled')->willReturn(true); $this->classificationSettingsService = $this->createMock(ClassificationSettingsService::class); + $this->provisioningManager = $this->createMock(ProvisioningManager::class); $this->controller = new PageController( $this->appName, $this->request, @@ -180,7 +187,8 @@ protected function setUp(): void { $this->appManager, $this->contextChatSettingsService, $this->classificationSettingsService, - $this->appConfig + $this->appConfig, + $this->provisioningManager, ); } @@ -188,6 +196,12 @@ public function testIndex(): void { $account1 = $this->createMock(Account::class); $account2 = $this->createMock(Account::class); $mailbox = $this->createStub(Mailbox::class); + $mailAccount1 = $this->createMock(MailAccount::class); + $mailAccount1->method('getProvisioningId')->willReturn(null); + $mailAccount2 = $this->createMock(MailAccount::class); + $mailAccount2->method('getProvisioningId')->willReturn(null); + $account1->method('getMailAccount')->willReturn($mailAccount1); + $account2->method('getMailAccount')->willReturn($mailAccount2); $this->preferences->expects($this->exactly(15)) ->method('getPreference') ->willReturnMap([ @@ -347,7 +361,10 @@ public function testIndex(): void { $this->classificationSettingsService->expects(($this->once())) ->method(('isClassificationEnabledByDefault')) ->willReturn(true); - $this->initialState->expects($this->exactly(27)) + $this->provisioningManager->expects($this->never()) + ->method('getConfigById'); + + $this->initialState->expects($this->exactly(28)) ->method('provideInitialState') ->withConsecutive( ['debug', true], @@ -361,6 +378,7 @@ public function testIndex(): void { ['smime-sign-aliases',[]], ['sort-order', 'newest'], ['password-is-unavailable', true], + ['master-password-enabled', false], ['preferences', [ 'attachment-size-limit' => 123, 'external-avatars' => 'true',