<?php

declare(strict_types=1);

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

namespace OCA\Mail\IMAP;

use Horde_Imap_Client_Exception;
use Horde_Imap_Client_Exception_NoSupportExtension;
use Horde_Imap_Client_Socket;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IMemcache;
use OCP\IMemcacheTTL;
use function floor;

/**
 * "Decorator" around Horde's IMAP client to add auth error rate limiting.
 *
 * This is not a real decorator because the component to decorate doesn't have
 * an interface, making it hard to base a decorator on composition.
 * For simplicity the component is decorated by inheritance.
 */
class HordeImapClient extends Horde_Imap_Client_Socket {
	private ?IMemcache $rateLimiterCache = null;
	private ?ITimeFactory $timeFactory = null;
	private ?string $hash = null;

	public function __construct(
		array $params,
		private IMAPClientFactory $factory,
	) {
		parent::__construct($params);
	}

	public function enableRateLimiter(
		IMemcache $cache,
		string $hash,
		ITimeFactory $timeFactory,
	): void {
		$this->rateLimiterCache = $cache;
		$this->timeFactory = $timeFactory;
		$this->hash = $hash;
	}

	#[\Override]
	public function login() {
		$initiallyAutheticated = $this->_isAuthenticated;
		parent::login();

		if ($initiallyAutheticated === false && $this->capability->query('ID')) {
			try {
				$this->sendID();
				/* ID is queued - force sending the queued command. */
				$this->_sendCmd($this->_pipeline());
			} catch (Horde_Imap_Client_Exception_NoSupportExtension) {
				// Ignore if server doesn't support ID extension.
			}
		}
	}

	private const RATE_LIMIT_WINDOW = 3 * 60 * 60;

	protected function imapLogin() {
		$result = parent::_login();
		$this->factory->recordLogin($this->_params['hostspec']);
		return $result;
	}

	#[\Override]
	protected function _login() {
		if ($this->rateLimiterCache === null) {
			return $this->imapLogin();
		}

		$now = $this->timeFactory->getTime();
		$window = floor($now / self::RATE_LIMIT_WINDOW);
		$cacheKey = $this->hash . (string)$window;

		$counter = $this->rateLimiterCache->get($cacheKey);
		if ($counter !== null && $counter >= 3) {
			// Enough errors. Let's fail without involving IMAP
			throw new Horde_Imap_Client_Exception(
				'Too many auth attempts',
				Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
			);
		}

		try {
			return $this->imapLogin();
		} catch (Horde_Imap_Client_Exception $e) {
			if ($e->getCode() === Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED
				&& in_array($e->getMessage(), ['Authentication failed.', 'Mail server denied authentication.'], true)) {
				$this->rateLimiterCache->inc($cacheKey);
				if ($this->rateLimiterCache instanceof IMemcacheTTL) {
					$this->rateLimiterCache->setTTL($cacheKey, self::RATE_LIMIT_WINDOW);
				}
			}
			throw $e;
		}
	}
}
