Skip to content

Commit

Permalink
Merge pull request #10659 from nextcloud/feat/setup-check-connection-…
Browse files Browse the repository at this point in the history
…speed

feat: check connection performance of mail service
  • Loading branch information
SebastianKrupinski authored Feb 12, 2025
2 parents 4b1b6ca + 800e964 commit 41e082d
Show file tree
Hide file tree
Showing 7 changed files with 412 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
use OCA\Mail\Service\Search\MailSearch;
use OCA\Mail\Service\TrustedSenderService;
use OCA\Mail\Service\UserPreferenceService;
use OCA\Mail\SetupChecks\MailConnectionPerformance;
use OCA\Mail\SetupChecks\MailTransport;
use OCA\Mail\Vendor\Favicon\Favicon;
use OCP\AppFramework\App;
Expand Down Expand Up @@ -160,6 +161,7 @@ public function register(IRegistrationContext $context): void {
$context->registerNotifierService(Notifier::class);

$context->registerSetupCheck(MailTransport::class);
$context->registerSetupCheck(MailConnectionPerformance::class);

// bypass Horde Translation system
Horde_Translation::setHandler('Horde_Imap_Client', new HordeTranslationHandler());
Expand Down
20 changes: 20 additions & 0 deletions lib/Db/MailAccountMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,24 @@ public function getAllUserIdsWithAccounts(): array {

return $this->findEntities($query);
}

public function getRandomAccountIdsByImapHost(string $host, int $limit = 3): array {
$query = $this->db->getQueryBuilder();
$query->select('id')
->from($this->getTableName())
->where($query->expr()->eq('inbound_host', $query->createNamedParameter($host), IQueryBuilder::PARAM_STR))
->setMaxResults(1000);
$result = $query->executeQuery();
$ids = $result->fetchAll(\PDO::FETCH_COLUMN);
$result->closeCursor();
// Pick 3 random accounts or any available
if ($ids !== [] && count($ids) >= $limit) {
$rids = array_rand($ids, $limit);
if (!is_array($rids)) {
$rids = [$rids];
}
return array_intersect_key($ids, array_values($rids));
}
return $ids;
}
}
15 changes: 15 additions & 0 deletions lib/Db/ProvisioningMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,19 @@ public function get(int $id): ?Provisioning {
return null;
}
}

/**
* @since 4.2.0
*
* @return array<int,string>
*/
public function findUniqueImapHosts(): array {
$query = $this->db->getQueryBuilder();
$query->selectDistinct('imap_host')
->from('mail_provisionings');
$result = $query->executeQuery();
$data = $result->fetchAll(\PDO::FETCH_COLUMN);
$result->closeCursor();
return $data;
}
}
108 changes: 108 additions & 0 deletions lib/SetupChecks/MailConnectionPerformance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

declare(strict_types=1);

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

namespace OCA\Mail\SetupChecks;

use OCA\Mail\Account;
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Db\ProvisioningMapper;
use OCA\Mail\IMAP\FolderMapper;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCP\IL10N;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
use Psr\Log\LoggerInterface;
use Throwable;

class MailConnectionPerformance implements ISetupCheck {
public function __construct(
private IL10N $l10n,
private LoggerInterface $logger,
private ProvisioningMapper $provisioningMapper,
private MailAccountMapper $accountMapper,
private IMAPClientFactory $clientFactory,
private FolderMapper $folderMapper,
private MicroTime $microtime,
) {
}

public function getName(): string {
return $this->l10n->t('Mail connection performance');
}

public function getCategory(): string {
return 'mail';
}

public function run(): SetupResult {
// retrieve unique imap hosts for provisionings and abort if none exists
$hosts = $this->provisioningMapper->findUniqueImapHosts();
if (empty($hosts)) {
return SetupResult::success();
}
// retrieve random account ids for each host
$accounts = [];
foreach ($hosts as $host) {
$accounts[$host] = $this->accountMapper->getRandomAccountIdsByImapHost($host);
}
// test accounts
$tests = [];
foreach ($accounts as $host => $collection) {
foreach ($collection as $accountId) {
$account = new Account($this->accountMapper->findById((int)$accountId));
$client = $this->clientFactory->getClient($account);
try {
$tStart = $this->microtime->getNumeric();
// time login
$client->login();
$tLogin = $this->microtime->getNumeric();
// time operation
$list = $client->listMailboxes('*');
$status = $client->status(key($list));
$tOperation = $this->microtime->getNumeric();

$tests[$host][$accountId] = ['start' => $tStart, 'login' => $tLogin, 'operation' => $tOperation];
} catch (Throwable $e) {
$this->logger->warning('Error occurred while performing system check on mail account: ' . $account->getId());
} finally {
$client->close();
}
}
}
// calculate performance
$performance = [];
foreach ($tests as $host => $test) {
$tLogin = 0;
$tOperation = 0;
foreach ($test as $entry) {
[$start, $login, $operation] = array_values($entry);
$tLogin += ($login - $start);
$tOperation += ($operation - $login);
}
$performance[$host]['login'] = $tLogin / count($tests[$host]);
$performance[$host]['operation'] = $tOperation / count($tests[$host]);
}
// display performance test outcome
foreach ($performance as $host => $entry) {
[$login, $operation] = array_values($entry);
if ($login > 1) {
return SetupResult::warning(
$this->l10n->t('Slow mail service detected (%1$s) an attempt to connect to several accounts took an average of %2$s seconds per account', [$host, round($login, 3)])
);
}
if ($operation > 1) {
return SetupResult::warning(
$this->l10n->t('Slow mail service detected (%1$s) an attempt to perform a mail box list operation on several accounts took an average of %2$s seconds per account', [$host, round($operation, 3)])
);
}
}
return SetupResult::success();
}

}
18 changes: 18 additions & 0 deletions lib/SetupChecks/MicroTime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

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

namespace OCA\Mail\SetupChecks;

class MicroTime {

public function getNumeric(): float {
return (float)microtime(true);
}

}
22 changes: 22 additions & 0 deletions tests/Integration/Db/ProvisioningMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,26 @@ public function testGet() {
}
}
}

public function testFindUniqueImapHosts() {
$provisioning = new Provisioning();
$provisioning->setProvisioningDomain($this->data['provisioningDomain']);
$provisioning->setEmailTemplate($this->data['emailTemplate']);
$provisioning->setImapUser($this->data['imapUser']);
$provisioning->setImapHost($this->data['imapHost']);
$provisioning->setImapPort(42);
$provisioning->setImapSslMode($this->data['imapSslMode']);
$provisioning->setSmtpUser($this->data['smtpUser']);
$provisioning->setSmtpHost($this->data['smtpHost']);
$provisioning->setSmtpPort(24);
$provisioning->setSmtpSslMode($this->data['smtpSslMode']);
$provisioning->setSieveEnabled($this->data['sieveEnabled']);
$provisioning = $this->mapper->insert($provisioning);

$hosts = $this->mapper->findUniqueImapHosts();

$this->assertIsArray($hosts);
$this->assertNotEmpty($hosts);
$this->assertEquals($this->data['imapHost'], $hosts[0]);
}
}
Loading

0 comments on commit 41e082d

Please sign in to comment.