Skip to content

feat(provisioning): Add support for Dovecot master user authentication#12442

Draft
kesselb wants to merge 13 commits intomainfrom
local-add-masteruser
Draft

feat(provisioning): Add support for Dovecot master user authentication#12442
kesselb wants to merge 13 commits intomainfrom
local-add-masteruser

Conversation

@kesselb
Copy link
Contributor

@kesselb kesselb commented Feb 17, 2026

Local copy of #12306 with conflicts resolved

'smtpSslMode' => $this->getSmtpSslMode(),
'masterPasswordEnabled' => $this->getMasterPasswordEnabled(),
'masterPassword' => !empty($this->getMasterPassword()) ? self::MASTER_PASSWORD_PLACEHOLDER : null,
'masterUser' => $this->getMasterUser(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial PR flagged the master user as confidential (like the masterPasword). The username doesn't sound too critical to me, so I've dropped it.

}
if (!isset($data['imapPort']) || (int)$data['imapPort'] === 0) {
$exception->setField('imapHost', false);
$exception->setField('imapPort', false);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, yet I fixed it while on it. It's extra commit; we can pull that out if necessary.

$masterPasswordEnabled = (bool)($data['masterPasswordEnabled'] ?? false);
$masterPassword = $data['masterPassword'] ?? '';
$masterUser = $data['masterUser'] ?? '';
$masterUserSeparator = $data['masterUserSeparator'] ?? '';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial PR had a check "if masterUser not empty, and masterUser is not the placeholder, and masterPasswordEnabled is false", then make masterPasswordEnabled required.

I've reworked it to only show the inputs for password, username, and separator when the checkbox is toggled.

Backend-wise, the validation should follow the checkbox. If master password enabled, then we need a password. If non-empty username is given, also the separator is needed.

In addition, the current values are now cleared if the master password is disabled.

if ($provisioningId !== null) {
$provisioning = $this->provisioningMapper->get($provisioningId);
if ($provisioning !== null && !empty($provisioning->getMasterUser())) {
$separator = $provisioning->getMasterUserSeparator() ?? '*';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChristophWurst if $provisioning = null, throw (like for oauth)?

$provisioning = $this->provisioningMapper->get($provisioningId);
if ($provisioning !== null && !empty($provisioning->getMasterUser())) {
$separator = $provisioning->getMasterUserSeparator() ?? '*';
$user = $user . $separator . $provisioning->getMasterUser();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChristophWurst wdyt about moving that logic to a trait?

@kesselb kesselb requested a review from Copilot March 9, 2026 17:26
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds provisioning support for Dovecot “master user” authentication by letting provisioned accounts authenticate as user{separator}masteruser (using the existing master password), and exposes this configuration in the provisioning settings UI.

Changes:

  • Add masterUser and masterUserSeparator fields to provisioning settings (UI + DB migration).
  • Append master-user login formatting in IMAP/SMTP/Sieve client factories when a provisioning config specifies a master user.
  • Extend validation and tests to cover the new provisioning fields and constructor wiring.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tests/Unit/SMTP/SmtpClientFactoryTest.php Updates factory construction to include ProvisioningMapper.
tests/Unit/Db/ProvisioningMapperTest.php Adds unit tests for provisioning validation (new file).
tests/Integration/Sieve/SieveClientFactoryTest.php Updates factory construction to include ProvisioningMapper.
tests/Integration/IMAP/IMAPClientFactoryTest.php Updates factory construction to include ProvisioningMapper.
tests/Integration/Framework/Caching.php Updates IMAP factory wiring to include ProvisioningMapper.
src/components/settings/ProvisioningSettings.vue Adds UI inputs/help text for master user + separator.
src/components/settings/ProvisionPreview.vue Updates preview to show effective login usernames and mode (static vs master user).
lib/Sieve/SieveClientFactory.php Applies master-user username formatting for Sieve connections.
lib/SMTP/SmtpClientFactory.php Applies master-user username formatting for SMTP connections.
lib/IMAP/IMAPClientFactory.php Applies master-user username formatting for IMAP connections.
lib/Migration/Version5007Date20260124120000.php Adds master_user and master_user_separator columns to mail_provisionings.
lib/Db/ProvisioningMapper.php Extends validation + entity population for master user/separator fields.
lib/Db/Provisioning.php Adds new provisioning entity properties + JSON serialization fields.
appinfo/info.xml Bumps app version to 5.7.0-rc.2.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +201 to +208
<input
id="mail-master-password"
v-model="masterPassword"
:disabled="loading"
type="password"
:required="masterPasswordEnabled">
<label for="mail-master-password"> {{ t('mail', 'Master password') }} </label>
</div>
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The master password input/label uses a static id/for ("mail-master-password"). This component uses setting.id to namespace other inputs; keeping this one static will create duplicate IDs when multiple provisioning settings are rendered and breaks label association / accessibility. Use an ID that includes setting.id (and match the label’s for).

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +107
$masterPasswordEnabled = (bool)($data['masterPasswordEnabled'] ?? false);
$masterPassword = $data['masterPassword'] ?? '';
$masterUser = $data['masterUser'] ?? '';
$masterUserSeparator = $data['masterUserSeparator'] ?? '';

if ($masterPasswordEnabled) {
if ($masterPassword === '') {
$exception->setField('masterPassword', false);
}
if ($masterUser !== '' && $masterUserSeparator === '') {
$exception->setField('masterUserSeparator', false);
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

masterUserSeparator is treated as required when masterUser is set, but the PR description calls the separator optional and other code paths default to * when unset. If the separator should be optional, adjust validation to treat empty as default * (and ideally normalize empty to null so factories can reliably fall back).

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +100
// Check for Dovecot master user authentication
$provisioningId = $account->getMailAccount()->getProvisioningId();
if ($provisioningId !== null) {
$provisioning = $this->provisioningMapper->get($provisioningId);
if ($provisioning !== null && !empty($provisioning->getMasterUser())) {
$separator = $provisioning->getMasterUserSeparator() ?? '*';
$user = $user . $separator . $provisioning->getMasterUser();
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly introduced Dovecot master-user behavior isn’t covered by tests in this file. Add a test case where the account has a provisioning with masterUser (and separator) set, and assert that the IMAP client is created with the expected combined username (including default * behavior when separator is unset/null).

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +69
// Check for Dovecot master user authentication
$provisioningId = $mailAccount->getProvisioningId();
if ($provisioningId !== null) {
$provisioning = $this->provisioningMapper->get($provisioningId);
if ($provisioning !== null && !empty($provisioning->getMasterUser())) {
$separator = $provisioning->getMasterUserSeparator() ?? '*';
$username = $username . $separator . $provisioning->getMasterUser();
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds an additional DB lookup ($this->provisioningMapper->get(...)) every time an SMTP transport is created for a provisioned account. If transports are created multiple times per request or during background jobs, consider caching provisionings by ID in the factory (or attaching provisioning data to the MailAccount) to avoid repeated queries.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +52
// Check for Dovecot master user authentication
$provisioningId = $account->getMailAccount()->getProvisioningId();
if ($provisioningId !== null) {
$provisioning = $this->provisioningMapper->get($provisioningId);
if ($provisioning !== null && !empty($provisioning->getMasterUser())) {
$separator = $provisioning->getMasterUserSeparator() ?? '*';
$user = $user . $separator . $provisioning->getMasterUser();
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly introduced Dovecot master-user behavior isn’t covered by tests. Consider adding an integration/unit test that sets a provisioning ID on the MailAccount, mocks ProvisioningMapper->get(...) to return a Provisioning with masterUser/separator, and asserts the created Sieve client uses the expected combined username.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +30
public function setup(): void {
parent::setUp();

$this->mapper = new ProvisioningMapper(
$this->createMock(IDBConnection::class),
new NullLogger(),
);
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHPUnit will not call setup(); the correct lifecycle hook is setUp(). As written, $this->mapper is never initialized and the first test will error when calling $this->mapper->validate(...). Rename to protected function setUp(): void (and keep the parent::setUp() call).

Copilot uses AI. Check for mistakes.
Comment on lines +229 to +236
<input
:id="'mail-master-user-separator' + setting.id"
v-model="masterUserSeparator"
:disabled="loading"
type="text"
maxlength="8"
:required="needsMasterUserSeparator">
</label>
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says the master-user separator is optional, and the preview defaults it to * when unset. However the UI marks it as required when masterUser is set, and the backend validation rejects an empty separator. Consider allowing an empty separator to mean “use default *” (and persist null), or update the PR description/UI to make the separator mandatory.

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +100
// Check for Dovecot master user authentication
$provisioningId = $account->getMailAccount()->getProvisioningId();
if ($provisioningId !== null) {
$provisioning = $this->provisioningMapper->get($provisioningId);
if ($provisioning !== null && !empty($provisioning->getMasterUser())) {
$separator = $provisioning->getMasterUserSeparator() ?? '*';
$user = $user . $separator . $provisioning->getMasterUser();
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds an extra DB lookup ($this->provisioningMapper->get(...)) every time an IMAP client is created for a provisioned account. If getClient is called frequently, this can become a noticeable overhead. Consider caching provisionings by ID in the factory (or passing the provisioning data along with the MailAccount) so the lookup happens at most once per provisioning per request.

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +100
// Check for Dovecot master user authentication
$provisioningId = $account->getMailAccount()->getProvisioningId();
if ($provisioningId !== null) {
$provisioning = $this->provisioningMapper->get($provisioningId);
if ($provisioning !== null && !empty($provisioning->getMasterUser())) {
$separator = $provisioning->getMasterUserSeparator() ?? '*';
$user = $user . $separator . $provisioning->getMasterUser();
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new master-user username formatting logic is duplicated across IMAP/SMTP/Sieve factories. To reduce drift (e.g., default separator handling, enablement checks), consider extracting this into a shared helper (e.g., on Provisioning or a small utility) and reuse it here.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +69
// Check for Dovecot master user authentication
$provisioningId = $mailAccount->getProvisioningId();
if ($provisioningId !== null) {
$provisioning = $this->provisioningMapper->get($provisioningId);
if ($provisioning !== null && !empty($provisioning->getMasterUser())) {
$separator = $provisioning->getMasterUserSeparator() ?? '*';
$username = $username . $separator . $provisioning->getMasterUser();
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly introduced Dovecot master-user behavior isn’t covered by unit tests. Consider adding a test that sets a provisioning ID on the MailAccount, mocks ProvisioningMapper->get(...) to return a Provisioning with masterUser/separator, and asserts the resulting transport params use the combined username.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants