Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/Db/Provisioning.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand All @@ -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');
Expand All @@ -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(),
Expand Down
11 changes: 11 additions & 0 deletions lib/Db/ProvisioningMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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'] ?? '');
Expand Down
16 changes: 15 additions & 1 deletion lib/IMAP/IMAPClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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,
Expand Down
52 changes: 52 additions & 0 deletions lib/Migration/Version5007Date20260124120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Mail\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* Add master_user and master_user_separator columns to mail_provisionings table
* for Dovecot Master User authentication support.
*
* @codeCoverageIgnore
*/
class Version5007Date20260124120000 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
#[\Override]
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
$schema = $schemaClosure();

$provisioningTable = $schema->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;
}
}
22 changes: 20 additions & 2 deletions lib/SMTP/SmtpClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

/**
Expand All @@ -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' => [
Expand Down
15 changes: 14 additions & 1 deletion lib/Sieve/SieveClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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 {
Expand Down
54 changes: 51 additions & 3 deletions src/components/settings/ProvisionPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
{{ t('mail', 'Email: {email}', { email }) }}<br>
{{
t('mail', 'IMAP: {user} on {host}:{port} ({ssl} encryption)', {
user: imapUser,
user: imapLoginUser,
host: imapHost,
port: imapPort,
ssl: imapSslMode,
})
}}<br>
{{
t('mail', 'SMTP: {user} on {host}:{port} ({ssl} encryption)', {
user: smtpUser,
user: smtpLoginUser,
host: smtpHost,
port: smtpPort,
ssl: smtpSslMode,
Expand All @@ -32,13 +32,21 @@
<span v-if="sieveEnabled">
{{
t('mail', 'Sieve: {user} on {host}:{port} ({ssl} encryption)', {
user: sieveUser,
user: sieveLoginUser,
host: sieveHost,
port: sievePort,
ssl: sieveSslMode,
})
}}<br>
</span>
<span v-if="hasMasterUser" class="master-user-info">
<br>
<em>{{ t('mail', 'Using Dovecot master user authentication') }}</em>
</span>
<span v-else-if="masterPasswordEnabled" class="master-password-info">
<br>
<em>{{ t('mail', 'Using static password for all users') }}</em>
</span>
</div>
</template>

Expand Down Expand Up @@ -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
},
},
}
</script>
Expand Down
Loading
Loading