Skip to content

Commit e846847

Browse files
feat: check connection performance of mail service
Signed-off-by: SebastianKrupinski <[email protected]>
1 parent 4b1b6ca commit e846847

7 files changed

+410
-0
lines changed

lib/AppInfo/Application.php

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
use OCA\Mail\Service\Search\MailSearch;
6767
use OCA\Mail\Service\TrustedSenderService;
6868
use OCA\Mail\Service\UserPreferenceService;
69+
use OCA\Mail\SetupChecks\MailConnectionPerformance;
6970
use OCA\Mail\SetupChecks\MailTransport;
7071
use OCA\Mail\Vendor\Favicon\Favicon;
7172
use OCP\AppFramework\App;
@@ -160,6 +161,7 @@ public function register(IRegistrationContext $context): void {
160161
$context->registerNotifierService(Notifier::class);
161162

162163
$context->registerSetupCheck(MailTransport::class);
164+
$context->registerSetupCheck(MailConnectionPerformance::class);
163165

164166
// bypass Horde Translation system
165167
Horde_Translation::setHandler('Horde_Imap_Client', new HordeTranslationHandler());

lib/Db/MailAccountMapper.php

+17
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,21 @@ public function getAllUserIdsWithAccounts(): array {
234234

235235
return $this->findEntities($query);
236236
}
237+
238+
public function getRandomAccountIdsByImapHost(string $host, int $limit = 3): array {
239+
$query = $this->db->getQueryBuilder();
240+
$query->select('id')
241+
->from($this->getTableName())
242+
->where($query->expr()->eq('inbound_host', $query->createNamedParameter($host), IQueryBuilder::PARAM_STR))
243+
->setMaxResults(1000);
244+
$result = $query->executeQuery();
245+
$ids = $result->fetchAll(\PDO::FETCH_COLUMN);
246+
$result->closeCursor();
247+
// Pick 3 random accounts or any available
248+
if ($ids !== [] && count($ids) >= $limit) {
249+
$rids = array_rand($ids, $limit);
250+
return array_intersect_key($ids, array_values($rids));
251+
}
252+
return $ids;
253+
}
237254
}

lib/Db/ProvisioningMapper.php

