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
24 changes: 21 additions & 3 deletions lib/Command/DebugAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

namespace OCA\Mail\Command;

use OCA\Mail\Account;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Support\DebugLogPathFactory;
use OCP\AppFramework\Db\DoesNotExistException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
Expand All @@ -26,6 +28,7 @@ final class DebugAccount extends Command {
public function __construct(
private AccountService $accountService,
private LoggerInterface $logger,
private DebugLogPathFactory $debugLogPathFactory,
) {
parent::__construct();
}
Expand All @@ -48,7 +51,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$debug = false;

try {
$account = $this->accountService->findById($accountId)->getMailAccount();
$mailAccount = $this->accountService->findById($accountId)->getMailAccount();
} catch (DoesNotExistException $e) {
$output->writeln("<error>Account $accountId does not exist</error>");
return 1;
Expand All @@ -63,8 +66,23 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$debug = true;
}

$account->setDebug($debug);
$this->accountService->save($account);
$mailAccount->setDebug($debug);
$this->accountService->save($mailAccount);

$account = new Account($mailAccount);

if ($debug) {
$output->writeln("<info>Debug mode enabled for account $accountId</info>");
} else {
$output->writeln("<info>Debug mode disabled for account $accountId</info>");
if ($this->debugLogPathFactory->isActive($account)) {
$output->writeln('<comment>Note: the system-wide "app.mail.debug" config value is enabled, so debug logging for this account is still active.</comment>');
}
$output->writeln('Existing log files are not deleted automatically:');
}
$output->writeln('IMAP log: ' . $this->debugLogPathFactory->buildPath($account, 'imap'));
$output->writeln('SMTP log: ' . $this->debugLogPathFactory->buildPath($account, 'smtp'));
$output->writeln('Sieve log: ' . $this->debugLogPathFactory->buildPath($account, 'sieve'));

return 0;
}
Expand Down
8 changes: 5 additions & 3 deletions lib/IMAP/IMAPClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use OCA\Mail\Cache\HordeCacheFactory;
use OCA\Mail\Events\BeforeImapClientCreated;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Support\DebugLogPathFactory;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICacheFactory;
Expand Down Expand Up @@ -51,6 +52,7 @@ public function __construct(
IEventDispatcher $eventDispatcher,
ITimeFactory $timeFactory,
private HordeCacheFactory $hordeCacheFactory,
private DebugLogPathFactory $debugLogPathFactory,
) {
$this->crypto = $crypto;
$this->config = $config;
Expand Down Expand Up @@ -132,9 +134,9 @@ public function getClient(Account $account, bool $useCache = true): Horde_Imap_C
'backend' => $this->hordeCacheFactory->newCache($account),
];
}
if ($account->getMailAccount()->getDebug() || $this->config->getSystemValueBool('app.mail.debug')) {
$fn = "mail-{$account->getUserId()}-{$account->getId()}-imap.log";
$params['debug'] = $this->config->getSystemValue('datadirectory') . '/' . $fn;
$debugLogPath = $this->debugLogPathFactory->getPath($account, 'imap');
if ($debugLogPath !== null) {
$params['debug'] = $debugLogPath;
}

$client = new HordeImapClient($params, $this);
Expand Down
8 changes: 5 additions & 3 deletions lib/SMTP/SmtpClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Horde_Smtp_Password_Xoauth2;
use OCA\Mail\Account;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Support\DebugLogPathFactory;
use OCA\Mail\Support\HostNameFactory;
use OCP\IConfig;
use OCP\Security\ICrypto;
Expand All @@ -30,6 +31,7 @@ public function __construct(
IConfig $config,
ICrypto $crypto,
private HostNameFactory $hostNameFactory,
private DebugLogPathFactory $debugLogPathFactory,
) {
$this->config = $config;
$this->crypto = $crypto;
Expand Down Expand Up @@ -80,9 +82,9 @@ public function create(Account $account): Horde_Mail_Transport {
$decryptedAccessToken,
);
}
if ($account->getMailAccount()->getDebug() || $this->config->getSystemValueBool('app.mail.debug')) {
$fn = "mail-{$account->getUserId()}-{$account->getId()}-smtp.log";
$params['debug'] = $this->config->getSystemValue('datadirectory') . '/' . $fn;
$debugLogPath = $this->debugLogPathFactory->getPath($account, 'smtp');
if ($debugLogPath !== null) {
$params['debug'] = $debugLogPath;
}
return new Horde_Mail_Transport_Smtphorde($params);
}
Expand Down
13 changes: 7 additions & 6 deletions lib/Sieve/SieveClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use Horde\ManageSieve;
use OCA\Mail\Account;
use OCA\Mail\Support\DebugLogPathFactory;
use OCP\IConfig;
use OCP\Security\ICrypto;

Expand All @@ -19,7 +20,11 @@ class SieveClientFactory {
private IConfig $config;
private array $cache = [];

public function __construct(ICrypto $crypto, IConfig $config) {
public function __construct(
ICrypto $crypto,
IConfig $config,
private DebugLogPathFactory $debugLogPathFactory,
) {
$this->crypto = $crypto;
$this->config = $config;
}
Expand All @@ -38,11 +43,7 @@ public function getClient(Account $account): ManageSieve {
$password = $account->getMailAccount()->getInboundPassword();
}

if ($account->getMailAccount()->getDebug() || $this->config->getSystemValueBool('app.mail.debug')) {
$logFile = $this->config->getSystemValue('datadirectory') . "/mail-{$account->getUserId()}-{$account->getId()}-sieve.log";
} else {
$logFile = null;
}
$logFile = $this->debugLogPathFactory->getPath($account, 'sieve');

$this->cache[$account->getId()] = $this->createClient(
$account->getMailAccount()->getSieveHost(),
Expand Down
47 changes: 47 additions & 0 deletions lib/Support/DebugLogPathFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

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

namespace OCA\Mail\Support;

use OCA\Mail\Account;
use OCP\IConfig;

class DebugLogPathFactory {
public function __construct(
private IConfig $config,
) {
}

/**
* Whether IMAP/SMTP/Sieve debug logging is active for this account, either
* because it was enabled for the account itself or system-wide.
*/
public function isActive(Account $account): bool {
return $account->getMailAccount()->getDebug() || $this->config->getSystemValueBool('app.mail.debug');
}

/**
* @return string|null the log file path, or null if debug logging is not active
*/
public function getPath(Account $account, string $protocol): ?string {
return $this->isActive($account) ? $this->buildPath($account, $protocol) : null;
}

/**
* Build the log file path regardless of whether debug logging is currently active.
*
* The account might not have been persisted yet (e.g. during the connectivity
* check when setting up a new account), in which case its id is not known yet.
*/
public function buildPath(Account $account, string $protocol): string {
$id = $account->getId() ?? 'new';
$fn = "mail-{$account->getUserId()}-{$id}-{$protocol}.log";
return $this->config->getSystemValue('datadirectory') . '/' . $fn;
}
}
2 changes: 2 additions & 0 deletions tests/Integration/Framework/Caching.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use OC\Memcache\Factory;
use OCA\Mail\Cache\HordeCacheFactory;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Support\DebugLogPathFactory;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
Expand Down Expand Up @@ -60,6 +61,7 @@ public static function getImapClientFactoryAndConfiguredCacheFactory(?ICrypto $c
Server::get(IEventDispatcher::class),
Server::get(ITimeFactory::class),
Server::get(HordeCacheFactory::class),
new DebugLogPathFactory($config),
);
return [$imapClient, $cacheFactory];
}
Expand Down
2 changes: 2 additions & 0 deletions tests/Integration/IMAP/IMAPClientFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use OCA\Mail\Db\MailAccount;
use OCA\Mail\IMAP\HordeImapClient;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Support\DebugLogPathFactory;
use OCA\Mail\Tests\Integration\Framework\Caching;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
Expand Down Expand Up @@ -62,6 +63,7 @@ protected function setUp(): void {
$this->eventDispatcher,
$this->timeFactory,
$this->hordeCacheFactory,
new DebugLogPathFactory($this->config),
);
}

Expand Down
3 changes: 2 additions & 1 deletion tests/Integration/Sieve/SieveClientFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use OCA\Mail\Account;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Sieve\SieveClientFactory;
use OCA\Mail\Support\DebugLogPathFactory;
use OCP\IConfig;
use OCP\Security\ICrypto;
use OCP\Server;
Expand Down Expand Up @@ -46,7 +47,7 @@ protected function setUp(): void {
['app.mail.debug', false, false],
]);

$this->factory = new SieveClientFactory($this->crypto, $this->config);
$this->factory = new SieveClientFactory($this->crypto, $this->config, new DebugLogPathFactory($this->config));
}

