Skip to content

Commit

Permalink
feat: apply personal out-of-office data to the auto responder
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
  • Loading branch information
st3iny committed Nov 21, 2023
1 parent 7bb191a commit a5fe9c2
Show file tree
Hide file tree
Showing 32 changed files with 2,019 additions and 609 deletions.
16 changes: 15 additions & 1 deletion appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,21 @@
'url' => '/api/drafts/move/{id}',
'verb' => 'POST',
],

[
'name' => 'outOfOffice#getState',
'url' => '/api/out-of-office/{accountId}',
'verb' => 'GET',
],
[
'name' => 'outOfOffice#update',
'url' => '/api/out-of-office/{accountId}',
'verb' => 'POST',
],
[
'name' => 'outOfOffice#followSystem',
'url' => '/api/out-of-office/{accountId}/follow-system',
'verb' => 'POST',
],
],
'resources' => [
'accounts' => ['url' => '/api/accounts'],
Expand Down
10 changes: 10 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\Listener\MoveJunkListener;
use OCA\Mail\Listener\NewMessageClassificationListener;
use OCA\Mail\Listener\OauthTokenRefreshListener;
use OCA\Mail\Listener\OutOfOfficeListener;
use OCA\Mail\Listener\SaveSentMessageListener;
use OCA\Mail\Listener\SpamReportListener;
use OCA\Mail\Listener\UserDeletedListener;
Expand All @@ -88,6 +89,8 @@
use OCP\Dashboard\IAPIWidgetV2;
use OCP\IServerContainer;
use OCP\Search\IFilteringProvider;
use OCP\User\Events\OutOfOfficeEndedEvent;
use OCP\User\Events\OutOfOfficeStartedEvent;
use OCP\User\Events\UserDeletedEvent;
use OCP\Util;
use Psr\Container\ContainerInterface;
Expand Down Expand Up @@ -142,6 +145,13 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(SynchronizationEvent::class, AccountSynchronizedThreadUpdaterListener::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);

// TODO: drop condition if nextcloud < 28 is not supported anymore
if (class_exists(OutOfOfficeStartedEvent::class)
&& class_exists(OutOfOfficeEndedEvent::class)) {
$context->registerEventListener(OutOfOfficeStartedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeEndedEvent::class, OutOfOfficeListener::class);
}

$context->registerMiddleWare(ErrorMiddleware::class);
$context->registerMiddleWare(ProvisioningMiddleware::class);

Expand Down
163 changes: 163 additions & 0 deletions lib/Controller/OutOfOfficeController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
*
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Mail\Controller;

use DateTimeImmutable;
use OCA\Mail\AppInfo\Application;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Http\JsonResponse;
use OCA\Mail\Http\TrapError;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\OutOfOffice\OutOfOfficeState;
use OCA\Mail\Service\OutOfOfficeService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IRequest;
use OCP\IUserSession;
use OCP\User\IAvailabilityCoordinator;

class OutOfOfficeController extends Controller {
public function __construct(
IRequest $request,
private ?string $userId,
private IUserSession $userSession,
private AccountService $accountService,
private OutOfOfficeService $outOfOfficeService,
private IAvailabilityCoordinator $availabilityCoordinator,
private ITimeFactory $timeFactory,
) {
parent::__construct(Application::APP_ID, $request);
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*/
#[TrapError]
public function getState(int $accountId): JsonResponse {
if ($this->userId === null) {
return JsonResponse::fail([], Http::STATUS_FORBIDDEN);
}

$account = $this->accountService->findById($accountId);
if ($account->getUserId() !== $this->userId) {
return JsonResponse::fail([], Http::STATUS_NOT_FOUND);
}

$state = $this->outOfOfficeService->parseState($account->getMailAccount());
return JsonResponse::success($state);
}

/**
* @NoAdminRequired
*/
#[TrapError]
public function followSystem(int $accountId) {
$user = $this->userSession->getUser();
if ($user === null) {
return JsonResponse::fail([], Http::STATUS_FORBIDDEN);
}

$account = $this->accountService->findById($accountId);
if ($account->getUserId() !== $user->getUID()) {
return JsonResponse::fail([], Http::STATUS_NOT_FOUND);
}

$mailAccount = $account->getMailAccount();
if (!$mailAccount->getOutOfOfficeFollowsSystem()) {
$mailAccount->setOutOfOfficeFollowsSystem(true);
$this->accountService->update($mailAccount);
}

$state = null;
$now = $this->timeFactory->now()->getTimestamp();
$currentOutOfOfficeData = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
if ($currentOutOfOfficeData !== null
&& $currentOutOfOfficeData->getStartDate() <= $now
&& $currentOutOfOfficeData->getEndDate() > $now) {
// In the middle of a running absence => enable auto responder
$state = new OutOfOfficeState(
true,
new DateTimeImmutable("@" . $currentOutOfOfficeData->getStartDate()),
new DateTimeImmutable("@" . $currentOutOfOfficeData->getEndDate()),
$currentOutOfOfficeData->getShortMessage(),
$currentOutOfOfficeData->getMessage(),
);
$this->outOfOfficeService->update($mailAccount, $state);
} else {
// Absence has not yet started or has already ended => disable auto responder
$this->outOfOfficeService->disable($mailAccount);
}

return JsonResponse::success($state);
}

/**
* @NoAdminRequired
*/
#[TrapError]
public function update(
int $accountId,
bool $enabled,
?string $start,
?string $end,
string $subject,
string $message,
): JsonResponse {
if ($this->userId === null) {
return JsonResponse::fail([], Http::STATUS_FORBIDDEN);
}

$account = $this->accountService->findById($accountId);
if ($account->getUserId() !== $this->userId) {
return JsonResponse::fail([], Http::STATUS_NOT_FOUND);
}

if ($enabled && $start === null) {
throw new ServiceException("Missing start date");
}

$mailAccount = $account->getMailAccount();
if ($mailAccount->getOutOfOfficeFollowsSystem()) {
$mailAccount->setOutOfOfficeFollowsSystem(false);
$this->accountService->update($mailAccount);
}

$state = new OutOfOfficeState(
$enabled,
$start ? new DateTimeImmutable($start) : null,
$end ? new DateTimeImmutable($end) : null,
$subject,
$message,
);
$this->outOfOfficeService->update($mailAccount, $state);

$newState = $this->outOfOfficeService->parseState($mailAccount);
return JsonResponse::success($newState);
}
}
22 changes: 20 additions & 2 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\User\IAvailabilityCoordinator;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Throwable;
use function class_exists;
Expand All @@ -77,6 +80,7 @@ class PageController extends Controller {
private SmimeService $smimeService;
private AiIntegrationsService $aiIntegrationsService;
private IUserManager $userManager;
private ?IAvailabilityCoordinator $availabilityCoordinator;

public function __construct(string $appName,
IRequest $request,
Expand All @@ -96,7 +100,8 @@ public function __construct(string $appName,
ICredentialStore $credentialStore,
SmimeService $smimeService,
AiIntegrationsService $aiIntegrationsService,
IUserManager $userManager, ) {
IUserManager $userManager,
ContainerInterface $container) {
parent::__construct($appName, $request);

$this->urlGenerator = $urlGenerator;
Expand All @@ -116,6 +121,12 @@ public function __construct(string $appName,
$this->smimeService = $smimeService;
$this->aiIntegrationsService = $aiIntegrationsService;
$this->userManager = $userManager;

try {
$this->availabilityCoordinator = $container->get(IAvailabilityCoordinator::class);
} catch (ContainerExceptionInterface $e) {
$this->availabilityCoordinator = null;
}
}

/**
Expand Down Expand Up @@ -174,7 +185,7 @@ public function index(): TemplateResponse {
'sort-order',
$this->preferences->getPreference($this->currentUserId, 'sort-order', 'newest')
);

try {
$password = $this->credentialStore->getLoginCredentials()->getPassword();
$passwordIsUnavailable = $password === null || $password === '';
Expand Down Expand Up @@ -277,6 +288,13 @@ function (SmimeCertificate $certificate) {
),
);

if ($this->availabilityCoordinator !== null) {
$this->initialStateService->provideInitialState(
'enable-system-out-of-office',
$this->availabilityCoordinator->isEnabled(),
);
}

$csp = new ContentSecurityPolicy();
$csp->addAllowedFrameDomain('\'self\'');
$response->setContentSecurityPolicy($csp);
Expand Down
55 changes: 9 additions & 46 deletions lib/Controller/SieveController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

/**
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* Mail
*
Expand All @@ -30,7 +31,7 @@
use OCA\Mail\Exception\CouldNotConnectException;
use OCA\Mail\Http\JsonResponse as MailJsonResponse;
use OCA\Mail\Http\TrapError;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\SieveService;
use OCA\Mail\Sieve\SieveClientFactory;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
Expand All @@ -42,7 +43,6 @@
use Psr\Log\LoggerInterface;

class SieveController extends Controller {
private AccountService $accountService;
private MailAccountMapper $mailAccountMapper;
private SieveClientFactory $sieveClientFactory;
private string $currentUserId;
Expand All @@ -52,16 +52,15 @@ class SieveController extends Controller {

public function __construct(IRequest $request,
string $UserId,
AccountService $accountService,
MailAccountMapper $mailAccountMapper,
SieveClientFactory $sieveClientFactory,
ICrypto $crypto,
IRemoteHostValidator $hostValidator,
LoggerInterface $logger
LoggerInterface $logger,
private SieveService $sieveService,
) {
parent::__construct(Application::APP_ID, $request);
$this->currentUserId = $UserId;
$this->accountService = $accountService;
$this->mailAccountMapper = $mailAccountMapper;
$this->sieveClientFactory = $sieveClientFactory;
$this->crypto = $crypto;
Expand All @@ -78,21 +77,14 @@ public function __construct(IRequest $request,
*
* @throws CouldNotConnectException
* @throws ClientException
* @throws ManagesieveException
*/
#[TrapError]
public function getActiveScript(int $id): JSONResponse {
$sieve = $this->getClient($id);

$scriptName = $sieve->getActive();
if ($scriptName === null) {
$script = '';
} else {
$script = $sieve->getScript($scriptName);
}

$activeScript = $this->sieveService->getActiveScript($this->currentUserId, $id);
return new JSONResponse([
'scriptName' => $scriptName,
'script' => $script,
'scriptName' => $activeScript->getName(),
'script' => $activeScript->getScript(),
]);
}

Expand All @@ -106,16 +98,11 @@ public function getActiveScript(int $id): JSONResponse {
*
* @throws ClientException
* @throws CouldNotConnectException
* @throws ManagesieveException
*/
#[TrapError]
public function updateActiveScript(int $id, string $script): JSONResponse {
$sieve = $this->getClient($id);

$scriptName = $sieve->getActive() ?? 'nextcloud';

try {
$sieve->installScript($scriptName, $script, true);
$this->sieveService->updateActiveScript($this->currentUserId, $id, $script);
} catch (ManagesieveException $e) {
$this->logger->error('Installing sieve script failed: ' . $e->getMessage(), ['app' => 'mail', 'exception' => $e]);
return new JSONResponse(data: ['message' => $e->getMessage()], statusCode: Http::STATUS_UNPROCESSABLE_ENTITY);
Expand Down Expand Up @@ -200,28 +187,4 @@ public function updateAccount(int $id,
$this->mailAccountMapper->save($mailAccount);
return new JSONResponse(['sieveEnabled' => $mailAccount->isSieveEnabled()]);
}

/**
* @param int $id
*
* @return \Horde\ManageSieve
*
* @throws ClientException
* @throws CouldNotConnectException
*/
protected function getClient(int $id): \Horde\ManageSieve {
$account = $this->accountService->find($this->currentUserId, $id);

if (!$account->getMailAccount()->isSieveEnabled()) {
throw new ClientException('ManageSieve is disabled.');
}

try {
$sieve = $this->sieveClientFactory->getClient($account);
} catch (ManagesieveException $e) {
throw new CouldNotConnectException($e, 'ManageSieve', $account->getMailAccount()->getSieveHost(), $account->getMailAccount()->getSievePort());
}

return $sieve;
}
}
Loading

0 comments on commit a5fe9c2

Please sign in to comment.