diff --git a/lib/Db/Provisioning.php b/lib/Db/Provisioning.php
index 8d85c6d153..ee74c00840 100644
--- a/lib/Db/Provisioning.php
+++ b/lib/Db/Provisioning.php
@@ -39,6 +39,10 @@
* @method void setMasterPasswordEnabled(bool $masterPasswordEnabled)
* @method string|null getMasterPassword()
* @method void setMasterPassword(string $masterPassword)
+ * @method string|null getMasterUser()
+ * @method void setMasterUser(?string $masterUser)
+ * @method string|null getMasterUserSeparator()
+ * @method void setMasterUserSeparator(?string $masterUserSeparator)
* @method bool|null getSieveEnabled()
* @method void setSieveEnabled(bool $sieveEnabled)
* @method string|null getSieveHost()
@@ -72,6 +76,8 @@ class Provisioning extends Entity implements JsonSerializable {
protected $smtpSslMode;
protected $masterPasswordEnabled;
protected $masterPassword;
+ protected $masterUser;
+ protected $masterUserSeparator;
protected $sieveEnabled;
protected $sieveUser;
protected $sieveHost;
@@ -86,6 +92,8 @@ public function __construct() {
$this->addType('smtpPort', 'integer');
$this->addType('masterPasswordEnabled', 'boolean');
$this->addType('masterPassword', 'string');
+ $this->addType('masterUser', 'string');
+ $this->addType('masterUserSeparator', 'string');
$this->addType('sieveEnabled', 'boolean');
$this->addType('sievePort', 'integer');
$this->addType('ldapAliasesProvisioning', 'boolean');
@@ -108,6 +116,8 @@ public function jsonSerialize() {
'smtpSslMode' => $this->getSmtpSslMode(),
'masterPasswordEnabled' => $this->getMasterPasswordEnabled(),
'masterPassword' => !empty($this->getMasterPassword()) ? self::MASTER_PASSWORD_PLACEHOLDER : null,
+ 'masterUser' => !empty($this->getMasterUser()) ? self::MASTER_PASSWORD_PLACEHOLDER : null,
+ 'masterUserSeparator' => $this->getMasterUserSeparator() ?? '*',
'sieveEnabled' => $this->getSieveEnabled(),
'sieveUser' => $this->getSieveUser(),
'sieveHost' => $this->getSieveHost(),
diff --git a/lib/Db/ProvisioningMapper.php b/lib/Db/ProvisioningMapper.php
index 0486b84a30..312ee430e9 100644
--- a/lib/Db/ProvisioningMapper.php
+++ b/lib/Db/ProvisioningMapper.php
@@ -92,6 +92,13 @@ public function validate(array $data): Provisioning {
$exception->setField('ldapAliasesAttribute', false);
}
+ // Master password is required when master user is set
+ $masterUser = $data['masterUser'] ?? '';
+ $masterPasswordEnabled = (bool)($data['masterPasswordEnabled'] ?? false);
+ if (!empty($masterUser) && $masterUser !== Provisioning::MASTER_PASSWORD_PLACEHOLDER && !$masterPasswordEnabled) {
+ $exception->setField('masterPasswordEnabled', false);
+ }
+
if (!empty($exception->getFields())) {
throw $exception;
}
@@ -113,6 +120,10 @@ public function validate(array $data): Provisioning {
if (isset($data['masterPassword']) && $data['masterPassword'] !== Provisioning::MASTER_PASSWORD_PLACEHOLDER) {
$provisioning->setMasterPassword($data['masterPassword']);
}
+ if (isset($data['masterUser']) && $data['masterUser'] !== Provisioning::MASTER_PASSWORD_PLACEHOLDER) {
+ $provisioning->setMasterUser($data['masterUser']);
+ }
+ $provisioning->setMasterUserSeparator($data['masterUserSeparator'] ?? '*');
$provisioning->setSieveEnabled((bool)$data['sieveEnabled']);
$provisioning->setSieveHost($data['sieveHost'] ?? '');
diff --git a/lib/IMAP/IMAPClientFactory.php b/lib/IMAP/IMAPClientFactory.php
index 93f88d6040..5e37f5cab2 100644
--- a/lib/IMAP/IMAPClientFactory.php
+++ b/lib/IMAP/IMAPClientFactory.php
@@ -14,6 +14,7 @@
use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
use OCA\Mail\Cache\HordeCacheFactory;
+use OCA\Mail\Db\ProvisioningMapper;
use OCA\Mail\Events\BeforeImapClientCreated;
use OCA\Mail\Exception\ServiceException;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -41,19 +42,22 @@ class IMAPClientFactory {
private ITimeFactory $timeFactory;
private HordeCacheFactory $hordeCacheFactory;
+ private ProvisioningMapper $provisioningMapper;
public function __construct(ICrypto $crypto,
IConfig $config,
ICacheFactory $cacheFactory,
IEventDispatcher $eventDispatcher,
ITimeFactory $timeFactory,
- HordeCacheFactory $hordeCacheFactory) {
+ HordeCacheFactory $hordeCacheFactory,
+ ProvisioningMapper $provisioningMapper) {
$this->crypto = $crypto;
$this->config = $config;
$this->cacheFactory = $cacheFactory;
$this->eventDispatcher = $eventDispatcher;
$this->timeFactory = $timeFactory;
$this->hordeCacheFactory = $hordeCacheFactory;
+ $this->provisioningMapper = $provisioningMapper;
}
/**
@@ -85,6 +89,16 @@ public function getClient(Account $account, bool $useCache = true): Horde_Imap_C
$sslMode = false;
}
+ // 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();
+ }
+ }
+
$params = [
'username' => $user,
'password' => $decryptedPassword,
diff --git a/lib/Migration/Version5007Date20260124120000.php b/lib/Migration/Version5007Date20260124120000.php
new file mode 100644
index 0000000000..5826e86f8c
--- /dev/null
+++ b/lib/Migration/Version5007Date20260124120000.php
@@ -0,0 +1,52 @@
+getTable('mail_provisionings');
+ if (!$provisioningTable->hasColumn('master_user')) {
+ $provisioningTable->addColumn('master_user', Types::STRING, [
+ 'notnull' => false,
+ 'length' => 256,
+ ]);
+ }
+ if (!$provisioningTable->hasColumn('master_user_separator')) {
+ $provisioningTable->addColumn('master_user_separator', Types::STRING, [
+ 'notnull' => false,
+ 'length' => 8,
+ 'default' => '*',
+ ]);
+ }
+
+ return $schema;
+ }
+}
diff --git a/lib/SMTP/SmtpClientFactory.php b/lib/SMTP/SmtpClientFactory.php
index 00eed8cf3d..8dc1e398bf 100644
--- a/lib/SMTP/SmtpClientFactory.php
+++ b/lib/SMTP/SmtpClientFactory.php
@@ -14,6 +14,7 @@
use Horde_Mail_Transport_Smtphorde;
use Horde_Smtp_Password_Xoauth2;
use OCA\Mail\Account;
+use OCA\Mail\Db\ProvisioningMapper;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Support\HostNameFactory;
use OCP\IConfig;
@@ -29,12 +30,16 @@ class SmtpClientFactory {
/** @var HostNameFactory */
private $hostNameFactory;
+ private ProvisioningMapper $provisioningMapper;
+
public function __construct(IConfig $config,
ICrypto $crypto,
- HostNameFactory $hostNameFactory) {
+ HostNameFactory $hostNameFactory,
+ ProvisioningMapper $provisioningMapper) {
$this->config = $config;
$this->crypto = $crypto;
$this->hostNameFactory = $hostNameFactory;
+ $this->provisioningMapper = $provisioningMapper;
}
/**
@@ -50,12 +55,25 @@ public function create(Account $account): Horde_Mail_Transport {
$decryptedPassword = $this->crypto->decrypt($mailAccount->getOutboundPassword());
}
$security = $mailAccount->getOutboundSslMode();
+
+ $username = $mailAccount->getOutboundUser();
+
+ // 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();
+ }
+ }
+
$params = [
'localhost' => $this->hostNameFactory->getHostName(),
'host' => $mailAccount->getOutboundHost(),
'password' => $decryptedPassword,
'port' => $mailAccount->getOutboundPort(),
- 'username' => $mailAccount->getOutboundUser(),
+ 'username' => $username,
'secure' => $security === 'none' ? false : $security,
'timeout' => (int)$this->config->getSystemValue('app.mail.smtp.timeout', 20),
'context' => [
diff --git a/lib/Sieve/SieveClientFactory.php b/lib/Sieve/SieveClientFactory.php
index bd0fb0b4b6..b381b1ea8f 100644
--- a/lib/Sieve/SieveClientFactory.php
+++ b/lib/Sieve/SieveClientFactory.php
@@ -11,17 +11,20 @@
use Horde\ManageSieve;
use OCA\Mail\Account;
+use OCA\Mail\Db\ProvisioningMapper;
use OCP\IConfig;
use OCP\Security\ICrypto;
class SieveClientFactory {
private ICrypto $crypto;
private IConfig $config;
+ private ProvisioningMapper $provisioningMapper;
private array $cache = [];
- public function __construct(ICrypto $crypto, IConfig $config) {
+ public function __construct(ICrypto $crypto, IConfig $config, ProvisioningMapper $provisioningMapper) {
$this->crypto = $crypto;
$this->config = $config;
+ $this->provisioningMapper = $provisioningMapper;
}
/**
@@ -38,6 +41,16 @@ public function getClient(Account $account): ManageSieve {
$password = $account->getMailAccount()->getInboundPassword();
}
+ // 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();
+ }
+ }
+
if ($account->getDebug() || $this->config->getSystemValueBool('app.mail.debug')) {
$logFile = $this->config->getSystemValue('datadirectory') . '/mail-' . $account->getUserId() . '-' . $account->getId() . '-sieve.log';
} else {
diff --git a/src/components/settings/ProvisionPreview.vue b/src/components/settings/ProvisionPreview.vue
index 715f7edf61..e3ba6b5fb0 100644
--- a/src/components/settings/ProvisionPreview.vue
+++ b/src/components/settings/ProvisionPreview.vue
@@ -15,7 +15,7 @@
{{ t('mail', 'Email: {email}', { email }) }}
{{
t('mail', 'IMAP: {user} on {host}:{port} ({ssl} encryption)', {
- user: imapUser,
+ user: imapLoginUser,
host: imapHost,
port: imapPort,
ssl: imapSslMode,
@@ -23,7 +23,7 @@
}}
{{
t('mail', 'SMTP: {user} on {host}:{port} ({ssl} encryption)', {
- user: smtpUser,
+ user: smtpLoginUser,
host: smtpHost,
port: smtpPort,
ssl: smtpSslMode,
@@ -32,13 +32,21 @@
{{
t('mail', 'Sieve: {user} on {host}:{port} ({ssl} encryption)', {
- user: sieveUser,
+ user: sieveLoginUser,
host: sieveHost,
port: sievePort,
ssl: sieveSslMode,
})
}}
+
+
+
+ {{ t('mail', 'Using static password for all users') }}
+
@@ -117,6 +125,46 @@ export default {
sieveUser() {
return this.templates.sieveUser.replace('%USERID%', this.data.uid).replace('%EMAIL%', this.data.email)
},
+
+ masterPasswordEnabled() {
+ return this.templates.masterPasswordEnabled
+ },
+
+ masterUser() {
+ return this.templates.masterUser || ''
+ },
+
+ masterUserSeparator() {
+ return this.templates.masterUserSeparator || '*'
+ },
+
+ hasMasterUser() {
+ return this.masterUser && this.masterUser !== '********' && this.masterUser.length > 0
+ },
+
+ imapLoginUser() {
+ const baseUser = this.imapUser
+ if (this.hasMasterUser) {
+ return baseUser + this.masterUserSeparator + this.masterUser
+ }
+ return baseUser
+ },
+
+ smtpLoginUser() {
+ const baseUser = this.smtpUser
+ if (this.hasMasterUser) {
+ return baseUser + this.masterUserSeparator + this.masterUser
+ }
+ return baseUser
+ },
+
+ sieveLoginUser() {
+ const baseUser = this.sieveUser
+ if (this.hasMasterUser) {
+ return baseUser + this.masterUserSeparator + this.masterUser
+ }
+ return baseUser
+ },
},
}
diff --git a/src/components/settings/ProvisioningSettings.vue b/src/components/settings/ProvisioningSettings.vue
index ac7be23921..10b2282f7e 100644
--- a/src/components/settings/ProvisioningSettings.vue
+++ b/src/components/settings/ProvisioningSettings.vue
@@ -191,7 +191,8 @@
:id="'mail-master-password-enabled' + setting.id"
v-model="masterPasswordEnabled"
type="checkbox"
- class="checkbox">
+ class="checkbox"
+ :required="masterUser.length > 0 && masterUser !== '********'">
@@ -205,6 +206,36 @@
:required="masterPasswordEnabled">
+
+ {{ t('mail', 'When only master password is set, all users will authenticate with their normal username and this static password.') }} +
+{{ t('mail', 'When master user is set, authentication will use the Dovecot master user format: user{separator}masteruser with the master password.') }}
+{{ t('mail', 'The separator between the user and master user (default: *)') }}
+