+15
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,19 @@ public function get(int $id): ?Provisioning {
138138
return null;
139139
}
140140
}
141+
142+
/**
143+
* @since 4.2.0
144+
*
145+
* @return array<int,string>
146+
*/
147+
public function findUniqueImapHosts(): array {
148+
$query = $this->db->getQueryBuilder();
149+
$query->selectDistinct('imap_host')
150+
->from('mail_provisionings');
151+
$result = $query->executeQuery();
152+
$data = $result->fetchAll(\PDO::FETCH_COLUMN);
153+
$result->closeCursor();
154+
return $data;
155+
}
141156
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Mail\SetupChecks;
11+
12+
use OCA\Mail\Account;
13+
use OCA\Mail\Db\MailAccountMapper;
14+
use OCA\Mail\Db\ProvisioningMapper;
15+
use OCA\Mail\IMAP\FolderMapper;
16+
use OCA\Mail\IMAP\IMAPClientFactory;
17+
use OCP\IL10N;
18+
use OCP\SetupCheck\ISetupCheck;
19+
use OCP\SetupCheck\SetupResult;
20+
use Psr\Log\LoggerInterface;
21+
use Throwable;
22+
23+
class MailConnectionPerformance implements ISetupCheck {
24+
public function __construct(
25+
private IL10N $l10n,
26+
private LoggerInterface $logger,
27+
private ProvisioningMapper $provisioningMapper,
28+
private MailAccountMapper $accountMapper,
29+
private IMAPClientFactory $clientFactory,
30+
private FolderMapper $folderMapper,
31+
private MicroTime $microtime,
32+
) {
33+
}
34+
35+
public function getName(): string {
36+
return $this->l10n->t('Mail connection performance');
37+
}
38+
39+
public function getCategory(): string {
40+
return 'mail';
41+
}
42+
43+
public function run(): SetupResult {
44+
// retrieve unique imap hosts for provisionings and abort if none exists
45+
$hosts = $this->provisioningMapper->findUniqueImapHosts();
46+
if (empty($hosts)) {
47+
return SetupResult::success();
48+
}
49+
// retrieve random account ids for each host
50+
$accounts = [];
51+
foreach ($hosts as $host) {
52+
$accounts[$host] = $this->accountMapper->getRandomAccountIdsByImapHost($host);
53+
}
54+
// test accounts
55+
$tests = [];
56+
foreach ($accounts as $host => $collection) {
57+
foreach ($collection as $accountId) {
58+
$account = new Account($this->accountMapper->findById((int)$accountId));
59+
$client = $this->clientFactory->getClient($account);
60+
try {
61+
$tStart = $this->microtime->get(true);
62+
// time login
63+
$client->login();
64+
$tLogin = $this->microtime->get(true);
65+
// time operation
66+
$list = $client->listMailboxes('*');
67+
$status = $client->status(key($list));
68+
$tOperation = $this->microtime->get(true);
69+
70+
$tests[$host][$accountId] = ['start' => $tStart, 'login' => $tLogin, 'operation' => $tOperation];
71+
} catch (Throwable $e) {
72+
$this->logger->warning('Error occurred while performing system check on mail account: ' . $account->getId());
73+
} finally {
74+
$client->close();
75+
}
76+
}
77+
}
78+
// calculate performance
79+
$performance = [];
80+
foreach ($tests as $host => $test) {
81+
$tLogin = 0;
82+
$tOperation = 0;
83+
foreach ($test as $entry) {
84+
[$start, $login, $operation] = array_values($entry);
85+
$tLogin += ($login - $start);
86+
$tOperation += ($operation - $login);
87+
}
88+
$performance[$host]['login'] = $tLogin / count($tests[$host]);
89+
$performance[$host]['operation'] = $tOperation / count($tests[$host]);
90+
}
91+
// display performance test outcome
92+
foreach ($performance as $host => $entry) {
93+
[$login, $operation] = array_values($entry);
94+
if ($login > 1) {
95+
return SetupResult::warning(
96+
$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)])
97+
);
98+
}
99+
if ($operation > 1) {
100+
return SetupResult::warning(
101+
$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)])
102+
);
103+
}
104+
}
105+
return SetupResult::success();
106+
}
107+
108+
}

lib/SetupChecks/MicroTime.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Mail\SetupChecks;
11+
12+
class MicroTime {
13+
14+
public function get(bool $asFloat = false): string|float {
15+
return microtime($asFloat);
16+
}
17+
18+
}

tests/Integration/Db/ProvisioningMapperTest.php

+22
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,26 @@ public function testGet() {
180180
}
181181
}
182182
}
183+
184+
public function testFindUniqueImapHosts() {
185+
$provisioning = new Provisioning();
186+
$provisioning->setProvisioningDomain($this->data['provisioningDomain']);
187+
$provisioning->setEmailTemplate($this->data['emailTemplate']);
188+
$provisioning->setImapUser($this->data['imapUser']);
189+
$provisioning->setImapHost($this->data['imapHost']);
190+
$provisioning->setImapPort(42);
191+
$provisioning->setImapSslMode($this->data['imapSslMode']);
192+
$provisioning->setSmtpUser($this->data['smtpUser']);
193+
$provisioning->setSmtpHost($this->data['smtpHost']);
194+
$provisioning->setSmtpPort(24);
195+
$provisioning->setSmtpSslMode($this->data['smtpSslMode']);
196+
$provisioning->setSieveEnabled($this->data['sieveEnabled']);
197+
$provisioning = $this->mapper->insert($provisioning);
198+
199+
$hosts = $this->mapper->findUniqueImapHosts();
200+
201+
$this->assertIsArray($hosts);
202+
$this->assertNotEmpty($hosts);
203+
$this->assertEquals($this->data['imapHost'], $hosts[0]);
204+
}
183205
}

0 commit comments

Comments
 (0)