Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorCyrille Bollu <StCyr@users.noreply.github.com>2021-03-01 15:52:09 +0300
committerGitHub <noreply@github.com>2021-03-01 15:52:09 +0300
commit7019e65f8dad3d9302d281946fc7feb14cbe288c (patch)
tree6477da8e3d4fe05559e8f309e87ba29050732823 /lib
parent065c121c5696b3a9aad1741d29e1e47c38440fbc (diff)
parent42d9bfb0e583a49764524b55f09042290580811e (diff)
Merge branch 'master' into dependabot/composer/vimeo/psalm-4.6.1
Diffstat (limited to 'lib')
-rw-r--r--lib/Command/CreateAccount.php14
-rw-r--r--lib/Command/DeleteAccount.php92
-rw-r--r--lib/Contracts/IMailManager.php8
-rw-r--r--lib/Contracts/ITrustedSenderService.php2
-rw-r--r--lib/Controller/AccountsController.php7
-rwxr-xr-xlib/Controller/MessagesController.php38
-rw-r--r--lib/Controller/SettingsController.php14
-rw-r--r--lib/Controller/SieveController.php219
-rw-r--r--lib/Controller/TrustedSendersController.php12
-rw-r--r--lib/Db/MailAccount.php35
-rw-r--r--lib/Db/MessageMapper.php27
-rw-r--r--lib/Db/TrustedSender.php6
-rw-r--r--lib/Db/TrustedSenderMapper.php23
-rw-r--r--lib/Exception/CouldNotConnectException.php36
-rw-r--r--lib/IMAP/FolderMapper.php14
-rw-r--r--lib/IMAP/MessageMapper.php113
-rw-r--r--lib/Listener/NewMessageClassificationListener.php7
-rw-r--r--lib/Migration/AddSieveToProvisioningConfig.php85
-rw-r--r--lib/Migration/Version1090Date20210127160127.php56
-rw-r--r--lib/Migration/Version1090Date20210216154409.php34
-rw-r--r--lib/Model/IMAPMessage.php18
-rw-r--r--lib/Service/AccountService.php15
-rw-r--r--lib/Service/MailManager.php61
-rw-r--r--lib/Service/MailTransmission.php20
-rw-r--r--lib/Service/Provisioning/Config.php39
-rw-r--r--lib/Service/Provisioning/Manager.php30
-rw-r--r--lib/Service/SetupService.php37
-rw-r--r--lib/Service/TrustedSenderService.php8
-rw-r--r--lib/Settings/AdminSettings.php5
-rw-r--r--lib/Sieve/SieveClientFactory.php116
-rw-r--r--lib/Sieve/SieveLogger.php46
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);
+ }
+}