/**
Expand Down
137 changes: 137 additions & 0 deletions tests/Unit/Command/DebugAccountTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

declare(strict_types=1);

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

namespace OCA\Mail\Tests\Unit\Command;

use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\Command\DebugAccount;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Support\DebugLogPathFactory;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Tester\CommandTester;

class DebugAccountTest extends TestCase {
private AccountService|\PHPUnit\Framework\MockObject\MockObject $accountService;
private IConfig|\PHPUnit\Framework\MockObject\MockObject $config;
private DebugAccount $command;

protected function setUp(): void {
parent::setUp();

$this->accountService = $this->createMock(AccountService::class);
$this->config = $this->createMock(IConfig::class);
$this->config->method('getSystemValue')
->with('datadirectory')
->willReturn('/data');
$this->config->method('getSystemValueBool')
->with('app.mail.debug')
->willReturn(false);

$this->command = new DebugAccount(
$this->accountService,
$this->createMock(LoggerInterface::class),
new DebugLogPathFactory($this->config),
);
}

private function mockAccount(): MailAccount {
$mailAccount = new MailAccount();
$mailAccount->setId(1);
$mailAccount->setUserId('bob');

$this->accountService->method('findById')
->with(1)
->willReturn(new Account($mailAccount));

return $mailAccount;
}

public function testEnablesDebugAndPrintsLogPaths(): void {
$mailAccount = $this->mockAccount();
$this->accountService->expects($this->once())
->method('save')
->with($this->callback(fn (MailAccount $a) => $a->getDebug() === true));

$tester = new CommandTester($this->command);
$exitCode = $tester->execute(['account-id' => '1', '--on' => true]);

$this->assertSame(0, $exitCode);
$display = $tester->getDisplay();
$this->assertStringContainsString('Debug mode enabled', $display);
$this->assertStringContainsString('/data/mail-bob-1-imap.log', $display);
$this->assertStringContainsString('/data/mail-bob-1-smtp.log', $display);
$this->assertStringContainsString('/data/mail-bob-1-sieve.log', $display);
}

public function testDisablesDebugAndPrintsLogPathsWithoutDeletingThem(): void {
$this->mockAccount();
$this->accountService->expects($this->once())
->method('save')
->with($this->callback(fn (MailAccount $a) => $a->getDebug() === false));

$tester = new CommandTester($this->command);
$exitCode = $tester->execute(['account-id' => '1', '--off' => true]);

$this->assertSame(0, $exitCode);
$display = $tester->getDisplay();
$this->assertStringContainsString('Debug mode disabled', $display);
$this->assertStringContainsString('not deleted automatically', $display);
$this->assertStringNotContainsString('still active', $display);
$this->assertStringContainsString('/data/mail-bob-1-imap.log', $display);
$this->assertStringContainsString('/data/mail-bob-1-smtp.log', $display);
$this->assertStringContainsString('/data/mail-bob-1-sieve.log', $display);
}

public function testWarnsWhenDisablingButSystemWideDebugIsStillOn(): void {
$this->config = $this->createMock(IConfig::class);
$this->config->method('getSystemValue')
->with('datadirectory')
->willReturn('/data');
$this->config->method('getSystemValueBool')
->with('app.mail.debug')
->willReturn(true);
$this->command = new DebugAccount(
$this->accountService,
$this->createMock(LoggerInterface::class),
new DebugLogPathFactory($this->config),
);
$this->mockAccount();

$tester = new CommandTester($this->command);
$exitCode = $tester->execute(['account-id' => '1', '--off' => true]);

$this->assertSame(0, $exitCode);
$this->assertStringContainsString('still active', $tester->getDisplay());
}

public function testRejectsOnAndOffTogether(): void {
$this->mockAccount();
$this->accountService->expects($this->never())->method('save');

$tester = new CommandTester($this->command);
$exitCode = $tester->execute(['account-id' => '1', '--on' => true, '--off' => true]);

$this->assertSame(1, $exitCode);
}

public function testFailsWhenAccountDoesNotExist(): void {
$this->accountService->method('findById')
->with(1)
->willThrowException(new DoesNotExistException('not found'));

$tester = new CommandTester($this->command);
$exitCode = $tester->execute(['account-id' => '1']);

$this->assertSame(1, $exitCode);
}
}
Loading
Loading