diff options
author | Cyrille Bollu <StCyr@users.noreply.github.com> | 2021-03-01 15:52:09 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-01 15:52:09 +0300 |
commit | 7019e65f8dad3d9302d281946fc7feb14cbe288c (patch) | |
tree | 6477da8e3d4fe05559e8f309e87ba29050732823 /lib | |
parent | 065c121c5696b3a9aad1741d29e1e47c38440fbc (diff) | |
parent | 42d9bfb0e583a49764524b55f09042290580811e (diff) |
Merge branch 'master' into dependabot/composer/vimeo/psalm-4.6.1
Diffstat (limited to 'lib')
31 files changed, 1152 insertions, 85 deletions
diff --git a/lib/Command/CreateAccount.php b/lib/Command/CreateAccount.php index 61c32b454..a1b95a283 100644 --- a/lib/Command/CreateAccount.php +++ b/lib/Command/CreateAccount.php @@ -25,6 +25,7 @@ namespace OCA\Mail\Command; use OCA\Mail\Db\MailAccount; use OCA\Mail\Service\AccountService; +use OCP\IUserManager; use OCP\Security\ICrypto; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -52,11 +53,17 @@ class CreateAccount extends Command { /** @var \OCP\Security\ICrypto */ private $crypto; - public function __construct(AccountService $service, ICrypto $crypto) { + /** @var IUserManager */ + private $userManager; + + public function __construct(AccountService $service, + ICrypto $crypto, + IUserManager $userManager) { parent::__construct(); $this->accountService = $service; $this->crypto = $crypto; + $this->userManager = $userManager; } /** @@ -99,6 +106,11 @@ class CreateAccount extends Command { $smtpUser = $input->getArgument(self::ARGUMENT_SMTP_USER); $smtpPassword = $input->getArgument(self::ARGUMENT_SMTP_PASSWORD); + if (!$this->userManager->userExists($userId)) { + $output->writeln("<error>User $userId does not exist</error>"); + return 1; + } + $account = new MailAccount(); $account->setUserId($userId); $account->setName($name); diff --git a/lib/Command/DeleteAccount.php b/lib/Command/DeleteAccount.php new file mode 100644 index 000000000..c32b41fa0 --- /dev/null +++ b/lib/Command/DeleteAccount.php @@ -0,0 +1,92 @@ +<?php + +declare(strict_types=1); + +/** + * @author Anna Larch <anna.larch@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Command; + +use OCA\Mail\Account; +use Psr\Log\LoggerInterface; +use OCA\Mail\Service\AccountService; +use OCA\Mail\Exception\ClientException; +use OCP\AppFramework\Db\DoesNotExistException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class DeleteAccount extends Command { + public const ARGUMENT_ACCOUNT_ID = 'account-id'; + + /** @var AccountService */ + private $accountService; + + /** @var LoggerInterface */ + private $logger; + + public function __construct(AccountService $service, + LoggerInterface $logger) { + parent::__construct(); + + $this->accountService = $service; + $this->logger = $logger; + } + + /** + * @return void + */ + protected function configure() { + $this->setName('mail:account:delete'); + $this->setDescription('Delete an IMAP account'); + $this->addArgument(self::ARGUMENT_ACCOUNT_ID, InputArgument::REQUIRED); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $accountId = (int)$input->getArgument(self::ARGUMENT_ACCOUNT_ID); + + try { + $account = $this->accountService->findById($accountId); + } catch (DoesNotExistException $e) { + $output->writeLn('<error>This account does not exist</error>'); + return 1; + } + $output->writeLn("<info>Found account with email: " . $account->getEmail() . "</info>"); + + if ($account->getMailAccount()->getProvisioned() === true) { + $output->writeLn('<error>This is a provisioned account which can not be deleted from CLI. Use the Provisioning UI instead.</error>'); + return 2; + } + $output->writeLn("<info>Deleting " . $account->getEmail() . "</info>"); + $this->delete($account, $output); + + return 0; + } + + private function delete(Account $account, OutputInterface $output): void { + $id = $account->getId(); + try { + $this->accountService->deleteByAccountId($account->getId()); + } catch (ClientException $e) { + throw $e; + } + $output->writeLn("<info>Deleted account $id </info>"); + } +} diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index ca17c0edf..9b3f48e41 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -221,4 +221,12 @@ interface IMailManager { */ public function enableMailboxBackgroundSync(Mailbox $mailbox, bool $syncInBackground): Mailbox; + + /** + * @param Account $account + * @param Mailbox $mailbox + * @param Message $message + * @return array + */ + public function getMailAttachments(Account $account, Mailbox $mailbox, Message $message) : array; } diff --git a/lib/Contracts/ITrustedSenderService.php b/lib/Contracts/ITrustedSenderService.php index 7f0de8aab..1f25e93d4 100644 --- a/lib/Contracts/ITrustedSenderService.php +++ b/lib/Contracts/ITrustedSenderService.php @@ -30,7 +30,7 @@ use OCA\Mail\Db\TrustedSender; interface ITrustedSenderService { public function isTrusted(string $uid, string $email): bool; - public function trust(string $uid, string $email, ?bool $trust = true); + public function trust(string $uid, string $email, string $type, ?bool $trust = true); /** * @param string $uid diff --git a/lib/Controller/AccountsController.php b/lib/Controller/AccountsController.php index 9c79668e9..be4ba7602 100644 --- a/lib/Controller/AccountsController.php +++ b/lib/Controller/AccountsController.php @@ -194,6 +194,13 @@ class AccountsController extends Controller { string $smtpSslMode = null, string $smtpUser = null, string $smtpPassword = null): JSONResponse { + try { + // Make sure the account actually exists + $this->accountService->find($this->currentUserId, $id); + } catch (ClientException $e) { + return new JSONResponse([], Http::STATUS_BAD_REQUEST); + } + $account = null; $errorMessage = null; try { diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php index ad49a8275..6361d415e 100755 --- a/lib/Controller/MessagesController.php +++ b/lib/Controller/MessagesController.php @@ -51,6 +51,7 @@ use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Http\ZipResponse; use OCP\Files\Folder; use OCP\Files\IMimeTypeDetector; use OCP\IL10N; @@ -527,6 +528,43 @@ class MessagesController extends Controller { /** * @NoAdminRequired + * @NoCSRFRequired + * @TrapError + * + * @param int $id the message id + * @param string $attachmentId + * + * @return ZipResponse|JSONResponse + * + * @throws ClientException + * @throws ServiceException + * @throws DoesNotExistException + */ + public function downloadAttachments(int $id): Response { + try { + $message = $this->mailManager->getMessage($this->currentUserId, $id); + $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); + $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + + $attachments = $this->mailManager->getMailAttachments($account, $mailbox, $message); + $zip = new ZipResponse($this->request, 'attachments'); + + foreach ($attachments as $attachment) { + $fileName = $attachment['name']; + $fh = fopen("php://temp", 'r+'); + fputs($fh, $attachment['content']); + $size = (int)$attachment['size']; + rewind($fh); + $zip->addResource($fh, $fileName, $size); + } + return $zip; + } + + /** + * @NoAdminRequired * @TrapError * * @param int $id diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 829068fc2..d65f04f03 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -50,7 +50,12 @@ class SettingsController extends Controller { string $smtpUser, string $smtpHost, int $smtpPort, - string $smtpSslMode): JSONResponse { + string $smtpSslMode, + bool $sieveEnabled, + string $sieveUser, + string $sieveHost, + int $sievePort, + string $sieveSslMode): JSONResponse { $this->provisioningManager->newProvisioning( $emailTemplate, $imapUser, @@ -60,7 +65,12 @@ class SettingsController extends Controller { $smtpUser, $smtpHost, $smtpPort, - $smtpSslMode + $smtpSslMode, + $sieveEnabled, + $sieveUser, + $sieveHost, + $sievePort, + $sieveSslMode ); return new JSONResponse([]); diff --git a/lib/Controller/SieveController.php b/lib/Controller/SieveController.php new file mode 100644 index 000000000..11fc5b186 --- /dev/null +++ b/lib/Controller/SieveController.php @@ -0,0 +1,219 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Controller; + +use Horde\ManageSieve\Exception as ManagesieveException; +use OCA\Mail\AppInfo\Application; +use OCA\Mail\Db\MailAccountMapper; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Exception\CouldNotConnectException; +use OCA\Mail\Service\AccountService; +use OCA\Mail\Sieve\SieveClientFactory; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\Security\ICrypto; + +class SieveController extends Controller { + + /** @var AccountService */ + private $accountService; + + /** @var MailAccountMapper */ + private $mailAccountMapper; + + /** @var SieveClientFactory */ + private $sieveClientFactory; + + /** @var string */ + private $currentUserId; + + /** @var ICrypto */ + private $crypto; + + /** + * AccountsController constructor. + * + * @param IRequest $request + * @param string $UserId + * @param AccountService $accountService + * @param MailAccountMapper $mailAccountMapper + * @param SieveClientFactory $sieveClientFactory + * @param ICrypto $crypto + */ + public function __construct(IRequest $request, + string $UserId, + AccountService $accountService, + MailAccountMapper $mailAccountMapper, + SieveClientFactory $sieveClientFactory, + ICrypto $crypto + ) { + parent::__construct(Application::APP_ID, $request); + $this->currentUserId = $UserId; + $this->accountService = $accountService; + $this->mailAccountMapper = $mailAccountMapper; + $this->sieveClientFactory = $sieveClientFactory; + $this->crypto = $crypto; + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id account id + * + * @return JSONResponse + * + * @throws CouldNotConnectException + * @throws ClientException + */ + public function getActiveScript(int $id): JSONResponse { + $sieve = $this->getClient($id); + + $scriptName = $sieve->getActive(); + if ($scriptName === null) { + $script = ''; + } else { + $script = $sieve->getScript($scriptName); + } + + return new JSONResponse([ + 'scriptName' => $scriptName, + 'script' => $script, + ]); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id account id + * @param string $script + * + * @return JSONResponse + * + * @throws ClientException + * @throws CouldNotConnectException + * @throws ManagesieveException + */ + public function updateActiveScript(int $id, string $script): JSONResponse { + $sieve = $this->getClient($id); + + $scriptName = $sieve->getActive() ?? 'nextcloud'; + $sieve->installScript($scriptName, $script, true); + + return new JSONResponse(); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id account id + * @param bool $sieveEnabled + * @param string $sieveHost + * @param int $sievePort + * @param string $sieveUser + * @param string $sievePassword + * @param string $sieveSslMode + * + * @return JSONResponse + * + * @throws CouldNotConnectException + * @throws DoesNotExistException + */ + public function updateAccount(int $id, + bool $sieveEnabled, + string $sieveHost, + int $sievePort, + string $sieveUser, + string $sievePassword, + string $sieveSslMode + ): JSONResponse { + $mailAccount = $this->mailAccountMapper->find($this->currentUserId, $id); + + if ($sieveEnabled === false) { + $mailAccount->setSieveEnabled(false); + $mailAccount->setSieveHost(null); + $mailAccount->setSievePort(null); + $mailAccount->setSieveUser(null); + $mailAccount->setSievePassword(null); + $mailAccount->setSieveSslMode(null); + + $this->mailAccountMapper->save($mailAccount); + return new JSONResponse(['sieveEnabled' => $mailAccount->isSieveEnabled()]); + } + + if (empty($sieveUser)) { + $sieveUser = $mailAccount->getInboundUser(); + } + + if (empty($sievePassword)) { + $sievePassword = $mailAccount->getInboundPassword(); + } else { + $sievePassword = $this->crypto->encrypt($sievePassword); + } + + try { + $this->sieveClientFactory->createClient($sieveHost, $sievePort, $sieveUser, $sievePassword, $sieveSslMode); + } catch (ManagesieveException $e) { + throw CouldNotConnectException::create($e, 'ManageSieve', $sieveHost, $sievePort); + } + + $mailAccount->setSieveEnabled(true); + $mailAccount->setSieveHost($sieveHost); + $mailAccount->setSievePort($sievePort); + $mailAccount->setSieveUser($mailAccount->getInboundUser() === $sieveUser ? null : $sieveUser); + $mailAccount->setSievePassword($mailAccount->getInboundPassword() === $sievePassword ? null : $sievePassword); + $mailAccount->setSieveSslMode($sieveSslMode); + + $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 CouldNotConnectException('ManageSieve is disabled.'); + } + + try { + $sieve = $this->sieveClientFactory->getClient($account); + } catch (ManagesieveException $e) { + throw CouldNotConnectException::create($e, 'ManageSieve', $account->getMailAccount()->getSieveHost(), $account->getMailAccount()->getSievePort()); + } + + return $sieve; + } +} diff --git a/lib/Controller/TrustedSendersController.php b/lib/Controller/TrustedSendersController.php index 552101d8d..54c5d930c 100644 --- a/lib/Controller/TrustedSendersController.php +++ b/lib/Controller/TrustedSendersController.php @@ -54,13 +54,14 @@ class TrustedSendersController extends Controller { * @TrapError * * @param string $email - * + * @param string $type * @return JsonResponse */ - public function setTrusted(string $email): JsonResponse { + public function setTrusted(string $email, string $type): JsonResponse { $this->trustedSenderService->trust( $this->uid, - $email + $email, + $type ); return JsonResponse::success(null, Http::STATUS_CREATED); @@ -71,13 +72,14 @@ class TrustedSendersController extends Controller { * @TrapError * * @param string $email - * + * @param string $type * @return JsonResponse */ - public function removeTrust(string $email): JsonResponse { + public function removeTrust(string $email, string $type): JsonResponse { $this->trustedSenderService->trust( $this->uid, $email, + $type, false ); diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php index b5aa8ecae..3df3c1a67 100644 --- a/lib/Db/MailAccount.php +++ b/lib/Db/MailAccount.php @@ -77,6 +77,18 @@ use OCP\AppFramework\Db\Entity; * @method int|null getSentMailboxId() * @method void setTrashMailboxId(?int $id) * @method int|null getTrashMailboxId() + * @method bool isSieveEnabled() + * @method void setSieveEnabled(bool $sieveEnabled) + * @method string|null getSieveHost() + * @method void setSieveHost(?string $sieveHost) + * @method int|null getSievePort() + * @method void setSievePort(?int $sievePort) + * @method string|null getSieveSslMode() + * @method void setSieveSslMode(?string $sieveSslMode) + * @method string|null getSieveUser() + * @method void setSieveUser(?string $sieveUser) + * @method string|null getSievePassword() + * @method void setSievePassword(?string $sievePassword) */ class MailAccount extends Entity { protected $userId; @@ -109,6 +121,19 @@ class MailAccount extends Entity { /** @var int|null */ protected $trashMailboxId; + /** @var bool */ + protected $sieveEnabled = false; + /** @var string|null */ + protected $sieveHost; + /** @var integer|null */ + protected $sievePort; + /** @var string|null */ + protected $sieveSslMode; + /** @var string|null */ + protected $sieveUser; + /** @var string|null */ + protected $sievePassword; + /** * @param array $params */ @@ -168,6 +193,8 @@ class MailAccount extends Entity { $this->addType('draftsMailboxId', 'integer'); $this->addType('sentMailboxId', 'integer'); $this->addType('trashMailboxId', 'integer'); + $this->addType('sieveEnabled', 'boolean'); + $this->addType('sievePort', 'integer'); } /** @@ -192,6 +219,7 @@ class MailAccount extends Entity { 'draftsMailboxId' => $this->getDraftsMailboxId(), 'sentMailboxId' => $this->getSentMailboxId(), 'trashMailboxId' => $this->getTrashMailboxId(), + 'sieveEnabled' => $this->isSieveEnabled(), ]; if (!is_null($this->getOutboundHost())) { @@ -201,6 +229,13 @@ class MailAccount extends Entity { $result['smtpSslMode'] = $this->getOutboundSslMode(); } + if ($this->isSieveEnabled()) { + $result['sieveHost'] = $this->getSieveHost(); + $result['sievePort'] = $this->getSievePort(); + $result['sieveUser'] = $this->getSieveUser(); + $result['sieveSslMode'] = $this->getSieveSslMode(); + } + return $result; } } diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php index 77ec05776..81f1967a7 100644 --- a/lib/Db/MessageMapper.php +++ b/lib/Db/MessageMapper.php @@ -333,6 +333,7 @@ class MessageMapper extends QBMapper { ->set('flag_junk', $query->createParameter('flag_junk')) ->set('flag_notjunk', $query->createParameter('flag_notjunk')) ->set('flag_mdnsent', $query->createParameter('flag_mdnsent')) + ->set('flag_important', $query->createParameter('flag_important')) ->set('updated_at', $query->createNamedParameter($this->timeFactory->getTime())) ->where($query->expr()->andX( $query->expr()->eq('uid', $query->createParameter('uid')), @@ -356,6 +357,7 @@ class MessageMapper extends QBMapper { $query->setParameter('flag_junk', $message->getFlagJunk(), IQueryBuilder::PARAM_BOOL); $query->setParameter('flag_notjunk', $message->getFlagNotjunk(), IQueryBuilder::PARAM_BOOL); $query->setParameter('flag_mdnsent', $message->getFlagMdnsent(), IQueryBuilder::PARAM_BOOL); + $query->setParameter('flag_important', $message->getFlagImportant(), IQueryBuilder::PARAM_BOOL); $query->execute(); } @@ -433,37 +435,26 @@ class MessageMapper extends QBMapper { public function findThread(Account $account, int $messageId): array { $qb = $this->db->getQueryBuilder(); $subQb1 = $this->db->getQueryBuilder(); - $subQb2 = $this->db->getQueryBuilder(); $mailboxIdsQuery = $subQb1 ->select('id') ->from('mail_mailboxes') ->where($qb->expr()->eq('account_id', $qb->createNamedParameter($account->getId(), IQueryBuilder::PARAM_INT))); - $threadRootIdsQuery = $subQb2 - ->select('thread_root_id') - ->from($this->getTableName()) - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($messageId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT) - ); /** * Select the message with the given ID or any that has the same thread ID */ $selectMessages = $qb - ->select('*') - ->from($this->getTableName()) + ->select('m2.*') + ->from($this->getTableName(), 'm1') + ->leftJoin('m1', $this->getTableName(), 'm2', $qb->expr()->eq('m1.thread_root_id', 'm2.thread_root_id')) ->where( - $qb->expr()->in('mailbox_id', $qb->createFunction($mailboxIdsQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY), - $qb->expr()->orX( - $qb->expr()->eq('id', $qb->createNamedParameter($messageId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), - $qb->expr()->andX( - $qb->expr()->isNotNull('thread_root_id'), - $qb->expr()->in('thread_root_id', $qb->createFunction($threadRootIdsQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY) - ) - ) + $qb->expr()->in('m1.mailbox_id', $qb->createFunction($mailboxIdsQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY), + $qb->expr()->in('m2.mailbox_id', $qb->createFunction($mailboxIdsQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY), + $qb->expr()->eq('m1.id', $qb->createNamedParameter($messageId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), + $qb->expr()->isNotNull('m1.thread_root_id') ) ->orderBy('sent_at', 'desc'); - return $this->findRecipients($this->findEntities($selectMessages)); } diff --git a/lib/Db/TrustedSender.php b/lib/Db/TrustedSender.php index d02313f7c..3425c056a 100644 --- a/lib/Db/TrustedSender.php +++ b/lib/Db/TrustedSender.php @@ -33,6 +33,8 @@ use OCP\AppFramework\Db\Entity; * @method getEmail(): string * @method setUserId(string $userId): void * @method getUserId(): string + * @method setType(string $type): void + * @method getType(): string */ class TrustedSender extends Entity implements JsonSerializable { @@ -42,11 +44,15 @@ class TrustedSender extends Entity implements JsonSerializable { /** @var string */ protected $userId; + /** @var string */ + protected $type; + public function jsonSerialize() { return [ 'id' => $this->id, 'email' => $this->email, 'uid' => $this->userId, + 'type' => $this->type, ]; } } diff --git a/lib/Db/TrustedSenderMapper.php b/lib/Db/TrustedSenderMapper.php index 5d29f224a..b8b8eeae3 100644 --- a/lib/Db/TrustedSenderMapper.php +++ b/lib/Db/TrustedSenderMapper.php @@ -34,13 +34,24 @@ class TrustedSenderMapper extends QBMapper { } public function exists(string $uid, string $email): bool { + $emailObject = new \Horde_Mail_Rfc822_Address($email); + $host = $emailObject->host; $qb = $this->db->getQueryBuilder(); $select = $qb->select('*') ->from($this->getTableName()) ->where( - $qb->expr()->eq('user_id', $qb->createNamedParameter($uid)), - $qb->expr()->eq('email', $qb->createNamedParameter($email)) + $qb->expr()->orX( + $qb->expr()->andX( + $qb->expr()->eq('email', $qb->createNamedParameter($email)), + $qb->expr()->eq('type', $qb->createNamedParameter('individual')) + ), + $qb->expr()->andX( + $qb->expr()->eq('email', $qb->createNamedParameter($host)), + $qb->expr()->eq('type', $qb->createNamedParameter('domain')) + ) + ), + $qb->expr()->eq('user_id', $qb->createNamedParameter($uid)) ); /** @var TrustedSender[] $rows */ @@ -49,25 +60,27 @@ class TrustedSenderMapper extends QBMapper { return !empty($rows); } - public function create(string $uid, string $email): void { + public function create(string $uid, string $email, string $type): void { $qb = $this->db->getQueryBuilder(); $insert = $qb->insert($this->getTableName()) ->values([ 'user_id' => $qb->createNamedParameter($uid), 'email' => $qb->createNamedParameter($email), + 'type' => $qb->createNamedParameter($type), ]); $insert->execute(); } - public function remove(string $uid, string $email): void { + public function remove(string $uid, string $email, string $type): void { $qb = $this->db->getQueryBuilder(); $delete = $qb->delete($this->getTableName()) ->where( $qb->expr()->eq('user_id', $qb->createNamedParameter($uid)), - $qb->expr()->eq('email', $qb->createNamedParameter($email)) + $qb->expr()->eq('email', $qb->createNamedParameter($email)), + $qb->expr()->eq('type', $qb->createNamedParameter($type)) ); $delete->execute(); diff --git a/lib/Exception/CouldNotConnectException.php b/lib/Exception/CouldNotConnectException.php new file mode 100644 index 000000000..c996095ee --- /dev/null +++ b/lib/Exception/CouldNotConnectException.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Exception; + +use Throwable; + +class CouldNotConnectException extends ServiceException { + public static function create(Throwable $exception, string $service, string $host, int $port): self { + return new self( + "Connection to {$service} at {$host}:{$port} failed. {$exception->getMessage()}", + (int)$exception->getCode(), + $exception + ); + } +} diff --git a/lib/IMAP/FolderMapper.php b/lib/IMAP/FolderMapper.php index b40d5abeb..f12ea3af8 100644 --- a/lib/IMAP/FolderMapper.php +++ b/lib/IMAP/FolderMapper.php @@ -62,12 +62,24 @@ class FolderMapper { 'special_use' => true, ]); - return array_filter(array_map(function (array $mailbox) use ($account) { + return array_filter(array_map(function (array $mailbox) use ($account, $client) { if (in_array($mailbox['mailbox']->utf8, self::DOVECOT_SIEVE_FOLDERS, true)) { // This is a special folder that must not be shown return null; } + try { + $client->status($mailbox["mailbox"]); + } catch (Horde_Imap_Client_Exception $e) { + // ignore folders which cause errors on access + // (i.e. server-side system I/O errors) + if (in_array($e->getCode(), [ + Horde_Imap_Client_Exception::UNSPECIFIED, + ], true)) { + return null; + } + } + return new Folder( $account->getId(), $mailbox['mailbox'], diff --git a/lib/IMAP/MessageMapper.php b/lib/IMAP/MessageMapper.php index 96fdb2255..95161fd8a 100644 --- a/lib/IMAP/MessageMapper.php +++ b/lib/IMAP/MessageMapper.php @@ -201,11 +201,7 @@ class MessageMapper { $query->flags(); $query->uid(); $query->imapDate(); - $query->headers( - 'references', - [ - 'references', - ], + $query->headerText( [ 'cache' => true, 'peek' => true, @@ -471,28 +467,65 @@ class MessageMapper { } $structure = $structureResult->getStructure(); - $partsQuery = new Horde_Imap_Client_Fetch_Query(); - $partsQuery->fullText(); - foreach ($structure->partIterator() as $part) { + $partsQuery = $this->buildAttachmentsPartsQuery($structure, $attachmentIds); + + $parts = $client->fetch($mailbox, $partsQuery, [ + 'ids' => new Horde_Imap_Client_Ids([$uid]), + ]); + if (($messageData = $parts->first()) === null) { + throw new DoesNotExistException('Message does not exist'); + } + + $attachments = []; + foreach ($structure->partIterator() as $key => $part) { /** @var Horde_Mime_Part $part */ - if ($part->getMimeId() === '0') { - // Ignore message header - continue; - } - if (!empty($attachmentIds) && !in_array($part->getMIMEId(), $attachmentIds, true)) { - // We are looking for specific parts only and this is not one of them + + if (!$part->isAttachment()) { continue; } - $partsQuery->bodyPart($part->getMimeId(), [ - 'peek' => true, - ]); - $partsQuery->mimeHeader($part->getMimeId(), [ - 'peek' => true + $stream = $messageData->getBodyPart($key, true); + $mimeHeaders = $messageData->getMimeHeader($key, Horde_Imap_Client_Data_Fetch::HEADER_PARSE); + if ($enc = $mimeHeaders->getValue('content-transfer-encoding')) { + $part->setTransferEncoding($enc); + } + $part->setContents($stream, [ + 'usestream' => true, ]); - $partsQuery->bodyPartSize($part->getMimeId()); + $decoded = $part->getContents(); + + $attachments[] = $decoded; + } + return $attachments; + } + + /** + * Get Attachments with size, content and name properties + * + * @param Horde_Imap_Client_Socket $client + * @param string $mailbox + * @param integer $uid + * @param array|null $attachmentIds + * @return array[] + */ + public function getAttachments(Horde_Imap_Client_Socket $client, + string $mailbox, + int $uid, + ?array $attachmentIds = []): array { + $messageQuery = new Horde_Imap_Client_Fetch_Query(); + $messageQuery->structure(); + + $result = $client->fetch($mailbox, $messageQuery, [ + 'ids' => new Horde_Imap_Client_Ids([$uid]), + ]); + + if (($structureResult = $result->first()) === null) { + throw new DoesNotExistException('Message does not exist'); } + $structure = $structureResult->getStructure(); + $partsQuery = $this->buildAttachmentsPartsQuery($structure, $attachmentIds); + $parts = $client->fetch($mailbox, $partsQuery, [ 'ids' => new Horde_Imap_Client_Ids([$uid]), ]); @@ -516,14 +549,48 @@ class MessageMapper { $part->setContents($stream, [ 'usestream' => true, ]); - $decoded = $part->getContents(); - - $attachments[] = $decoded; + $attachments[] = [ + 'content' => $part->getContents(), + 'name' => $part->getName(), + 'size' => $part->getSize() + ]; } return $attachments; } /** + * Build the parts query for attachments + * + * @param $structure + * @param array $attachmentIds + * @return Horde_Imap_Client_Fetch_Query + */ + private function buildAttachmentsPartsQuery($structure, array $attachmentIds) : Horde_Imap_Client_Fetch_Query { + $partsQuery = new Horde_Imap_Client_Fetch_Query(); + $partsQuery->fullText(); + foreach ($structure->partIterator() as $part) { + /** @var Horde_Mime_Part $part */ + if ($part->getMimeId() === '0') { + // Ignore message header + continue; + } + if (!empty($attachmentIds) && !in_array($part->getMIMEId(), $attachmentIds, true)) { + // We are looking for specific parts only and this is not one of them + continue; + } + + $partsQuery->bodyPart($part->getMimeId(), [ + 'peek' => true, + ]); + $partsQuery->mimeHeader($part->getMimeId(), [ + 'peek' => true + ]); + $partsQuery->bodyPartSize($part->getMimeId()); + } + return $partsQuery; + } + + /** * @param Horde_Imap_Client_Socket $client * @param int[] $uids * diff --git a/lib/Listener/NewMessageClassificationListener.php b/lib/Listener/NewMessageClassificationListener.php index 365251883..56585730c 100644 --- a/lib/Listener/NewMessageClassificationListener.php +++ b/lib/Listener/NewMessageClassificationListener.php @@ -67,11 +67,16 @@ class NewMessageClassificationListener implements IEventListener { } } + // if the message is already flagged as important, we won't classify it again. + $messages = array_filter($event->getMessages(), function ($message) { + return ($message->getFlagImportant() === false); + }); + try { $predictions = $this->classifier->classifyImportance( $event->getAccount(), $event->getMailbox(), - $event->getMessages() + $messages ); foreach ($event->getMessages() as $message) { diff --git a/lib/Migration/AddSieveToProvisioningConfig.php b/lib/Migration/AddSieveToProvisioningConfig.php new file mode 100644 index 000000000..e871ff1cc --- /dev/null +++ b/lib/Migration/AddSieveToProvisioningConfig.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Migration; + +use OCA\Mail\Service\Provisioning\Config as ProvisioningConfig; +use OCA\Mail\Service\Provisioning\ConfigMapper as ProvisioningConfigMapper; +use OCP\IConfig; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class AddSieveToProvisioningConfig implements IRepairStep { + + /** @var IConfig */ + private $config; + + /** @var ProvisioningConfigMapper */ + private $configMapper; + + public function __construct(IConfig $config, ProvisioningConfigMapper $configMapper) { + $this->config = $config; + $this->configMapper = $configMapper; + } + + public function getName(): string { + return 'Add sieve defaults to provisioning config'; + } + + public function run(IOutput $output) { + if (!$this->shouldRun()) { + return; + } + + $config = $this->configMapper->load(); + if ($config === null) { + return; + } + + $reflectionClass = new \ReflectionClass(ProvisioningConfig::class); + $reflectionProperty = $reflectionClass->getProperty('data'); + + $reflectionProperty->setAccessible(true); + $data = $reflectionProperty->getValue($config); + + if (!isset($data['sieveEnabled'])) { + $data = array_merge($data, [ + 'sieveEnabled' => false, + 'sieveHost' => '', + 'sievePort' => 4190, + 'sieveUser' => '', + 'sieveSslMode' => 'tls', + ]); + } + + $reflectionProperty->setValue($config, $data); + $this->configMapper->save($config); + + $output->info('added sieve defaults to provisioning config'); + } + + protected function shouldRun(): bool { + $appVersion = $this->config->getAppValue('mail', 'installed_version', '0.0.0'); + return version_compare($appVersion, '1.9.0', '<'); + } +} diff --git a/lib/Migration/Version1090Date20210127160127.php b/lib/Migration/Version1090Date20210127160127.php new file mode 100644 index 000000000..8a52ae4fc --- /dev/null +++ b/lib/Migration/Version1090Date20210127160127.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Mail\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1090Date20210127160127 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('mail_accounts'); + $table->addColumn('sieve_enabled', 'boolean', [ + 'notnull' => true, + 'default' => false, + ]); + $table->addColumn('sieve_host', 'string', [ + 'notnull' => false, + 'length' => 64, + 'default' => null, + ]); + $table->addColumn('sieve_port', 'string', [ + 'notnull' => false, + 'length' => 6, + 'default' => null, + ]); + $table->addColumn('sieve_ssl_mode', 'string', [ + 'notnull' => false, + 'length' => 10, + 'default' => null, + ]); + $table->addColumn('sieve_user', 'string', [ + 'notnull' => false, + 'length' => 64, + 'default' => null, + ]); + $table->addColumn('sieve_password', 'string', [ + 'notnull' => false, + 'length' => 2048, + 'default' => null, + ]); + + return $schema; + } +} diff --git a/lib/Migration/Version1090Date20210216154409.php b/lib/Migration/Version1090Date20210216154409.php new file mode 100644 index 000000000..4a793a142 --- /dev/null +++ b/lib/Migration/Version1090Date20210216154409.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Mail\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1090Date20210216154409 extends SimpleMigrationStep { + + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('mail_trusted_senders'); + $table->addColumn('type', 'string', [ + 'notnull' => true, + 'default' => 'individual', + ]); + $table->addIndex(['type'], 'mail_trusted_senders_type'); + + return $schema; + } +} diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php index 1841d1cb2..bd8437e72 100644 --- a/lib/Model/IMAPMessage.php +++ b/lib/Model/IMAPMessage.php @@ -129,6 +129,7 @@ class IMAPMessage implements IMessage, JsonSerializable { } /** + * @deprecated Seems unused * @return array */ public function getFlags(): array { @@ -142,10 +143,12 @@ class IMAPMessage implements IMessage, JsonSerializable { 'forwarded' => in_array(Horde_Imap_Client::FLAG_FORWARDED, $flags), 'hasAttachments' => $this->hasAttachments($this->fetch->getStructure()), 'mdnsent' => in_array(Horde_Imap_Client::FLAG_MDNSENT, $flags, true), + 'important' => in_array('$important', $flags, true) ]; } /** + * @deprecated Seems unused * @param string[] $flags * * @throws Exception @@ -166,7 +169,7 @@ class IMAPMessage implements IMessage, JsonSerializable { private function getRawReferences(): string { /** @var Horde_Mime_Headers $headers */ - $headers = $this->fetch->getHeaders('references', Horde_Imap_Client_Data_Fetch::HEADER_PARSE); + $headers = $this->fetch->getHeaderText('0', Horde_Imap_Client_Data_Fetch::HEADER_PARSE); $header = $headers->getHeader('references'); if ($header === null) { return ''; @@ -180,7 +183,7 @@ class IMAPMessage implements IMessage, JsonSerializable { public function getDispositionNotificationTo(): string { /** @var Horde_Mime_Headers $headers */ - $headers = $this->fetch->getHeaders('mdn', Horde_Imap_Client_Data_Fetch::HEADER_PARSE); + $headers = $this->fetch->getHeaderText('0', Horde_Imap_Client_Data_Fetch::HEADER_PARSE); $header = $headers->getHeader('disposition-notification-to'); if ($header === null) { return ''; @@ -345,11 +348,10 @@ class IMAPMessage implements IMessage, JsonSerializable { $fetch_query->flags(); $fetch_query->size(); $fetch_query->imapDate(); - $fetch_query->headers( - 'mdn', - ['disposition-notification-to'], - ['cache' => true, 'peek' => true] - ); + $fetch_query->headerText([ + 'cache' => true, + 'peek' => true, + ]); // $list is an array of Horde_Imap_Client_Data_Fetch objects. $ids = new Horde_Imap_Client_Ids($this->messageId); @@ -692,7 +694,7 @@ class IMAPMessage implements IMessage, JsonSerializable { in_array('junk', $flags, true) ); $msg->setFlagNotjunk(in_array(Horde_Imap_Client::FLAG_NOTJUNK, $flags, true)); - $msg->setFlagImportant(false); + $msg->setFlagImportant(in_array('$important', $flags, true)); $msg->setFlagAttachments(false); $msg->setFlagMdnsent(in_array(Horde_Imap_Client::FLAG_MDNSENT, $flags, true)); diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php index 90766e98c..13462ff2d 100644 --- a/lib/Service/AccountService.php +++ b/lib/Service/AccountService.php @@ -126,6 +126,21 @@ class AccountService { } /** + * @param int $accountId + * + * @throws ClientException + */ + public function deleteByAccountId(int $accountId): void { + try { + $mailAccount = $this->mapper->findById($accountId); + } catch (DoesNotExistException $e) { + throw new ClientException("Account $accountId does not exist", 0, $e); + } + $this->aliasesService->deleteAll($accountId); + $this->mapper->delete($mailAccount); + } + + /** * @param MailAccount $newAccount * @return MailAccount */ diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index 444a1aac5..d37278477 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -26,6 +26,7 @@ namespace OCA\Mail\Service; use Horde_Imap_Client; use Horde_Imap_Client_Exception; use Horde_Imap_Client_Exception_NoSupportExtension; +use Horde_Imap_Client_Socket; use OCA\Mail\Account; use OCA\Mail\Contracts\IMailManager; use OCA\Mail\Db\Mailbox; @@ -386,10 +387,13 @@ class MailManager implements IMailManager { } // Only send system flags to the IMAP server as other flags might not be supported - $imapFlags = self::ALLOWED_FLAGS[$flag] ?? []; + $imapFlags = $this->filterFlags($account, $flag, $mailbox); try { foreach ($imapFlags as $imapFlag) { - if ($value) { + if (empty($imapFlag) === true) { + continue; + } + if ($value === true) { $this->imapMessageMapper->addFlag($client, $mb, $uid, $imapFlag); } else { $this->imapMessageMapper->removeFlag($client, $mb, $uid, $imapFlag); @@ -502,4 +506,57 @@ class MailManager implements IMailManager { $this->folderMapper->delete($client, $mailbox->getName()); $this->mailboxMapper->delete($mailbox); } + + /** + * @param Account $account + * @param Mailbox $mailbox + * @param Message $message + * @return array[] + */ + public function getMailAttachments(Account $account, Mailbox $mailbox, Message $message): array { + return $this->imapMessageMapper->getAttachments($this->imapClientFactory->getClient($account), $mailbox->getName(), $message->getUid()); + } + + /** + * Filter out IMAP flags that aren't supported by the client server + * + * @param Horde_Imap_Client_Socket $client + * @param string $flag + * @param string $mailbox + * @return array + */ + public function filterFlags(Account $account, string $flag, string $mailbox): array { + // check for RFC server flags + if (array_key_exists($flag, self::ALLOWED_FLAGS) === true) { + return self::ALLOWED_FLAGS[$flag]; + } + + // Only allow flag setting if IMAP supports Permaflags + // @TODO check if there are length & char limits on permflags + if ($this->isPermflagsEnabled($account, $mailbox) === true) { + return ["$" . $flag]; + } + return []; + } + + /** + * Check IMAP server for support for PERMANENTFLAGS + * + * @param Account $account + * @param string $mailbox + * @return boolean + */ + public function isPermflagsEnabled(Account $account, string $mailbox): bool { + $client = $this->imapClientFactory->getClient($account); + try { + $capabilities = $client->status($mailbox, Horde_Imap_Client::STATUS_PERMFLAGS); + } catch (Horde_Imap_Client_Exception $e) { + throw new ServiceException( + "Could not get message flag options from IMAP: " . $e->getMessage(), + (int) $e->getCode(), + $e + ); + } + return (is_array($capabilities) === true && array_key_exists('permflags', $capabilities) === true && in_array("\*", $capabilities['permflags'], true) === true); + } } diff --git a/lib/Service/MailTransmission.php b/lib/Service/MailTransmission.php index 4630efff6..729da0374 100644 --- a/lib/Service/MailTransmission.php +++ b/lib/Service/MailTransmission.php @@ -26,6 +26,7 @@ namespace OCA\Mail\Service; use Horde_Exception; use Horde_Imap_Client; use Horde_Imap_Client_Data_Fetch; +use Horde_Imap_Client_DateTime; use Horde_Imap_Client_Fetch_Query; use Horde_Imap_Client_Ids; use Horde_Mail_Transport_Null; @@ -432,12 +433,12 @@ class MailTransmission implements IMailTransmission { $query->flags(); $query->uid(); $query->imapDate(); - $query->headers( - 'mdn', - ['disposition-notification-to', 'original-recipient'], - ['cache' => true, 'peek' => true] - ); + $query->headerText([ + 'cache' => true, + 'peek' => true, + ]); + /** @var Horde_Imap_Client_Data_Fetch[] $fetchResults */ $fetchResults = iterator_to_array($imapClient->fetch($mailbox->getName(), $query, [ 'ids' => new Horde_Imap_Client_Ids([$message->getUid()]), ]), false); @@ -446,10 +447,10 @@ class MailTransmission implements IMailTransmission { throw new ServiceException('Message "' .$message->getId() . '" not found.'); } - /** @var \Horde_Imap_Client_DateTime $imapDate */ + /** @var Horde_Imap_Client_DateTime $imapDate */ $imapDate = $fetchResults[0]->getImapDate(); /** @var Horde_Mime_Headers $headers */ - $mdnHeaders = $fetchResults[0]->getHeaders('mdn', Horde_Imap_Client_Data_Fetch::HEADER_PARSE); + $mdnHeaders = $fetchResults[0]->getHeaderText('0', Horde_Imap_Client_Data_Fetch::HEADER_PARSE); /** @var Horde_Mime_Headers_Addresses|null $dispositionNotificationTo */ $dispositionNotificationTo = $mdnHeaders->getHeader('disposition-notification-to'); /** @var Horde_Mime_Headers_Addresses|null $originalRecipient */ @@ -482,7 +483,10 @@ class MailTransmission implements IMailTransmission { 'displayed', $account->getMailAccount()->getOutboundHost(), $smtpClient, - ['from_addr' => $account->getEMailAddress()] + [ + 'from_addr' => $account->getEMailAddress(), + 'charset' => 'UTF-8', + ] ); } catch (Horde_Mime_Exception $e) { throw new ServiceException('Unable to send mdn for message "' . $message->getId() . '" caused by: ' . $e->getMessage(), 0, $e); diff --git a/lib/Service/Provisioning/Config.php b/lib/Service/Provisioning/Config.php index 2af2f1827..74b8cda8d 100644 --- a/lib/Service/Provisioning/Config.php +++ b/lib/Service/Provisioning/Config.php @@ -111,6 +111,45 @@ class Config implements JsonSerializable { } /** + * @return boolean + */ + public function getSieveEnabled(): bool { + return (bool)$this->data['sieveEnabled']; + } + + /** + * @return string + */ + public function getSieveHost() { + return $this->data['sieveHost']; + } + + /** + * @return int + */ + public function getSievePort(): int { + return (int)$this->data['sievePort']; + } + + /** + * @param IUser $user + * @return string + */ + public function buildSieveUser(IUser $user) { + if (isset($this->data['sieveUser'])) { + return $this->buildUserEmail($this->data['sieveUser'], $user); + } + return $this->buildEmail($user); + } + + /** + * @return string + */ + public function getSieveSslMode() { + return $this->data['sieveSslMode']; + } + + /** * Replace %USERID% and %EMAIL% to allow special configurations * * @param string $original diff --git a/lib/Service/Provisioning/Manager.php b/lib/Service/Provisioning/Manager.php index 906de385f..742c87f13 100644 --- a/lib/Service/Provisioning/Manager.php +++ b/lib/Service/Provisioning/Manager.php @@ -100,7 +100,12 @@ class Manager { string $smtpUser, string $smtpHost, int $smtpPort, - string $smtpSslMode): void { + string $smtpSslMode, + bool $sieveEnabled, + string $sieveUser, + string $sieveHost, + int $sievePort, + string $sieveSslMode): void { $config = $this->configMapper->save(new Config([ 'active' => true, 'email' => $email, @@ -112,6 +117,11 @@ class Manager { 'smtpHost' => $smtpHost, 'smtpPort' => $smtpPort, 'smtpSslMode' => $smtpSslMode, + 'sieveEnabled' => $sieveEnabled, + 'sieveUser' => $sieveUser, + 'sieveHost' => $sieveHost, + 'sievePort' => $sievePort, + 'sieveSslMode' => $sieveSslMode, ])); $this->provision($config); @@ -119,10 +129,7 @@ class Manager { private function updateAccount(IUser $user, MailAccount $account, Config $config): MailAccount { $account->setEmail($config->buildEmail($user)); - if ($user->getDisplayName() !== $user->getUID()) { - // Only set if it's something meaningful - $account->setName($user->getDisplayName()); - } + $account->setName($user->getDisplayName()); $account->setInboundUser($config->buildImapUser($user)); $account->setInboundHost($config->getImapHost()); $account->setInboundPort($config->getImapPort()); @@ -131,6 +138,19 @@ class Manager { $account->setOutboundHost($config->getSmtpHost()); $account->setOutboundPort($config->getSmtpPort()); $account->setOutboundSslMode($config->getSmtpSslMode()); + $account->setSieveEnabled($config->getSieveEnabled()); + + if ($config->getSieveEnabled()) { + $account->setSieveUser($config->buildSieveUser($user)); + $account->setSieveHost($config->getSieveHost()); + $account->setSievePort($config->getSievePort()); + $account->setSieveSslMode($config->getSieveSslMode()); + } else { + $account->setSieveUser(null); + $account->setSieveHost(null); + $account->setSievePort(null); + $account->setSieveSslMode(null); + } return $account; } diff --git a/lib/Service/SetupService.php b/lib/Service/SetupService.php index 0be5984bf..ffd6146ac 100644 --- a/lib/Service/SetupService.php +++ b/lib/Service/SetupService.php @@ -26,9 +26,14 @@ declare(strict_types=1); namespace OCA\Mail\Service; +use Horde_Imap_Client_Exception; +use Horde_Mail_Exception; +use Horde_Mail_Transport_Smtphorde; use OCA\Mail\Account; use OCA\Mail\Db\MailAccount; +use OCA\Mail\Exception\CouldNotConnectException; use OCA\Mail\Exception\ServiceException; +use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\Service\AutoConfig\AutoConfig; use OCA\Mail\SMTP\SmtpClientFactory; use OCP\Security\ICrypto; @@ -48,6 +53,9 @@ class SetupService { /** @var SmtpClientFactory */ private $smtpClientFactory; + /** @var IMAPClientFactory */ + private $imapClientFactory; + /** var LoggerInterface */ private $logger; @@ -55,11 +63,13 @@ class SetupService { AccountService $accountService, ICrypto $crypto, SmtpClientFactory $smtpClientFactory, + IMAPClientFactory $imapClientFactory, LoggerInterface $logger) { $this->autoConfig = $autoConfig; $this->accountService = $accountService; $this->crypto = $crypto; $this->smtpClientFactory = $smtpClientFactory; + $this->imapClientFactory = $imapClientFactory; $this->logger = $logger; } @@ -124,12 +134,35 @@ class SetupService { $account = new Account($newAccount); $this->logger->debug('Connecting to account {account}', ['account' => $newAccount->getEmail()]); - $transport = $this->smtpClientFactory->create($account); - $account->testConnectivity($transport); + $this->testConnectivity($account); $this->accountService->save($newAccount); $this->logger->debug("account created " . $newAccount->getId()); return $account; } + + /** + * @param Account $account + * @throws CouldNotConnectException + */ + protected function testConnectivity(Account $account): void { + $mailAccount = $account->getMailAccount(); + + $imapClient = $this->imapClientFactory->getClient($account); + try { + $imapClient->login(); + } catch (Horde_Imap_Client_Exception $e) { + throw CouldNotConnectException::create($e, 'IMAP', $mailAccount->getInboundHost(), $mailAccount->getInboundPort()); + } + + $transport = $this->smtpClientFactory->create($account); + if ($transport instanceof Horde_Mail_Transport_Smtphorde) { + try { + $transport->getSMTPObject(); + } catch (Horde_Mail_Exception $e) { + throw CouldNotConnectException::create($e, 'SMTP', $mailAccount->getOutboundHost(), $mailAccount->getOutboundPort()); + } + } + } } diff --git a/lib/Service/TrustedSenderService.php b/lib/Service/TrustedSenderService.php index 8210be686..57cebbfa6 100644 --- a/lib/Service/TrustedSenderService.php +++ b/lib/Service/TrustedSenderService.php @@ -44,7 +44,7 @@ class TrustedSenderService implements ITrustedSenderService { ); } - public function trust(string $uid, string $email, ?bool $trust = true): void { + public function trust(string $uid, string $email, string $type, ?bool $trust = true): void { if ($trust && $this->isTrusted($uid, $email)) { // Nothing to do return; @@ -53,12 +53,14 @@ class TrustedSenderService implements ITrustedSenderService { if ($trust) { $this->mapper->create( $uid, - $email + $email, + $type ); } else { $this->mapper->remove( $uid, - $email + $email, + $type ); } } diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index 532749c85..7e4c47c98 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -61,6 +61,11 @@ class AdminSettings implements ISettings { 'smtpHost' => 'smtp.domain.com', 'smtpPort' => 587, 'smtpSslMode' => 'tls', + 'sieveEnabled' => false, + 'sieveUser' => '%USERID%@domain.com', + 'sieveHost' => 'imap.domain.com', + 'sievePort' => 4190, + 'sieveSslMode' => 'tls', ]) ); diff --git a/lib/Sieve/SieveClientFactory.php b/lib/Sieve/SieveClientFactory.php new file mode 100644 index 000000000..4e7359741 --- /dev/null +++ b/lib/Sieve/SieveClientFactory.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Sieve; + +use Horde\ManageSieve; +use OCA\Mail\Account; +use OCP\IConfig; +use OCP\Security\ICrypto; + +class SieveClientFactory { + + /** @var ICrypto */ + private $crypto; + + /** @var IConfig */ + private $config; + + private $cache = []; + + /** + * @param ICrypto $crypto + * @param IConfig $config + */ + public function __construct(ICrypto $crypto, IConfig $config) { + $this->crypto = $crypto; + $this->config = $config; + } + + /** + * @param Account $account + * @return ManageSieve + * @throws ManageSieve\Exception + */ + public function getClient(Account $account): ManageSieve { + if (!isset($this->cache[$account->getId()])) { + $user = $account->getMailAccount()->getSieveUser(); + if (empty($user)) { + $user = $account->getMailAccount()->getInboundUser(); + } + $password = $account->getMailAccount()->getSievePassword(); + if (empty($password)) { + $password = $account->getMailAccount()->getInboundPassword(); + } + + $this->cache[$account->getId()] = $this->createClient( + $account->getMailAccount()->getSieveHost(), + $account->getMailAccount()->getSievePort(), + $user, + $password, + $account->getMailAccount()->getSieveSslMode() + ); + } + + return $this->cache[$account->getId()]; + } + + /** + * @param string $host + * @param int $port + * @param string $user + * @param string $password + * @param string $sslMode + * @return ManageSieve + * @throws ManageSieve\Exception + */ + public function createClient(string $host, int $port, string $user, string $password, string $sslMode): ManageSieve { + if (empty($sslMode)) { + $sslMode = true; + } elseif ($sslMode === 'none') { + $sslMode = false; + } + + $params = [ + 'host' => $host, + 'port' => $port, + 'user' => $user, + 'password' => $this->crypto->decrypt($password), + 'secure' => $sslMode, + 'timeout' => (int)$this->config->getSystemValue('app.mail.sieve.timeout', 5), + 'context' => [ + 'ssl' => [ + 'verify_peer' => $this->config->getSystemValueBool('app.mail.verify-tls-peer', true), + 'verify_peer_name' => $this->config->getSystemValueBool('app.mail.verify-tls-peer', true), + + ] + ], + ]; + + if ($this->config->getSystemValue('debug', false)) { + $params['logger'] = new SieveLogger($this->config->getSystemValue('datadirectory') . '/horde_sieve.log'); + } + + return new ManageSieve($params); + } +} diff --git a/lib/Sieve/SieveLogger.php b/lib/Sieve/SieveLogger.php new file mode 100644 index 000000000..752467cdf --- /dev/null +++ b/lib/Sieve/SieveLogger.php @@ -0,0 +1,46 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Sieve; + +class SieveLogger { + /** @var resource */ + protected $stream; + + public function __construct(string $logFile) { + $stream = @fopen($logFile, 'ab'); + if ($stream === false) { + throw new \InvalidArgumentException('Unable to use "' . $logFile . '" as log file for sieve.'); + } + $this->stream = $stream; + } + + public function debug(string $message): void { + fwrite($this->stream, $message); + } + + public function __destruct() { + fflush($this->stream); + fclose($this->stream); + } +} |