Skip to content

Commit 999d405

Browse files
authored
Merge pull request #12489 from nextcloud/fix/cache/expiring-imap-ratelimit-key
fix(cache): expire the IMAP ratelimit keys via TTL
2 parents 0ba82ef + f1ac885 commit 999d405

File tree

2 files changed

+77
-3
lines changed

2 files changed

+77
-3
lines changed

lib/IMAP/HordeImapClient.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Horde_Imap_Client_Socket;
1515
use OCP\AppFramework\Utility\ITimeFactory;
1616
use OCP\IMemcache;
17+
use OCP\IMemcacheTTL;
1718
use function floor;
1819

1920
/**
@@ -53,14 +54,20 @@ public function login() {
5354
}
5455
}
5556

57+
private const RATE_LIMIT_WINDOW = 3 * 60 * 60;
58+
59+
protected function imapLogin() {
60+
return parent::_login();
61+
}
62+
5663
#[\Override]
5764
protected function _login() {
5865
if ($this->rateLimiterCache === null) {
59-
return parent::_login();
66+
return $this->imapLogin();
6067
}
6168

6269
$now = $this->timeFactory->getTime();
63-
$window = floor($now / (3 * 60 * 60));
70+
$window = floor($now / self::RATE_LIMIT_WINDOW);
6471
$cacheKey = $this->hash . $window;
6572

6673
$counter = $this->rateLimiterCache->get($cacheKey);
@@ -73,11 +80,14 @@ protected function _login() {
7380
}
7481

7582
try {
76-
return parent::_login();
83+
return $this->imapLogin();
7784
} catch (Horde_Imap_Client_Exception $e) {
7885
if ($e->getCode() === Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
7986
&& $e->getMessage() === 'Authentication failed.') {
8087
$this->rateLimiterCache->inc($cacheKey);
88+
if ($this->rateLimiterCache instanceof IMemcacheTTL) {
89+
$this->rateLimiterCache->setTTL($cacheKey, self::RATE_LIMIT_WINDOW);
90+
}
8191
}
8292
throw $e;
8393
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace Unit\IMAP;
11+
12+
use ChristophWurst\Nextcloud\Testing\TestCase;
13+
use Horde_Imap_Client_Exception;
14+
use OCA\Mail\IMAP\HordeImapClient;
15+
use OCP\AppFramework\Utility\ITimeFactory;
16+
use OCP\IMemcache;
17+
use OCP\IMemcacheTTL;
18+
use PHPUnit\Framework\MockObject\MockObject;
19+
20+
interface IMemcacheWithTTL extends IMemcache, IMemcacheTTL {
21+
}
22+
23+
/**
24+
* Testable subclass that stubs out the real IMAP connection.
25+
*/
26+
class TestableHordeImapClient extends HordeImapClient {
27+
public function __construct() {
28+
// Skip Horde constructor — we only test rate limiter logic.
29+
}
30+
31+
protected function imapLogin() {
32+
throw new Horde_Imap_Client_Exception(
33+
'Authentication failed.',
34+
Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED,
35+
);
36+
}
37+
}
38+
39+
class HordeImapClientTest extends TestCase {
40+
private IMemcacheWithTTL|MockObject $cache;
41+
private ITimeFactory|MockObject $timeFactory;
42+
private TestableHordeImapClient $client;
43+
44+
protected function setUp(): void {
45+
parent::setUp();
46+
47+
$this->cache = $this->createMock(IMemcacheWithTTL::class);
48+
$this->timeFactory = $this->createMock(ITimeFactory::class);
49+
50+
$this->client = new TestableHordeImapClient();
51+
$this->client->enableRateLimiter($this->cache, 'testhash', $this->timeFactory);
52+
}
53+
54+
public function testSetsTtlOnAuthFailure(): void {
55+
$this->timeFactory->method('getTime')->willReturn(100000);
56+
$expectedKey = 'testhash9';
57+
$this->cache->method('get')->with($expectedKey)->willReturn(null);
58+
$this->cache->expects($this->once())->method('inc')->with($expectedKey);
59+
$this->cache->expects($this->once())->method('setTTL')->with($expectedKey, 3 * 60 * 60);
60+
61+
$this->expectException(Horde_Imap_Client_Exception::class);
62+
$this->client->login();
63+
}
64+
}

0 commit comments

Comments
 (0)