fix(outbox): add status for messages

and try resending failed messages at POF

Signed-off-by: Anna Larch <anna@nextcloud.com>
This commit is contained in:
Anna Larch 2024-02-26 11:06:54 +01:00
Родитель 95e6ba62a5
Коммит 7c59040785
54 изменённых файлов: 2783 добавлений и 2124 удалений

Просмотреть файл

@ -40,7 +40,6 @@ use OCA\Mail\Dashboard\ImportantMailWidgetV2;
use OCA\Mail\Dashboard\UnreadMailWidget;
use OCA\Mail\Dashboard\UnreadMailWidgetV2;
use OCA\Mail\Events\BeforeImapClientCreated;
use OCA\Mail\Events\BeforeMessageSentEvent;
use OCA\Mail\Events\DraftMessageCreatedEvent;
use OCA\Mail\Events\DraftSavedEvent;
use OCA\Mail\Events\MailboxesSynchronizedEvent;
@ -55,9 +54,7 @@ use OCA\Mail\Http\Middleware\ErrorMiddleware;
use OCA\Mail\Http\Middleware\ProvisioningMiddleware;
use OCA\Mail\Listener\AccountSynchronizedThreadUpdaterListener;
use OCA\Mail\Listener\AddressCollectionListener;
use OCA\Mail\Listener\AntiAbuseListener;
use OCA\Mail\Listener\DeleteDraftListener;
use OCA\Mail\Listener\FlagRepliedMessageListener;
use OCA\Mail\Listener\HamReportListener;
use OCA\Mail\Listener\InteractionListener;
use OCA\Mail\Listener\MailboxesSynchronizedSpecialMailboxesUpdater;
@ -68,7 +65,6 @@ use OCA\Mail\Listener\NewMessageClassificationListener;
use OCA\Mail\Listener\OauthTokenRefreshListener;
use OCA\Mail\Listener\OptionalIndicesListener;
use OCA\Mail\Listener\OutOfOfficeListener;
use OCA\Mail\Listener\SaveSentMessageListener;
use OCA\Mail\Listener\SpamReportListener;
use OCA\Mail\Listener\UserDeletedListener;
use OCA\Mail\Notification\Notifier;
@ -132,7 +128,6 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(AddMissingIndicesEvent::class, OptionalIndicesListener::class);
$context->registerEventListener(BeforeImapClientCreated::class, OauthTokenRefreshListener::class);
$context->registerEventListener(BeforeMessageSentEvent::class, AntiAbuseListener::class);
$context->registerEventListener(DraftSavedEvent::class, DeleteDraftListener::class);
$context->registerEventListener(DraftMessageCreatedEvent::class, DeleteDraftListener::class);
$context->registerEventListener(OutboxMessageCreatedEvent::class, DeleteDraftListener::class);
@ -143,9 +138,7 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(MessageFlaggedEvent::class, MoveJunkListener::class);
$context->registerEventListener(MessageDeletedEvent::class, MessageCacheUpdaterListener::class);
$context->registerEventListener(MessageSentEvent::class, AddressCollectionListener::class);
$context->registerEventListener(MessageSentEvent::class, FlagRepliedMessageListener::class);
$context->registerEventListener(MessageSentEvent::class, InteractionListener::class);
$context->registerEventListener(MessageSentEvent::class, SaveSentMessageListener::class);
$context->registerEventListener(NewMessagesSynchronized::class, NewMessageClassificationListener::class);
$context->registerEventListener(NewMessagesSynchronized::class, MessageKnownSinceListener::class);
$context->registerEventListener(SynchronizationEvent::class, AccountSynchronizedThreadUpdaterListener::class);

Просмотреть файл

@ -95,7 +95,7 @@ class QuotaJob extends TimedJob {
}
$quota = $this->mailManager->getQuota($account);
if($quota === null) {
if ($quota === null) {
$this->logger->debug('Could not get quota information for account <' . $account->getEmail() . '>', ['app' => 'mail']);
return;
}

Просмотреть файл

@ -24,7 +24,6 @@ declare(strict_types=1);
namespace OCA\Mail\Contracts;
use OCA\Mail\Account;
use OCA\Mail\Db\Alias;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\Message;
@ -37,27 +36,12 @@ interface IMailTransmission {
/**
* Send a new message or reply to an existing one
*
* @param NewMessageData $messageData
* @param string|null $repliedToMessageId
* @param Alias|null $alias
* @param Message|null $draft
*
* @param Account $account
* @param LocalMessage $localMessage
* @throws SentMailboxNotSetException
* @throws ServiceException
*/
public function sendMessage(NewMessageData $messageData,
?string $repliedToMessageId = null,
?Alias $alias = null,
?Message $draft = null): void;
/**
* @param Account $account
* @param LocalMessage $message
* @throws ClientException
* @throws ServiceException
* @return void
*/
public function sendLocalMessage(Account $account, LocalMessage $message): void;
public function sendMessage(Account $account, LocalMessage $localMessage): void;
/**
* @param Account $account

Просмотреть файл

@ -172,7 +172,6 @@ class DraftsController extends Controller {
$message = $this->service->getMessage($id, $this->userId);
$account = $this->accountService->find($this->userId, $accountId);
$message->setType(LocalMessage::TYPE_DRAFT);
$message->setAccountId($accountId);
$message->setAliasId($aliasId);

Просмотреть файл

@ -163,7 +163,7 @@ class OutboxController extends Controller {
$outboxMessage = $this->service->convertDraft($draftMessage, $sendAt);
return JsonResponse::success(
return JsonResponse::success(
$outboxMessage,
Http::STATUS_CREATED,
);
@ -209,6 +209,9 @@ class OutboxController extends Controller {
?int $sendAt = null
): JsonResponse {
$message = $this->service->getMessage($id, $this->userId);
if ($message->getStatus() === LocalMessage::STATUS_PROCESSED) {
return JsonResponse::error('Cannot modify already sent message', Http::STATUS_FORBIDDEN, [$message]);
}
$account = $this->accountService->find($this->userId, $accountId);
$message->setAccountId($accountId);
@ -217,7 +220,6 @@ class OutboxController extends Controller {
$message->setBody($body);
$message->setEditorBody($editorBody);
$message->setHtml($isHtml);
$message->setFailed($failed);
$message->setInReplyToMessageId($inReplyToMessageId);
$message->setSendAt($sendAt);
$message->setSmimeSign($smimeSign);
@ -244,8 +246,12 @@ class OutboxController extends Controller {
$message = $this->service->getMessage($id, $this->userId);
$account = $this->accountService->find($this->userId, $message->getAccountId());
$this->service->sendMessage($message, $account);
return JsonResponse::success(
$message = $this->service->sendMessage($message, $account);
if($message->getStatus() !== LocalMessage::STATUS_PROCESSED) {
return JsonResponse::error('Could not send message', Http::STATUS_INTERNAL_SERVER_ERROR, [$message]);
}
return JsonResponse::success(
'Message sent', Http::STATUS_ACCEPTED
);
}

Просмотреть файл

@ -60,13 +60,30 @@ use function array_filter;
* @method setSmimeCertificateId(?int $smimeCertificateId)
* @method bool|null getSmimeEncrypt()
* @method setSmimeEncrypt (bool $smimeEncryt)
* @method int|null getStatus();
* @method setStatus(?int $status);
* @method string|null getRaw()
* @method setRaw(string|null $raw)
*/
class LocalMessage extends Entity implements JsonSerializable {
public const TYPE_OUTGOING = 0;
public const TYPE_DRAFT = 1;
public const STATUS_RAW = 0;
public const STATUS_NO_SENT_MAILBOX = 1;
public const STATUS_SMIME_SIGN_NO_CERT_ID = 2;
public const STATUS_SMIME_SIGN_CERT = 3;
public const STATUS_SMIME_SIGN_FAIL = 4;
public const STATUS_SMIME_ENCRYPT_NO_CERT_ID = 5;
public const STATUS_SMIME_ENCRYPT_CERT = 6;
public const STATUS_SMIME_ENCRYT_FAIL = 7;
public const STATUS_TOO_MANY_RECIPIENTS = 8;
public const STATUS_RATELIMIT = 9;
public const STATUS_SMPT_SEND_FAIL = 10;
public const STATUS_IMAP_SENT_MAILBOX_FAIL = 11;
public const STATUS_PROCESSED = 12;
/**
* @var int
* @var int<1,12>
* @psalm-var self::TYPE_*
*/
protected $type;
@ -116,6 +133,15 @@ class LocalMessage extends Entity implements JsonSerializable {
/** @var bool|null */
protected $smimeEncrypt;
/**
* @var int|null
* @psalm-var int-mask-of<self::STATUS_*>
*/
protected $status;
/** @var string|null */
protected $raw;
public function __construct() {
$this->addType('type', 'integer');
$this->addType('accountId', 'integer');
@ -127,6 +153,7 @@ class LocalMessage extends Entity implements JsonSerializable {
$this->addType('smimeSign', 'boolean');
$this->addType('smimeCertificateId', 'integer');
$this->addType('smimeEncrypt', 'boolean');
$this->addType('status', 'integer');
}
#[ReturnTypeWillChange]
@ -168,6 +195,8 @@ class LocalMessage extends Entity implements JsonSerializable {
'smimeCertificateId' => $this->getSmimeCertificateId(),
'smimeSign' => $this->getSmimeSign() === true,
'smimeEncrypt' => $this->getSmimeEncrypt() === true,
'status' => $this->getStatus(),
'raw' => $this->getRaw(),
];
}

Просмотреть файл

@ -64,7 +64,8 @@ class LocalMessageMapper extends QBMapper {
->join('a', $this->getTableName(), 'm', $qb->expr()->eq('m.account_id', 'a.id'))
->where(
$qb->expr()->eq('a.user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR),
$qb->expr()->eq('m.type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)
$qb->expr()->eq('m.type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
$qb->expr()->neq('m.status', $qb->createNamedParameter(LocalMessage::STATUS_PROCESSED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)
);
$rows = $qb->executeQuery();
@ -134,10 +135,6 @@ class LocalMessageMapper extends QBMapper {
$qb->expr()->isNotNull('send_at'),
$qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
$qb->expr()->lte('send_at', $qb->createNamedParameter($time, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
$qb->expr()->orX(
$qb->expr()->isNull('failed'),
$qb->expr()->eq('failed', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL), IQueryBuilder::PARAM_BOOL),
)
)
->orderBy('send_at', 'asc');
$messages = $this->findEntities($select);

Просмотреть файл

@ -1,95 +0,0 @@
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\Events;
use Horde_Mime_Mail;
use OCA\Mail\Account;
use OCA\Mail\Db\Message;
use OCA\Mail\Model\IMessage;
use OCA\Mail\Model\NewMessageData;
use OCP\EventDispatcher\Event;
/**
* @psalm-immutable
*/
class BeforeMessageSentEvent extends Event {
/** @var Account */
private $account;
/** @var NewMessageData */
private $newMessageData;
/** @var Message|null */
private $draft;
/** @var IMessage */
private $message;
/** @var Horde_Mime_Mail */
private $mail;
/** @var string|null */
private $repliedToMessageId;
public function __construct(Account $account,
NewMessageData $newMessageData,
?string $repliedToMessageId,
?Message $draft,
IMessage $message,
Horde_Mime_Mail $mail) {
parent::__construct();
$this->account = $account;
$this->newMessageData = $newMessageData;
$this->repliedToMessageId = $repliedToMessageId;
$this->draft = $draft;
$this->message = $message;
$this->mail = $mail;
}
public function getAccount(): Account {
return $this->account;
}
public function getNewMessageData(): NewMessageData {
return $this->newMessageData;
}
public function getRepliedToMessageId(): ?string {
return $this->repliedToMessageId;
}
public function getDraft(): ?Message {
return $this->draft;
}
public function getMessage(): IMessage {
return $this->message;
}
public function getMail(): Horde_Mime_Mail {
return $this->mail;
}
}

Просмотреть файл

@ -34,15 +34,15 @@ class DraftSavedEvent extends Event {
/** @var Account */
private $account;
/** @var NewMessageData */
/** @var NewMessageData|null */
private $newMessageData;
/** @var Message|null */
private $draft;
public function __construct(Account $account,
NewMessageData $newMessageData,
?Message $draft) {
?NewMessageData $newMessageData = null,
?Message $draft = null) {
parent::__construct();
$this->account = $account;
$this->newMessageData = $newMessageData;
@ -53,7 +53,7 @@ class DraftSavedEvent extends Event {
return $this->account;
}
public function getNewMessageData(): NewMessageData {
public function getNewMessageData(): ?NewMessageData {
return $this->newMessageData;
}

Просмотреть файл

@ -25,11 +25,8 @@ declare(strict_types=1);
namespace OCA\Mail\Events;
use Horde_Mime_Mail;
use OCA\Mail\Account;
use OCA\Mail\Db\Message;
use OCA\Mail\Model\IMessage;
use OCA\Mail\Model\NewMessageData;
use OCA\Mail\Db\LocalMessage;
use OCP\EventDispatcher\Event;
/**
@ -39,57 +36,17 @@ class MessageSentEvent extends Event {
/** @var Account */
private $account;
/** @var NewMessageData */
private $newMessageData;
/** @var null|string */
private $repliedToMessageId;
/** @var Message|null */
private $draft;
/** @var IMessage */
private $message;
/** @var Horde_Mime_Mail */
private $mail;
public function __construct(Account $account,
NewMessageData $newMessageData,
?string $repliedToMessageId,
?Message $draft,
IMessage $message,
Horde_Mime_Mail $mail) {
private LocalMessage $localMessage) {
parent::__construct();
$this->account = $account;
$this->newMessageData = $newMessageData;
$this->repliedToMessageId = $repliedToMessageId;
$this->draft = $draft;
$this->message = $message;
$this->mail = $mail;
}
public function getAccount(): Account {
return $this->account;
}
public function getNewMessageData(): NewMessageData {
return $this->newMessageData;
}
public function getRepliedToMessageId(): ?string {
return $this->repliedToMessageId;
}
public function getDraft(): ?Message {
return $this->draft;
}
public function getMessage(): IMessage {
return $this->message;
}
public function getMail(): Horde_Mime_Mail {
return $this->mail;
public function getLocalMessage(): LocalMessage {
return $this->localMessage;
}
}

Просмотреть файл

@ -35,7 +35,6 @@ use Horde_Imap_Client_Search_Query;
use Horde_Imap_Client_Socket;
use Horde_Mime_Exception;
use Horde_Mime_Headers;
use Horde_Mime_Mail;
use Horde_Mime_Part;
use Html2Text\Html2Text;
use OCA\Mail\Attachment;
@ -398,7 +397,7 @@ class MessageMapper {
*/
public function save(Horde_Imap_Client_Socket $client,
Mailbox $mailbox,
Horde_Mime_Mail $mail,
string $mail,
array $flags = []): int {
$flags = array_merge([
Horde_Imap_Client::FLAG_SEEN,
@ -408,7 +407,7 @@ class MessageMapper {
$mailbox->getName(),
[
[
'data' => $mail->getRaw(),
'data' => $mail,
'flags' => $flags,
]
]

Просмотреть файл

@ -26,8 +26,10 @@ declare(strict_types=1);
namespace OCA\Mail\Listener;
use OCA\Mail\Contracts\IUserPreferences;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Events\MessageSentEvent;
use OCA\Mail\Service\AutoCompletion\AddressCollector;
use OCA\Mail\Service\TransmissionService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use Psr\Log\LoggerInterface;
@ -48,7 +50,8 @@ class AddressCollectionListener implements IEventListener {
public function __construct(IUserPreferences $preferences,
AddressCollector $collector,
LoggerInterface $logger) {
LoggerInterface $logger,
private TransmissionService $transmissionService) {
$this->collector = $collector;
$this->logger = $logger;
$this->preferences = $preferences;
@ -65,10 +68,12 @@ class AddressCollectionListener implements IEventListener {
// Non-essential feature, hence we catch all possible errors
try {
$message = $event->getMessage();
$addresses = $message->getTo()
->merge($message->getCC())
->merge($message->getBCC());
$message = $event->getLocalMessage();
$to = $this->transmissionService->getAddressList($message, Recipient::TYPE_TO);
$cc = $this->transmissionService->getAddressList($message, Recipient::TYPE_CC);
$bcc = $this->transmissionService->getAddressList($message, Recipient::TYPE_BCC);
$addresses = $to->merge($cc)->merge($bcc);
$this->collector->addAddresses($event->getAccount()->getUserId(), $addresses);
} catch (Throwable $e) {

Просмотреть файл

@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\Listener;
use OCA\Mail\Events\BeforeMessageSentEvent;
use OCA\Mail\Service\AntiAbuseService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
/**
* @template-implements IEventListener<Event|BeforeMessageSentEvent>
*/
class AntiAbuseListener implements IEventListener {
/** @var IUserManager */
private $userManager;
/** @var AntiAbuseService */
private $service;
/** @var LoggerInterface */
private $logger;
public function __construct(IUserManager $userManager,
AntiAbuseService $service,
LoggerInterface $logger) {
$this->service = $service;
$this->userManager = $userManager;
$this->logger = $logger;
}
public function handle(Event $event): void {
if (!($event instanceof BeforeMessageSentEvent)) {
return;
}
$user = $this->userManager->get($event->getAccount()->getUserId());
if ($user === null) {
$this->logger->error('User {user} for mail account {id} does not exist', [
'user' => $event->getAccount()->getUserId(),
'id' => $event->getAccount()->getId(),
]);
return;
}
$this->service->onBeforeMessageSent(
$user,
$event->getNewMessageData(),
);
}
}

Просмотреть файл

@ -1,114 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\Listener;
use Horde_Imap_Client;
use Horde_Imap_Client_Exception;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\MessageMapper as DbMessageMapper;
use OCA\Mail\Events\MessageSentEvent;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use Psr\Log\LoggerInterface;
/**
* @template-implements IEventListener<Event|MessageSentEvent>
*/
class FlagRepliedMessageListener implements IEventListener {
/** @var IMAPClientFactory */
private $imapClientFactory;
/** @var MailboxMapper */
private $mailboxMapper;
/** @var MessageMapper */
private $messageMapper;
/** @var LoggerInterface */
private $logger;
/** @var DbMessageMapper */
private $dbMessageMapper;
public function __construct(IMAPClientFactory $imapClientFactory,
MailboxMapper $mailboxMapper,
DbMessageMapper $dbMessageMapper,
MessageMapper $mapper,
LoggerInterface $logger) {
$this->imapClientFactory = $imapClientFactory;
$this->mailboxMapper = $mailboxMapper;
$this->dbMessageMapper = $dbMessageMapper;
$this->messageMapper = $mapper;
$this->logger = $logger;
}
public function handle(Event $event): void {
if (!($event instanceof MessageSentEvent) || $event->getRepliedToMessageId() === null) {
return;
}
$messages = $this->dbMessageMapper->findByMessageId($event->getAccount(), $event->getRepliedToMessageId());
if ($messages === []) {
return;
}
try {
$client = $this->imapClientFactory->getClient($event->getAccount());
foreach ($messages as $message) {
try {
$mailbox = $this->mailboxMapper->findById($message->getMailboxId());
//ignore read-only mailboxes
if ($mailbox->getMyAcls() !== null && !strpos($mailbox->getMyAcls(), "w")) {
continue;
}
// ignore drafts and sent
if ($mailbox->isSpecialUse('sent') || $mailbox->isSpecialUse('drafts')) {
continue;
}
// Mark all other mailboxes that contain the message with the same imap message id as replied
$this->messageMapper->addFlag(
$client,
$mailbox,
[$message->getUid()],
Horde_Imap_Client::FLAG_ANSWERED
);
} catch (DoesNotExistException | Horde_Imap_Client_Exception $e) {
$this->logger->warning('Could not flag replied message: ' . $e, [
'exception' => $e,
]);
}
$message->setFlagAnswered(true);
$this->dbMessageMapper->update($message);
}
} finally {
$client->logout();
}
}
}

Просмотреть файл

@ -70,16 +70,19 @@ class InteractionListener implements IEventListener {
$this->logger->debug('no user object found');
return;
}
$recipients = $event->getMessage()->getTo()
->merge($event->getMessage()->getCC())
->merge($event->getMessage()->getBCC());
foreach ($recipients->iterate() as $recipient) {
$message = $event->getLocalMessage();
$emails = [];
foreach ($message->getRecipients() as $recipient) {
if (in_array($recipient->getEmail(), $emails)) {
continue;
}
$interactionEvent = new ContactInteractedWithEvent($user);
$email = $recipient->getEmail();
if ($email === null) {
// Weird, bot ok
continue;
}
$emails[] = $email;
$interactionEvent->setEmail($email);
$this->dispatcher->dispatch(ContactInteractedWithEvent::class, $interactionEvent);
}

Просмотреть файл

@ -1,101 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\Listener;
use Horde_Imap_Client_Exception;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Events\MessageSentEvent;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use Psr\Log\LoggerInterface;
/**
* @template-implements IEventListener<Event|MessageSentEvent>
*/
class SaveSentMessageListener implements IEventListener {
/** @var MailboxMapper */
private $mailboxMapper;
/** @var IMAPClientFactory */
private $imapClientFactory;
/** @var MessageMapper */
private $messageMapper;
/** @var LoggerInterface */
private $logger;
public function __construct(MailboxMapper $mailboxMapper,
IMAPClientFactory $imapClientFactory,
MessageMapper $messageMapper,
LoggerInterface $logger) {
$this->mailboxMapper = $mailboxMapper;
$this->imapClientFactory = $imapClientFactory;
$this->messageMapper = $messageMapper;
$this->logger = $logger;
}
public function handle(Event $event): void {
if (!($event instanceof MessageSentEvent)) {
return;
}
$sentMailboxId = $event->getAccount()->getMailAccount()->getSentMailboxId();
if ($sentMailboxId === null) {
$this->logger->warning("No sent mailbox exists, can't save sent message");
return;
}
// Save the message in the sent mailbox
try {
$sentMailbox = $this->mailboxMapper->findById(
$sentMailboxId
);
} catch (DoesNotExistException $e) {
$this->logger->error("Sent mailbox could not be found", [
'exception' => $e,
]);
return;
}
$client = $this->imapClientFactory->getClient($event->getAccount());
try {
$this->messageMapper->save(
$client,
$sentMailbox,
$event->getMail()
);
} catch (Horde_Imap_Client_Exception $e) {
throw new ServiceException('Could not save sent message on IMAP', 0, $e);
} finally {
$client->logout();
}
}
}

Просмотреть файл

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* @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\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version3600Date20240220134813 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$localMessagesTable = $schema->getTable('mail_local_messages');
if (!$localMessagesTable->hasColumn('status')) {
$localMessagesTable->addColumn('status', Types::INTEGER, [
'notnull' => false,
'default' => 0,
]);
}
if (!$localMessagesTable->hasColumn('raw')) {
$localMessagesTable->addColumn('raw', Types::TEXT, [
'notnull' => false,
'default' => null,
]);
}
return $schema;
}
}

43
lib/Send/AHandler.php Normal file
Просмотреть файл

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Mail\Send;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
abstract class AHandler {
protected AHandler|null $next = null;
public function setNext(AHandler $next): AHandler {
$this->next = $next;
return $next;
}
abstract public function process(Account $account, LocalMessage $localMessage): LocalMessage;
protected function processNext(Account $account, LocalMessage $localMessage): LocalMessage {
if ($this->next !== null) {
return $this->next->process($account, $localMessage);
}
return $localMessage;
}
}

Просмотреть файл

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Mail\Send;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Service\AntiAbuseService;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
class AntiAbuseHandler extends AHandler {
public function __construct(private IUserManager $userManager,
private AntiAbuseService $service,
private LoggerInterface $logger) {
}
public function process(Account $account, LocalMessage $localMessage): LocalMessage {
if ($localMessage->getStatus() === LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL
|| $localMessage->getStatus() === LocalMessage::STATUS_PROCESSED) {
return $this->processNext($account, $localMessage);
}
$user = $this->userManager->get($account->getUserId());
if ($user === null) {
$this->logger->error('User {user} for mail account {id} does not exist', [
'user' => $account->getUserId(),
'id' => $account->getId(),
]);
// What to do here?
return $localMessage;
}
$this->service->onBeforeMessageSent(
$user,
$localMessage,
);
// We don't react to a ratelimited message / a message that has too many recipients
// at this point.
// Any future improvement from https://github.com/nextcloud/mail/issues/6461
// should refactor the chain to stop at this point unless the force send option is true
return $this->processNext($account, $localMessage);
}
}

56
lib/Send/Chain.php Normal file
Просмотреть файл

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Mail\Send;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Service\Attachment\AttachmentService;
class Chain {
public function __construct(private SentMailboxHandler $sentMailboxHandler,
private AntiAbuseHandler $antiAbuseHandler,
private SendHandler $sendHandler,
private CopySentMessageHandler $copySentMessageHandler,
private FlagRepliedMessageHandler $flagRepliedMessageHandler,
private AttachmentService $attachmentService,
private LocalMessageMapper $localMessageMapper,
) {
}
public function process(Account $account, LocalMessage $localMessage): void {
$handlers = $this->sentMailboxHandler;
$handlers->setNext($this->antiAbuseHandler)
->setNext($this->sendHandler)
->setNext($this->copySentMessageHandler)
->setNext($this->flagRepliedMessageHandler);
$result = $handlers->process($account, $localMessage);
if ($result->getStatus() === LocalMessage::STATUS_PROCESSED) {
$this->attachmentService->deleteLocalMessageAttachments($account->getUserId(), $result->getId());
$this->localMessageMapper->deleteWithRecipients($result);
return;
}
$this->localMessageMapper->update($result);
}
}

Просмотреть файл

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Mail\Send;
use Horde_Imap_Client_Exception;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use Psr\Log\LoggerInterface;
class CopySentMessageHandler extends AHandler {
public function __construct(private IMAPClientFactory $imapClientFactory,
private MailboxMapper $mailboxMapper,
private LoggerInterface $logger,
private MessageMapper $messageMapper
) {
}
public function process(Account $account, LocalMessage $localMessage): LocalMessage {
if ($localMessage->getStatus() === LocalMessage::STATUS_PROCESSED) {
return $this->processNext($account, $localMessage);
}
$sentMailboxId = $account->getMailAccount()->getSentMailboxId();
if ($sentMailboxId === null) {
// We can't write the "sent mailbox" status here bc that would trigger an additional send.
// Thus, we leave the "imap copy to sent mailbox" status.
$localMessage->setStatus(LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL);
$this->logger->warning("No sent mailbox exists, can't save sent message");
return $localMessage;
}
// Save the message in the sent mailbox
try {
$sentMailbox = $this->mailboxMapper->findById(
$sentMailboxId
);
} catch (DoesNotExistException $e) {
// We can't write the "sent mailbox" status here bc that would trigger an additional send.
// Thus, we leave the "imap copy to sent mailbox" status.
$localMessage->setStatus(LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL);
$this->logger->error('Sent mailbox could not be found', [
'exception' => $e,
]);
return $localMessage;
}
$client = $this->imapClientFactory->getClient($account);
try {
$this->messageMapper->save(
$client,
$sentMailbox,
$localMessage->getRaw()
);
$localMessage->setStatus(LocalMessage::STATUS_PROCESSED);
} catch (Horde_Imap_Client_Exception $e) {
$localMessage->setStatus(LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL);
$this->logger->error('Could not copy message to sent mailbox', [
'exception' => $e,
]);
return $localMessage;
} finally {
$client->logout();
}
return $this->processNext($account, $localMessage);
}
}

Просмотреть файл

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Mail\Send;
use Horde_Imap_Client;
use Horde_Imap_Client_Exception;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\MessageMapper as DbMessageMapper;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use Psr\Log\LoggerInterface;
class FlagRepliedMessageHandler extends AHandler {
public function __construct(private IMAPClientFactory $imapClientFactory,
private MailboxMapper $mailboxMapper,
private LoggerInterface $logger,
private MessageMapper $messageMapper,
private DbMessageMapper $dbMessageMapper,
) {
}
public function process(Account $account, LocalMessage $localMessage): LocalMessage {
if ($localMessage->getStatus() !== LocalMessage::STATUS_PROCESSED) {
return $localMessage;
}
if ($localMessage->getInReplyToMessageId() === null) {
return $this->processNext($account, $localMessage);
}
$messages = $this->dbMessageMapper->findByMessageId($account, $localMessage->getInReplyToMessageId());
if ($messages === []) {
return $this->processNext($account, $localMessage);
}
try {
$client = $this->imapClientFactory->getClient($account);
foreach ($messages as $message) {
try {
$mailbox = $this->mailboxMapper->findById($message->getMailboxId());
//ignore read-only mailboxes
if ($mailbox->getMyAcls() !== null && !strpos($mailbox->getMyAcls(), 'w')) {
continue;
}
// ignore drafts and sent
if ($mailbox->isSpecialUse('sent') || $mailbox->isSpecialUse('drafts')) {
continue;
}
// Mark all other mailboxes that contain the message with the same imap message id as replied
$this->messageMapper->addFlag(
$client,
$mailbox,
[$message->getUid()],
Horde_Imap_Client::FLAG_ANSWERED
);
$message->setFlagAnswered(true);
$this->dbMessageMapper->update($message);
} catch (DoesNotExistException|Horde_Imap_Client_Exception $e) {
$this->logger->warning('Could not flag replied message: ' . $e, [
'exception' => $e,
]);
}
}
} finally {
$client->logout();
}
return $this->processNext($account, $localMessage);
}
}

48
lib/Send/SendHandler.php Normal file
Просмотреть файл

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Mail\Send;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Db\LocalMessage;
class SendHandler extends AHandler {
public function __construct(private IMailTransmission $transmission,
) {
}
public function process(Account $account, LocalMessage $localMessage): LocalMessage {
if ($localMessage->getStatus() === LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL
|| $localMessage->getStatus() === LocalMessage::STATUS_PROCESSED) {
return $this->processNext($account, $localMessage);
}
$this->transmission->sendMessage($account, $localMessage);
if ($localMessage->getStatus() === LocalMessage::STATUS_RAW || $localMessage->getStatus() === null) {
return $this->processNext($account, $localMessage);
}
// Something went wrong during the sending
return $localMessage;
}
}

Просмотреть файл

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Mail\Send;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
class SentMailboxHandler extends AHandler {
public function process(Account $account, LocalMessage $localMessage): LocalMessage {
if ($account->getMailAccount()->getSentMailboxId() === null) {
$localMessage->setStatus(LocalMessage::STATUS_NO_SENT_MAILBOX);
return $localMessage;
}
return $this->processNext($account, $localMessage);
}
}

Просмотреть файл

@ -26,7 +26,7 @@ declare(strict_types=1);
namespace OCA\Mail\Service;
use OCA\Mail\AppInfo\Application;
use OCA\Mail\Model\NewMessageData;
use OCA\Mail\Db\LocalMessage;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\ICacheFactory;
use OCP\IConfig;
@ -58,8 +58,7 @@ class AntiAbuseService {
$this->logger = $logger;
}
public function onBeforeMessageSent(IUser $user,
NewMessageData $messageData): void {
public function onBeforeMessageSent(IUser $user, LocalMessage $localMessage): void {
$abuseDetection = $this->config->getAppValue(
Application::APP_ID,
'abuse_detection',
@ -70,12 +69,11 @@ class AntiAbuseService {
return;
}
$this->checkNumberOfRecipients($user, $messageData);
$this->checkRateLimits($user, $messageData);
$this->checkNumberOfRecipients($user, $localMessage);
$this->checkRateLimits($user, $localMessage);
}
private function checkNumberOfRecipients(IUser $user,
NewMessageData $messageData): void {
private function checkNumberOfRecipients(IUser $user, LocalMessage $message): void {
$numberOfRecipientsThreshold = (int)$this->config->getAppValue(
Application::APP_ID,
'abuse_number_of_recipients_per_message_threshold',
@ -85,11 +83,10 @@ class AntiAbuseService {
return;
}
$actualNumberOfRecipients = count($messageData->getTo())
+ count($messageData->getCc())
+ count($messageData->getBcc());
$actualNumberOfRecipients = count($message->getRecipients());
if ($actualNumberOfRecipients >= $numberOfRecipientsThreshold) {
$message->setStatus(LocalMessage::STATUS_TOO_MANY_RECIPIENTS);
$this->logger->alert('User {user} sends to a suspicious number of recipients. {expected} are allowed. {actual} are used', [
'user' => $user->getUID(),
'expected' => $numberOfRecipientsThreshold,
@ -98,8 +95,7 @@ class AntiAbuseService {
}
}
private function checkRateLimits(IUser $user,
NewMessageData $messageData): void {
private function checkRateLimits(IUser $user, LocalMessage $message): void {
if (!$this->cacheFactory->isAvailable()) {
// No cache, no rate limits
return;
@ -110,16 +106,21 @@ class AntiAbuseService {
return;
}
$this->checkRateLimitsForPeriod($user, $messageData, $cache, '15m', 15 * 60);
$this->checkRateLimitsForPeriod($user, $messageData, $cache, '1h', 60 * 60);
$this->checkRateLimitsForPeriod($user, $messageData, $cache, '1d', 24 * 60 * 60);
$ratelimited = (
$this->checkRateLimitsForPeriod($user, $cache, '15m', 15 * 60, $message) ||
$this->checkRateLimitsForPeriod($user, $cache, '1h', 60 * 60, $message) ||
$this->checkRateLimitsForPeriod($user, $cache, '1d', 24 * 60 * 60, $message)
);
if ($ratelimited) {
$message->setStatus(LocalMessage::STATUS_RATELIMIT);
}
}
private function checkRateLimitsForPeriod(IUser $user,
NewMessageData $messageData,
IMemcache $cache,
string $id,
int $period): void {
int $period,
LocalMessage $message): bool {
$maxNumberOfMessages = (int)$this->config->getAppValue(
Application::APP_ID,
'abuse_number_of_messages_per_' . $id,
@ -127,7 +128,7 @@ class AntiAbuseService {
);
if ($maxNumberOfMessages === 0) {
// No limit set
return;
return false;
}
$now = $this->timeFactory->getTime();
@ -136,7 +137,7 @@ class AntiAbuseService {
$periodStart = ((int)($now / $period)) * $period;
$cacheKey = implode('_', ['counter', $id, $periodStart]);
$cache->add($cacheKey, 0);
$counter = $cache->inc($cacheKey, count($messageData->getTo()) + count($messageData->getCc()) + count($messageData->getBcc()));
$counter = $cache->inc($cacheKey, count($message->getRecipients()));
if ($counter >= $maxNumberOfMessages) {
$this->logger->alert('User {user} sends a supcious number of messages within {period}. {expected} are allowed. {actual} have been sent', [
@ -145,6 +146,8 @@ class AntiAbuseService {
'expected' => $maxNumberOfMessages,
'actual' => $counter,
]);
return true;
}
return false;
}
}

Просмотреть файл

@ -25,34 +25,36 @@ declare(strict_types=1);
namespace OCA\Mail\Service;
use Horde_Imap_Client_Exception;
use Horde_Mime_Exception;
use Horde_Mime_Mail;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\Exception\SentMailboxNotSetException;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Model\NewMessageData;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
use OCA\Mail\Service\DataUri\DataUriParser;
use OCA\Mail\SMTP\SmtpClientFactory;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
class AntiSpamService {
private const NAME = 'antispam_reporting';
private const MESSAGE_TYPE = 'message/rfc822';
/** @var IConfig */
private $config;
/** @var MessageMapper */
private $messageMapper;
/** @var IMailTransmission */
private $transmission;
public function __construct(IConfig $config,
MessageMapper $messageMapper,
IMailTransmission $transmission) {
$this->config = $config;
$this->messageMapper = $messageMapper;
$this->transmission = $transmission;
public function __construct(private IConfig $config,
private MessageMapper $dbMessageMapper,
private MailManager $mailManager,
private IMAPClientFactory $imapClientFactory,
private SmtpClientFactory $smtpClientFactory,
private ImapMessageMapper $messageMapper,
private LoggerInterface $logger,
) {
}
public function getSpamEmail(): string {
@ -99,25 +101,126 @@ class AntiSpamService {
$subject = ($flag === '$junk') ? $this->getSpamSubject() : $this->getHamSubject();
// Message to attach not found
$messageId = $this->messageMapper->getIdForUid($mailbox, $uid);
$messageId = $this->dbMessageMapper->getIdForUid($mailbox, $uid);
if ($messageId === null) {
throw new ServiceException('Could not find reported message');
}
$messageData = NewMessageData::fromRequest(
$account,
$reportEmail,
null,
null,
$subject,
$subject, // add any message body - not all IMAP servers accept empty emails
[['id' => $messageId, 'type' => self::MESSAGE_TYPE]]
if ($account->getMailAccount()->getSentMailboxId() === null) {
throw new ServiceException('Could not find sent mailbox');
}
$message = $account->newMessage();
$from = new AddressList([
Address::fromRaw($account->getName(), $account->getEMailAddress()),
]);
$to = new AddressList([
Address::fromRaw($reportEmail, $reportEmail),
]);
$message->setTo($to);
$message->setSubject($subject);
$message->setFrom($from);
$message->setContent($subject);
// Gets original of other message
$userId = $account->getMailAccount()->getUserId();
try {
$attachmentMessage = $this->mailManager->getMessage($userId, $messageId);
} catch (DoesNotExistException $e) {
$this->logger->error('Could not find reported email with message ID #' . $messageId, ['exception' => $e]);
return;
}
$mailbox = $this->mailManager->getMailbox($userId, $attachmentMessage->getMailboxId());
$client = $this->imapClientFactory->getClient($account);
try {
$fullText = $this->messageMapper->getFullText(
$client,
$mailbox->getName(),
$attachmentMessage->getUid(),
$userId
);
} finally {
$client->logout();
}
$message->addEmbeddedMessageAttachment(
$attachmentMessage->getSubject() . '.eml',
$fullText
);
$transport = $this->smtpClientFactory->create($account);
// build mime body
$headers = [
'From' => $message->getFrom()->first()->toHorde(),
'To' => $message->getTo()->toHorde(),
'Cc' => $message->getCC()->toHorde(),
'Bcc' => $message->getBCC()->toHorde(),
'Subject' => $message->getSubject(),
];
if (($inReplyTo = $message->getInReplyTo()) !== null) {
$headers['References'] = $inReplyTo;
$headers['In-Reply-To'] = $inReplyTo;
}
$mail = new Horde_Mime_Mail();
$mail->addHeaders($headers);
$mimeMessage = new MimeMessage(
new DataUriParser()
);
$mimePart = $mimeMessage->build(
true,
$message->getContent(),
$message->getAttachments()
);
$mail->setBasePart($mimePart);
// Send the message
try {
$this->transmission->sendMessage($messageData);
} catch (SentMailboxNotSetException | ServiceException $e) {
throw new ServiceException('Could not send report email from anti spam email service', 0, $e);
$mail->send($transport, false, false);
} catch (Horde_Mime_Exception $e) {
throw new ServiceException(
'Could not send message: ' . $e->getMessage(),
$e->getCode(),
$e
);
}
$sentMailboxId = $account->getMailAccount()->getSentMailboxId();
if ($sentMailboxId === null) {
$this->logger->warning("No sent mailbox exists, can't save sent message");
return;
}
// Save the message in the sent mailbox
try {
$sentMailbox = $this->mailManager->getMailbox(
$account->getUserId(),
$sentMailboxId
);
} catch (ClientException $e) {
$this->logger->error('Sent mailbox could not be found', [
'exception' => $e,
]);
return;
}
$client = $this->imapClientFactory->getClient($account);
try {
$this->messageMapper->save(
$client,
$sentMailbox,
$mail->getRaw(false)
);
} catch (Horde_Imap_Client_Exception $e) {
$this->logger->error('Could not move report email to sent mailbox, but the report email was sent. Reported email was id: #' . $messageId, ['exception' => $e]);
} finally {
$client->logout();
}
}
}

Просмотреть файл

@ -119,6 +119,10 @@ class DraftsService {
throw new ClientException('Cannot convert message to outbox message without at least one recipient');
}
// Explicitly reset the status, so we can try sending from scratch again
// in case the user has updated a failing component
$message->setStatus(LocalMessage::STATUS_RAW);
$message = $this->mapper->saveWithRecipients($message, $toRecipients, $ccRecipients, $bccRecipients);
if ($attachments === []) {

Просмотреть файл

@ -41,156 +41,79 @@ use Horde_Mime_Mdn;
use OCA\Mail\Account;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
use OCA\Mail\Contracts\IAttachmentService;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Db\Alias;
use OCA\Mail\Db\LocalAttachment;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Events\BeforeMessageSentEvent;
use OCA\Mail\Events\DraftSavedEvent;
use OCA\Mail\Events\MessageSentEvent;
use OCA\Mail\Events\SaveDraftEvent;
use OCA\Mail\Exception\AttachmentNotFoundException;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\SentMailboxNotSetException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Exception\SmimeEncryptException;
use OCA\Mail\Exception\SmimeSignException;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCA\Mail\Model\IMessage;
use OCA\Mail\Model\NewMessageData;
use OCA\Mail\Service\DataUri\DataUriParser;
use OCA\Mail\SMTP\SmtpClientFactory;
use OCA\Mail\Support\PerformanceLogger;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
use OCP\Files\Folder;
use Psr\Log\LoggerInterface;
use function array_filter;
use function array_map;
class MailTransmission implements IMailTransmission {
private SmimeService $smimeService;
/** @var Folder */
private $userFolder;
/** @var IAttachmentService */
private $attachmentService;
/** @var IMailManager */
private $mailManager;
/** @var IMAPClientFactory */
private $imapClientFactory;
/** @var SmtpClientFactory */
private $smtpClientFactory;
/** @var IEventDispatcher */
private $eventDispatcher;
/** @var MailboxMapper */
private $mailboxMapper;
/** @var MessageMapper */
private $messageMapper;
/** @var LoggerInterface */
private $logger;
/** @var PerformanceLogger */
private $performanceLogger;
/** @var AliasesService */
private $aliasesService;
/** @var GroupsIntegration */
private $groupsIntegration;
/**
* @param Folder $userFolder
*/
public function __construct($userFolder,
IAttachmentService $attachmentService,
IMailManager $mailManager,
IMAPClientFactory $imapClientFactory,
SmtpClientFactory $smtpClientFactory,
IEventDispatcher $eventDispatcher,
MailboxMapper $mailboxMapper,
MessageMapper $messageMapper,
LoggerInterface $logger,
PerformanceLogger $performanceLogger,
AliasesService $aliasesService,
GroupsIntegration $groupsIntegration,
SmimeService $smimeService) {
$this->userFolder = $userFolder;
$this->attachmentService = $attachmentService;
$this->mailManager = $mailManager;
$this->imapClientFactory = $imapClientFactory;
$this->smtpClientFactory = $smtpClientFactory;
$this->eventDispatcher = $eventDispatcher;
$this->mailboxMapper = $mailboxMapper;
$this->messageMapper = $messageMapper;
$this->logger = $logger;
$this->performanceLogger = $performanceLogger;
$this->aliasesService = $aliasesService;
$this->groupsIntegration = $groupsIntegration;
$this->smimeService = $smimeService;
public function __construct(
private IMAPClientFactory $imapClientFactory,
private SmtpClientFactory $smtpClientFactory,
private IEventDispatcher $eventDispatcher,
private MailboxMapper $mailboxMapper,
private MessageMapper $messageMapper,
private LoggerInterface $logger,
private PerformanceLogger $performanceLogger,
private AliasesService $aliasesService,
private TransmissionService $transmissionService
) {
}
public function sendMessage(NewMessageData $messageData,
?string $repliedToMessageId = null,
?Alias $alias = null,
?Message $draft = null): void {
$account = $messageData->getAccount();
if ($account->getMailAccount()->getSentMailboxId() === null) {
throw new SentMailboxNotSetException();
}
public function sendMessage(Account $account, LocalMessage $localMessage): void {
$to = $this->transmissionService->getAddressList($localMessage, Recipient::TYPE_TO);
$cc = $this->transmissionService->getAddressList($localMessage, Recipient::TYPE_CC);
$bcc = $this->transmissionService->getAddressList($localMessage, Recipient::TYPE_BCC);
$attachments = $this->transmissionService->getAttachments($localMessage);
if ($repliedToMessageId !== null) {
$message = $this->buildReplyMessage($account, $messageData, $repliedToMessageId);
} else {
$message = $this->buildNewMessage($account, $messageData);
$alias = null;
if ($localMessage->getAliasId() !== null) {
$alias = $this->aliasesService->find($localMessage->getAliasId(), $account->getUserId());
}
$account->setAlias($alias);
$fromEmail = $alias ? $alias->getAlias() : $account->getEMailAddress();
$from = new AddressList([
Address::fromRaw($account->getName(), $fromEmail),
]);
$message->setFrom($from);
$message->setCC($messageData->getCc());
$message->setBcc($messageData->getBcc());
$message->setContent($messageData->getBody());
$this->handleAttachments($account, $messageData, $message); // only ever going to be local attachments
$attachmentParts = [];
foreach ($attachments as $attachment) {
$part = $this->transmissionService->handleAttachment($account, $attachment);
if ($part !== null) {
$attachmentParts[] = $part;
}
}
$transport = $this->smtpClientFactory->create($account);
// build mime body
$headers = [
'From' => $message->getFrom()->first()->toHorde(),
'To' => $message->getTo()->toHorde(),
'Cc' => $message->getCC()->toHorde(),
'Bcc' => $message->getBCC()->toHorde(),
'Subject' => $message->getSubject(),
'From' => $from->first()->toHorde(),
'To' => $to->toHorde(),
'Cc' => $cc->toHorde(),
'Bcc' => $bcc->toHorde(),
'Subject' => $localMessage->getSubject(),
];
if (($inReplyTo = $message->getInReplyTo()) !== null) {
if (($inReplyTo = $localMessage->getInReplyToMessageId()) !== null) {
$headers['References'] = $inReplyTo;
$headers['In-Reply-To'] = $inReplyTo;
}
if ($messageData->isMdnRequested()) {
$headers[Horde_Mime_Mdn::MDN_HEADER] = $message->getFrom()->first()->toHorde();
}
$mail = new Horde_Mime_Mail();
$mail->addHeaders($headers);
@ -198,170 +121,59 @@ class MailTransmission implements IMailTransmission {
new DataUriParser()
);
$mimePart = $mimeMessage->build(
$messageData->isHtml(),
$message->getContent(),
$message->getAttachments()
$localMessage->isHtml(),
$localMessage->getBody(),
$attachmentParts
);
// TODO: add smimeEncrypt check if implemented
if ($messageData->getSmimeSign()) {
if ($messageData->getSmimeCertificateId() === null) {
throw new ServiceException('Could not send message: Requested S/MIME signature without certificate id');
}
try {
$certificate = $this->smimeService->findCertificate(
$messageData->getSmimeCertificateId(),
$account->getUserId(),
);
$mimePart = $this->smimeService->signMimePart($mimePart, $certificate);
} catch (DoesNotExistException $e) {
throw new ServiceException(
'Could not send message: Certificate does not exist: ' . $e->getMessage(),
$e->getCode(),
$e,
);
} catch (SmimeSignException | ServiceException $e) {
throw new ServiceException(
'Could not send message: Failed to sign MIME part: ' . $e->getMessage(),
$e->getCode(),
$e,
);
}
}
if ($messageData->getSmimeEncrypt()) {
if ($messageData->getSmimeCertificateId() === null) {
throw new ServiceException('Could not send message: Requested S/MIME signature without certificate id');
}
try {
$addressList = $messageData->getTo()
->merge($messageData->getCc())
->merge($messageData->getBcc());
$certificates = $this->smimeService->findCertificatesByAddressList($addressList, $account->getUserId());
$senderCertificate = $this->smimeService->findCertificate($messageData->getSmimeCertificateId(), $account->getUserId());
$certificates[] = $senderCertificate;
$mimePart = $this->smimeService->encryptMimePart($mimePart, $certificates);
} catch (DoesNotExistException $e) {
throw new ServiceException(
'Could not send message: Certificate does not exist: ' . $e->getMessage(),
$e->getCode(),
$e,
);
} catch (SmimeEncryptException | ServiceException $e) {
throw new ServiceException(
'Could not send message: Failed to encrypt MIME part: ' . $e->getMessage(),
$e->getCode(),
$e,
);
}
try {
$mimePart = $this->transmissionService->getSignMimePart($localMessage, $account, $mimePart);
$mimePart = $this->transmissionService->getEncryptMimePart($localMessage, $to, $cc, $bcc, $account, $mimePart);
} catch (ServiceException $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return;
}
$mail->setBasePart($mimePart);
$this->eventDispatcher->dispatchTyped(
new BeforeMessageSentEvent($account, $messageData, $repliedToMessageId, $draft, $message, $mail)
);
// Send the message
try {
$mail->send($transport, false, false);
$localMessage->setRaw($mail->getRaw(false));
} catch (Horde_Mime_Exception $e) {
throw new ServiceException(
'Could not send message: ' . $e->getMessage(),
$e->getCode(),
$e
);
$localMessage->setStatus(LocalMessage::STATUS_SMPT_SEND_FAIL);
$this->logger->error($e->getMessage(), ['exception' => $e]);
return;
}
$this->eventDispatcher->dispatchTyped(
new MessageSentEvent($account, $messageData, $repliedToMessageId, $draft, $message, $mail)
new MessageSentEvent($account, $localMessage)
);
}
public function sendLocalMessage(Account $account, LocalMessage $message): void {
$to = new AddressList(
array_map(
static function ($recipient) {
return Address::fromRaw($recipient->getLabel() ?? $recipient->getEmail(), $recipient->getEmail());
},
$this->groupsIntegration->expand(array_filter($message->getRecipients(), static function (Recipient $recipient) {
return $recipient->getType() === Recipient::TYPE_TO;
}))
)
);
$cc = new AddressList(
array_map(
static function ($recipient) {
return Address::fromRaw($recipient->getLabel() ?? $recipient->getEmail(), $recipient->getEmail());
},
$this->groupsIntegration->expand(array_filter($message->getRecipients(), static function (Recipient $recipient) {
return $recipient->getType() === Recipient::TYPE_CC;
}))
)
);
$bcc = new AddressList(
array_map(
static function ($recipient) {
return Address::fromRaw($recipient->getLabel() ?? $recipient->getEmail(), $recipient->getEmail());
},
$this->groupsIntegration->expand(array_filter($message->getRecipients(), static function (Recipient $recipient) {
return $recipient->getType() === Recipient::TYPE_BCC;
}))
)
);
$attachments = array_map(static function (LocalAttachment $attachment) {
// Convert to the untyped nested array used in \OCA\Mail\Controller\AccountsController::send
return [
'type' => 'local',
'id' => $attachment->getId(),
];
}, $message->getAttachments());
$messageData = new NewMessageData(
$account,
$to,
$cc,
$bcc,
$message->getSubject(),
$message->getBody(),
$attachments,
$message->isHtml(),
false,
$message->getSmimeCertificateId(),
$message->getSmimeSign() ?? false,
$message->getSmimeEncrypt() ?? false,
);
if ($message->getAliasId() !== null) {
$alias = $this->aliasesService->find($message->getAliasId(), $account->getUserId());
}
try {
$this->sendMessage($messageData, $message->getInReplyToMessageId(), $alias ?? null);
} catch (SentMailboxNotSetException $e) {
throw new ClientException('Could not send message: ' . $e->getMessage(), $e->getCode(), $e);
}
}
public function saveLocalDraft(Account $account, LocalMessage $message): void {
$messageData = $this->getNewMessageData($message, $account);
$to = $this->transmissionService->getAddressList($message, Recipient::TYPE_TO);
$cc = $this->transmissionService->getAddressList($message, Recipient::TYPE_CC);
$bcc = $this->transmissionService->getAddressList($message, Recipient::TYPE_BCC);
$attachments = $this->transmissionService->getAttachments($message);
$perfLogger = $this->performanceLogger->start('save local draft');
$account = $messageData->getAccount();
$imapMessage = $account->newMessage();
$imapMessage->setTo($messageData->getTo());
$imapMessage->setSubject($messageData->getSubject());
$imapMessage->setTo($to);
$imapMessage->setSubject($message->getSubject());
$from = new AddressList([
Address::fromRaw($account->getName(), $account->getEMailAddress()),
]);
$imapMessage->setFrom($from);
$imapMessage->setCC($messageData->getCc());
$imapMessage->setBcc($messageData->getBcc());
$imapMessage->setContent($messageData->getBody());
$imapMessage->setCC($cc);
$imapMessage->setBcc($bcc);
$imapMessage->setContent($message->getBody());
foreach ($attachments as $attachment) {
$this->transmissionService->handleAttachment($account, $attachment);
}
// build mime body
$headers = [
@ -398,7 +210,7 @@ class MailTransmission implements IMailTransmission {
$this->messageMapper->save(
$client,
$draftsMailbox,
$mail,
$mail->getRaw(false),
[Horde_Imap_Client::FLAG_DRAFT]
);
$perfLogger->step('save local draft message on IMAP');
@ -410,7 +222,7 @@ class MailTransmission implements IMailTransmission {
$client->logout();
}
$this->eventDispatcher->dispatchTyped(new DraftSavedEvent($account, $messageData, null));
$this->eventDispatcher->dispatchTyped(new DraftSavedEvent($account, null));
$perfLogger->step('emit post local draft save event');
$perfLogger->end();
@ -480,7 +292,7 @@ class MailTransmission implements IMailTransmission {
$newUid = $this->messageMapper->save(
$client,
$draftsMailbox,
$mail,
$mail->getRaw(false),
[Horde_Imap_Client::FLAG_DRAFT]
);
$perfLogger->step('save message on IMAP');
@ -502,201 +314,6 @@ class MailTransmission implements IMailTransmission {
return [$account, $draftsMailbox, $newUid];
}
private function buildReplyMessage(Account $account,
NewMessageData $messageData,
string $repliedToMessageId): IMessage {
// Reply
$message = $account->newMessage();
$message->setSubject($messageData->getSubject());
$message->setTo($messageData->getTo());
$message->setInReplyTo($repliedToMessageId);
return $message;
}
private function buildNewMessage(Account $account, NewMessageData $messageData): IMessage {
// New message
$message = $account->newMessage();
$message->setTo($messageData->getTo());
$message->setSubject($messageData->getSubject());
return $message;
}
/**
* @param Account $account
* @param NewMessageData $messageData
* @param IMessage $message
*
* @return void
*/
private function handleAttachments(Account $account, NewMessageData $messageData, IMessage $message): void {
foreach ($messageData->getAttachments() as $attachment) {
if (isset($attachment['type']) && $attachment['type'] === 'local') {
// Adds an uploaded attachment
$this->handleLocalAttachment($account, $attachment, $message);
} elseif (isset($attachment['type']) && $attachment['type'] === 'message') {
// Adds another message as attachment
$this->handleForwardedMessageAttachment($account, $attachment, $message);
} elseif (isset($attachment['type']) && $attachment['type'] === 'message/rfc822') {
// Adds another message as attachment with mime type 'message/rfc822
$this->handleEmbeddedMessageAttachments($account, $attachment, $message);
} elseif (isset($attachment['type']) && $attachment['type'] === 'message-attachment') {
// Adds an attachment from another email (use case is, eg., a mail forward)
$this->handleForwardedAttachment($account, $attachment, $message);
} else {
// Adds an attachment from Files
$this->handleCloudAttachment($attachment, $message);
}
}
}
/**
* @param Account $account
* @param array $attachment
* @param IMessage $message
*
* @return int|null
*/
private function handleLocalAttachment(Account $account, array $attachment, IMessage $message) {
if (!isset($attachment['id'])) {
$this->logger->warning('ignoring local attachment because its id is unknown');
return null;
}
$id = (int)$attachment['id'];
try {
[$localAttachment, $file] = $this->attachmentService->getAttachment($account->getMailAccount()->getUserId(), $id);
$message->addLocalAttachment($localAttachment, $file);
} catch (AttachmentNotFoundException $ex) {
$this->logger->warning('ignoring local attachment because it does not exist');
// TODO: rethrow?
return null;
}
}
/**
* Adds an attachment that's coming from another message's attachment (typical use case: email forwarding)
*
* @param Account $account
* @param mixed[] $attachment
* @param IMessage $message
*/
private function handleForwardedMessageAttachment(Account $account, array $attachment, IMessage $message): void {
// Gets original of other message
$userId = $account->getMailAccount()->getUserId();
$attachmentMessage = $this->mailManager->getMessage($userId, (int)$attachment['id']);
$mailbox = $this->mailManager->getMailbox($userId, $attachmentMessage->getMailboxId());
$client = $this->imapClientFactory->getClient($account);
try {
$fullText = $this->messageMapper->getFullText(
$client,
$mailbox->getName(),
$attachmentMessage->getUid(),
$userId
);
} finally {
$client->logout();
}
$message->addRawAttachment(
$attachment['displayName'] ?? $attachmentMessage->getSubject() . '.eml',
$fullText
);
}
/**
* Adds an email as attachment
*
* @param Account $account
* @param mixed[] $attachment
* @param IMessage $message
*/
private function handleEmbeddedMessageAttachments(Account $account, array $attachment, IMessage $message): void {
// Gets original of other message
$userId = $account->getMailAccount()->getUserId();
$attachmentMessage = $this->mailManager->getMessage($userId, (int)$attachment['id']);
$mailbox = $this->mailManager->getMailbox($userId, $attachmentMessage->getMailboxId());
$client = $this->imapClientFactory->getClient($account);
try {
$fullText = $this->messageMapper->getFullText(
$client,
$mailbox->getName(),
$attachmentMessage->getUid(),
$userId
);
} finally {
$client->logout();
}
$message->addEmbeddedMessageAttachment(
$attachment['displayName'] ?? $attachmentMessage->getSubject() . '.eml',
$fullText
);
}
/**
* Adds an attachment that's coming from another message's attachment (typical use case: email forwarding)
*
* @param Account $account
* @param mixed[] $attachment
* @param IMessage $message
*/
private function handleForwardedAttachment(Account $account, array $attachment, IMessage $message): void {
// Gets attachment from other message
$userId = $account->getMailAccount()->getUserId();
$attachmentMessage = $this->mailManager->getMessage($userId, (int)$attachment['messageId']);
$mailbox = $this->mailManager->getMailbox($userId, $attachmentMessage->getMailboxId());
$client = $this->imapClientFactory->getClient($account);
try {
$attachments = $this->messageMapper->getRawAttachments(
$client,
$mailbox->getName(),
$attachmentMessage->getUid(),
$userId,
[
$attachment['id']
]
);
} finally {
$client->logout();
}
// Attaches attachment to new message
$message->addRawAttachment($attachment['fileName'], $attachments[0]);
}
/**
* @param array $attachment
* @param IMessage $message
*
* @return File|null
*/
private function handleCloudAttachment(array $attachment, IMessage $message) {
if (!isset($attachment['fileName'])) {
$this->logger->warning('ignoring cloud attachment because its fileName is unknown');
return null;
}
$fileName = $attachment['fileName'];
if (!$this->userFolder->nodeExists($fileName)) {
$this->logger->warning('ignoring cloud attachment because the node does not exist');
return null;
}
$file = $this->userFolder->get($fileName);
if (!$file instanceof File) {
$this->logger->warning('ignoring cloud attachment because the node is not a file');
return null;
}
$message->addAttachmentFromFiles($file);
}
public function sendMdn(Account $account, Mailbox $mailbox, Message $message): void {
$query = new Horde_Imap_Client_Fetch_Query();
$query->flags();
@ -766,59 +383,4 @@ class MailTransmission implements IMailTransmission {
}
}
/**
* @param LocalMessage $message
* @param Account $account
* @return NewMessageData
*/
private function getNewMessageData(LocalMessage $message, Account $account): NewMessageData {
$to = new AddressList(
array_map(
static function ($recipient) {
return Address::fromRaw($recipient->getLabel() ?? $recipient->getEmail(), $recipient->getEmail());
},
$this->groupsIntegration->expand(array_filter($message->getRecipients(), static function (Recipient $recipient) {
return $recipient->getType() === Recipient::TYPE_TO;
}))
)
);
$cc = new AddressList(
array_map(
static function ($recipient) {
return Address::fromRaw($recipient->getLabel() ?? $recipient->getEmail(), $recipient->getEmail());
},
$this->groupsIntegration->expand(array_filter($message->getRecipients(), static function (Recipient $recipient) {
return $recipient->getType() === Recipient::TYPE_CC;
}))
)
);
$bcc = new AddressList(
array_map(
static function ($recipient) {
return Address::fromRaw($recipient->getLabel() ?? $recipient->getEmail(), $recipient->getEmail());
},
$this->groupsIntegration->expand(array_filter($message->getRecipients(), static function (Recipient $recipient) {
return $recipient->getType() === Recipient::TYPE_BCC;
}))
)
);
$attachments = array_map(function (LocalAttachment $attachment) {
// Convert to the untyped nested array used in \OCA\Mail\Controller\AccountsController::send
return [
'type' => 'local',
'id' => $attachment->getId(),
];
}, $message->getAttachments());
return new NewMessageData(
$account,
$to,
$cc,
$bcc,
$message->getSubject(),
$message->getBody(),
$attachments,
$message->isHtml()
);
}
}

Просмотреть файл

@ -34,8 +34,8 @@ use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Events\OutboxMessageCreatedEvent;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Send\Chain;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
@ -79,7 +79,9 @@ class OutboxService {
IMailManager $mailManager,
AccountService $accountService,
ITimeFactory $timeFactory,
LoggerInterface $logger) {
LoggerInterface $logger,
private Chain $sendChain,
) {
$this->transmission = $transmission;
$this->mapper = $mapper;
$this->attachmentService = $attachmentService;
@ -130,24 +132,9 @@ class OutboxService {
$this->mapper->deleteWithRecipients($message);
}
/**
* @param LocalMessage $message
* @param Account $account
* @return void
* @throws ClientException
* @throws ServiceException
*/
public function sendMessage(LocalMessage $message, Account $account): void {
try {
$this->transmission->sendLocalMessage($account, $message);
} catch (ClientException|ServiceException $e) {
// Mark as failed so the message is not sent repeatedly in background
$message->setFailed(true);
$this->mapper->update($message);
throw $e;
}
$this->attachmentService->deleteLocalMessageAttachments($account->getUserId(), $message->getId());
$this->mapper->deleteWithRecipients($message);
public function sendMessage(LocalMessage $message, Account $account): LocalMessage {
$this->sendChain->process($account, $message);
return $message;
}
/**
@ -194,6 +181,7 @@ class OutboxService {
$toRecipients = self::convertToRecipient($to, Recipient::TYPE_TO);
$ccRecipients = self::convertToRecipient($cc, Recipient::TYPE_CC);
$bccRecipients = self::convertToRecipient($bcc, Recipient::TYPE_BCC);
$message = $this->mapper->updateWithRecipients($message, $toRecipients, $ccRecipients, $bccRecipients);
if ($attachments === []) {
@ -251,16 +239,13 @@ class OutboxService {
}, $accountIds));
foreach ($messages as $message) {
$account = $accounts[$message->getAccountId()];
if ($account === null) {
// Ignore message of non-existent account
continue;
}
try {
$account = $accounts[$message->getAccountId()];
if ($account === null) {
// Ignore message of non-existent account
continue;
}
$this->sendMessage(
$message,
$account,
);
$this->sendChain->process($account, $message);
$this->logger->debug('Outbox message {id} sent', [
'id' => $message->getId(),
]);

Просмотреть файл

@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Mail\Service;
use OCA\Mail\Account;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
use OCA\Mail\Db\LocalAttachment;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Exception\AttachmentNotFoundException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Exception\SmimeEncryptException;
use OCA\Mail\Exception\SmimeSignException;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCP\AppFramework\Db\DoesNotExistException;
use Psr\Log\LoggerInterface;
class TransmissionService {
public function __construct(private GroupsIntegration $groupsIntegration,
private AttachmentService $attachmentService,
private LoggerInterface $logger,
private SmimeService $smimeService,
) {
}
/**
* @param LocalMessage $message
* @param int $type
* @return AddressList
*/
public function getAddressList(LocalMessage $message, int $type): AddressList {
return new AddressList(
array_map(
static function ($recipient) use ($type) {
return Address::fromRaw($recipient->getLabel() ?? $recipient->getEmail(), $recipient->getEmail());
},
$this->groupsIntegration->expand(
array_filter($message->getRecipients(), static function (Recipient $recipient) use ($type) {
return $recipient->getType() === $type;
})
)
)
);
}
/**
* @param LocalMessage $message
* @return array|array[]
*/
public function getAttachments(LocalMessage $message): array {
if(empty($message->getAttachments())) {
return [];
}
return array_map(static function (LocalAttachment $attachment) {
// Convert to the untyped nested array used in \OCA\Mail\Controller\AccountsController::send
return [
'type' => 'local',
'id' => $attachment->getId(),
];
}, $message->getAttachments());
}
/**
* @param Account $account
* @param array $attachment
* @return \Horde_Mime_Part|null
*/
public function handleAttachment(Account $account, array $attachment): ?\Horde_Mime_Part {
if (!isset($attachment['id'])) {
$this->logger->warning('ignoring local attachment because its id is unknown');
return null;
}
try {
[$localAttachment, $file] = $this->attachmentService->getAttachment($account->getMailAccount()->getUserId(), (int)$attachment['id']);
$part = new \Horde_Mime_Part();
$part->setCharset('us-ascii');
$part->setDisposition('attachment');
$part->setName($localAttachment->getFileName());
$part->setContents($file->getContent());
$part->setType($localAttachment->getMimeType());
return $part;
} catch (AttachmentNotFoundException $e) {
$this->logger->warning('Ignoring local attachment because it does not exist', ['exception' => $e]);
return null;
}
}
/**
* @param LocalMessage $localMessage
* @param Account $account
* @param \Horde_Mime_Part $mimePart
* @return \Horde_Mime_Part
* @throws ServiceException
*/
public function getSignMimePart(LocalMessage $localMessage, Account $account, \Horde_Mime_Part $mimePart): \Horde_Mime_Part {
if ($localMessage->getSmimeSign()) {
if ($localMessage->getSmimeCertificateId() === null) {
$localMessage->setStatus(LocalMessage::STATUS_SMIME_SIGN_NO_CERT_ID);
throw new ServiceException('Could not send message: Requested S/MIME signature without certificate id');
}
try {
$certificate = $this->smimeService->findCertificate(
$localMessage->getSmimeCertificateId(),
$account->getUserId(),
);
$mimePart = $this->smimeService->signMimePart($mimePart, $certificate);
} catch (DoesNotExistException $e) {
$localMessage->setStatus(LocalMessage::STATUS_SMIME_SIGN_CERT);
throw new ServiceException(
'Could not send message: Certificate does not exist: ' . $e->getMessage(),
$e->getCode(),
$e,
);
} catch (SmimeSignException|ServiceException $e) {
$localMessage->setStatus(LocalMessage::STATUS_SMIME_SIGN_FAIL);
throw new ServiceException(
'Could not send message: Failed to sign MIME part: ' . $e->getMessage(),
$e->getCode(),
$e,
);
}
}
return $mimePart;
}
/**
* @param LocalMessage $localMessage
* @param AddressList $to
* @param AddressList $cc
* @param AddressList $bcc
* @param Account $account
* @param \Horde_Mime_Part $mimePart
* @return \Horde_Mime_Part
* @throws ServiceException
*/
public function getEncryptMimePart(LocalMessage $localMessage, AddressList $to, AddressList $cc, AddressList $bcc, Account $account, \Horde_Mime_Part $mimePart): \Horde_Mime_Part {
if ($localMessage->getSmimeEncrypt()) {
if ($localMessage->getSmimeCertificateId() === null) {
$localMessage->setStatus(LocalMessage::STATUS_SMIME_ENCRYPT_NO_CERT_ID);
throw new ServiceException('Could not send message: Requested S/MIME signature without certificate id');
}
try {
$addressList = $to
->merge($cc)
->merge($bcc);
$certificates = $this->smimeService->findCertificatesByAddressList($addressList, $account->getUserId());
$senderCertificate = $this->smimeService->findCertificate($localMessage->getSmimeCertificateId(), $account->getUserId());
$certificates[] = $senderCertificate;
$mimePart = $this->smimeService->encryptMimePart($mimePart, $certificates);
} catch (DoesNotExistException $e) {
$localMessage->setStatus(LocalMessage::STATUS_SMIME_ENCRYPT_CERT);
throw new ServiceException(
'Could not send message: Certificate does not exist: ' . $e->getMessage(),
$e->getCode(),
$e,
);
} catch (SmimeEncryptException|ServiceException $e) {
$localMessage->setStatus(LocalMessage::STATUS_SMIME_ENCRYT_FAIL);
throw new ServiceException(
'Could not send message: Failed to encrypt MIME part: ' . $e->getMessage(),
$e->getCode(),
$e,
);
}
}
return $mimePart;
}
}

Просмотреть файл

@ -396,7 +396,9 @@ export default {
if (!data.sendAt || data.sendAt < Math.floor((now + UNDO_DELAY) / 1000)) {
// Awaiting here would keep the modal open for a long time and thus block the user
this.$store.dispatch('outbox/sendMessageWithUndo', { id: dataForServer.id })
this.$store.dispatch('outbox/sendMessageWithUndo', { id: dataForServer.id }).catch((error) => {
logger.debug('Could not send message', { error })
})
}
if (dataForServer.id) {
// Remove old draft envelope

Просмотреть файл

@ -21,7 +21,8 @@
-->
<template>
<ListItem class="outbox-message"
<ListItem v-if="message.status !== statusImapSentMailboxFail()"
class="outbox-message"
:class="{ selected }"
:name="title"
:details="details"
@ -50,6 +51,35 @@
</ActionButton>
</template>
</ListItem>
<ListItem v-else
class="outbox-message"
:name="title"
:class="{ selected }"
:details="details">
<template #icon>
<Avatar :display-name="avatarDisplayName" :email="avatarEmail" />
</template>
<template #subtitle>
{{ subjectForSubtitle }}
</template>
<template slot="actions">
<ActionButton :close-after-click="true"
@click="sendMessageNow">
{{ t('mail', 'Copy to "Sent" Mailbox') }}
<template #icon>
<Send :title="t('mail', 'Copy to Sent Mailbox')"
:size="20" />
</template>
</ActionButton>
<ActionButton :close-after-click="true"
@click="deleteMessage">
<template #icon>
<IconDelete :size="20" />
</template>
{{ t('mail', 'Delete') }}
</ActionButton>
</template>
</ListItem>
</template>
<script>
@ -64,7 +94,10 @@ import { showError, showSuccess } from '@nextcloud/dialogs'
import { matchError } from '../errors/match.js'
import { html, plain } from '../util/text.js'
import Send from 'vue-material-design-icons/Send.vue'
import { UNDO_DELAY } from '../store/constants.js'
import {
STATUS_IMAP_SENT_MAILBOX_FAIL,
UNDO_DELAY,
} from '../store/constants.js'
export default {
name: 'OutboxMessageListItem',
@ -97,7 +130,9 @@ export default {
return formatter.format(recipients)
},
details() {
if (this.message.failed) {
if (this.message.status === 11) {
return this.t('mail', 'Could not copy to "Sent" mailbox')
} else if (this.message.status !== 0) {
return this.t('mail', 'Message could not be sent')
}
if (!this.message.sendAt) {
@ -117,6 +152,9 @@ export default {
},
},
methods: {
statusImapSentMailboxFail() {
return STATUS_IMAP_SENT_MAILBOX_FAIL
},
async deleteMessage() {
try {
await this.$store.dispatch('outbox/deleteMessage', {
@ -139,9 +177,23 @@ export default {
sendAt: (new Date().getTime() + UNDO_DELAY) / 1000,
}
await this.$store.dispatch('outbox/updateMessage', { message, id: message.id })
await this.$store.dispatch('outbox/sendMessageWithUndo', { id: message.id })
try {
if (this.message.status !== STATUS_IMAP_SENT_MAILBOX_FAIL) {
await this.$store.dispatch('outbox/sendMessageWithUndo', { id: message.id })
} else {
await this.$store.dispatch('outbox/copyMessageToSentMailbox', { id: message.id })
}
} catch (error) {
logger.error('Could not send or copy message', { error })
if (error.data !== undefined) {
await this.$store.dispatch('outbox/updateMessage', { message: error.data[0], id: message.id })
}
}
},
async openModal() {
if (this.message.status === 11) {
return
}
await this.$store.dispatch('startComposerSession', {
type: 'outbox',
data: {

Просмотреть файл

@ -28,3 +28,5 @@ export const PAGE_SIZE = 20
export const UNDO_DELAY = TOAST_UNDO_TIMEOUT
export const EDITOR_MODE_HTML = 'richtext'
export const EDITOR_MODE_TEXT = 'plaintext'
export const STATUS_IMAP_SENT_MAILBOX_FAIL = 11

Просмотреть файл

@ -22,7 +22,7 @@
import * as OutboxService from '../../service/OutboxService.js'
import logger from '../../logger.js'
import { showError, showUndo } from '@nextcloud/dialogs'
import { showError, showSuccess, showUndo } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { html, plain } from '../../util/text.js'
import { UNDO_DELAY } from '../constants.js'
@ -131,6 +131,8 @@ export default {
await OutboxService.sendMessage(id)
logger.debug(`Outbox message ${id} sent`)
} catch (error) {
const m = error.response.data.data[0]
commit('updateMessage', { message: m })
logger.error(`Failed to send message ${id} from outbox`, { error })
throw error
}
@ -185,4 +187,27 @@ export default {
}, UNDO_DELAY)
})
},
/**
* "Send" a message
* The backend chain will handle the actual copying
* We need different toast texts and can do this without UNDO.
*
* @param {object} store Vuex destructuring object
* @param {Function} store.dispatch Vuex dispatch object
* @param {object} store.getters Vuex getters object
* @param {object} data Action data
* @param {number} data.id Id of outbox message to send
*/
async copyMessageToSentMailbox({ getters, dispatch }, { id }) {
const message = getters.getMessage(id)
try {
await dispatch('sendMessage', { id: message.id })
showSuccess(t('mail', 'Message copied to "Sent" mailbox'))
} catch (error) {
showError(t('mail', 'Could not copy message to "Sent" mailbox'))
logger.error('Could not copy message to "Sent" mailbox ' + message.id, { message })
}
},
}

Просмотреть файл

@ -39,7 +39,11 @@ export default {
Vue.delete(message, 'sendAt')
},
updateMessage(state, { message }) {
const existing = state.messages[message.id]
Vue.set(state.messages, message.id, Object.assign({}, existing, message))
const existing = state.messages[message.id] ?? {}
Vue.set(state.messages, message.id, Object.assign(existing, message))
// Add the message only if it's new
if (state.messageList.indexOf(message.id) === -1) {
state.messageList.unshift(message.id)
}
},
}

Просмотреть файл

@ -64,6 +64,7 @@ class LocalAttachmentMapperTest extends TestCase {
/** @var string */
private $user2 = 'dontFindMe';
private array $localMessageIds;
private array $attachmentIds;
protected function setUp(): void {
parent::setUp();

Просмотреть файл

@ -27,24 +27,29 @@ use ChristophWurst\Nextcloud\Testing\TestUser;
use OC;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IAttachmentService;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Db\RecipientMapper;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MailboxSync;
use OCA\Mail\IMAP\MessageMapper;
use OCA\Mail\Model\NewMessageData;
use OCA\Mail\Model\RepliedMessageData;
use OCA\Mail\Send\AntiAbuseHandler;
use OCA\Mail\Send\Chain;
use OCA\Mail\Send\CopySentMessageHandler;
use OCA\Mail\Send\FlagRepliedMessageHandler;
use OCA\Mail\Send\SendHandler;
use OCA\Mail\Send\SentMailboxHandler;
use OCA\Mail\Service\AliasesService;
use OCA\Mail\Service\Attachment\UploadedFile;
use OCA\Mail\Service\GroupsIntegration;
use OCA\Mail\Service\MailTransmission;
use OCA\Mail\Service\SmimeService;
use OCA\Mail\Service\TransmissionService;
use OCA\Mail\SMTP\SmtpClientFactory;
use OCA\Mail\Support\PerformanceLogger;
use OCA\Mail\Tests\Integration\Framework\ImapTest;
@ -71,6 +76,10 @@ class MailTransmissionIntegrationTest extends TestCase {
/** @var IMailTransmission */
private $transmission;
private Chain $chain;
private LocalMessageMapper $localMessageMapper;
private LocalMessage $message;
protected function setUp(): void {
parent::setUp();
@ -104,16 +113,38 @@ class MailTransmissionIntegrationTest extends TestCase {
$this->attachmentService = Server::get(IAttachmentService::class);
$userFolder = OC::$server->getUserFolder($this->user->getUID());
$recipientMapper = Server::get(RecipientMapper::class);
$recipient = new Recipient();
$recipient->setType(Recipient::TYPE_TO);
$recipient->setEmail('recipient@domain.com');
$recipientMapper->insert($recipient);
$this->localMessageMapper = Server::get(LocalMessageMapper::class);
$this->message = new LocalMessage();
$this->message->setAccountId($this->account->getId());
$this->message->setSubject('greetings');
$this->message->setBody('hello there');
$this->message->setType(LocalMessage::TYPE_OUTGOING);
$this->message->setHtml(false);
$this->message->setRecipients([$recipient]);
$this->message->setStatus(LocalMessage::STATUS_RAW);
$this->localMessageMapper->insert($this->message);
// Make sure the mailbox preferences are set
/** @var MailboxSync $mbSync */
$mbSync = Server::get(MailboxSync::class);
$mbSync->sync($this->account, new NullLogger(), true);
$this->transmission = new MailTransmission(
$userFolder,
$this->chain = new Chain(
Server::get(SentMailboxHandler::class),
Server::get(AntiAbuseHandler::class),
Server::get(SendHandler::class),
Server::get(CopySentMessageHandler::class),
Server::get(FlagRepliedMessageHandler::class),
$this->attachmentService,
Server::get(IMailManager::class),
Server::get(IMAPClientFactory::class),
$this->localMessageMapper,
);
$this->transmission = new MailTransmission(Server::get(IMAPClientFactory::class),
Server::get(SmtpClientFactory::class),
Server::get(IEventDispatcher::class),
Server::get(MailboxMapper::class),
@ -121,15 +152,12 @@ class MailTransmissionIntegrationTest extends TestCase {
Server::get(LoggerInterface::class),
Server::get(PerformanceLogger::class),
Server::get(AliasesService::class),
Server::get(GroupsIntegration::class),
Server::get(SmimeService::class),
Server::get(TransmissionService::class),
);
}
public function testSendMail() {
$message = NewMessageData::fromRequest($this->account, 'recipient@domain.com', null, null, 'greetings', 'hello there', []);
$this->transmission->sendMessage($message, null);
$this->chain->process($this->account, $this->message);
$this->addToAssertionCount(1);
}
@ -139,30 +167,12 @@ class MailTransmissionIntegrationTest extends TestCase {
'name' => 'text.txt',
'tmp_name' => dirname(__FILE__) . '/../../data/mail-message-123.txt',
]);
$this->attachmentService->addFile($this->user->getUID(), $file);
$message = NewMessageData::fromRequest($this->account, 'recipient@domain.com', null, null, 'greetings', 'hello there', [
[
'type' => 'local',
'id' => 13,
],
]);
$this->transmission->sendMessage($message, null);
$localAttachment = $this->attachmentService->addFile($this->user->getUID(), $file);
$this->addToAssertionCount(1);
}
$this->message->setAttachments([$localAttachment]);
public function testSendMailWithCloudAttachment() {
$userFolder = OC::$server->getUserFolder($this->user->getUID());
$userFolder->newFile('text.txt');
$message = NewMessageData::fromRequest($this->account, 'recipient@domain.com', null, null, 'greetings', 'hello there', [
[
'type' => 'Files',
'fileName' => 'text.txt',
],
]);
$this->transmission->sendMessage($message, null);
$this->chain->process($this->account, $this->message);
$this->addToAssertionCount(1);
}
@ -184,9 +194,9 @@ class MailTransmissionIntegrationTest extends TestCase {
$messageInReply->setUid($originalUID);
$messageInReply->setMessageId('message@server');
$messageInReply->setMailboxId($inbox->getId());
$this->message->setInReplyToMessageId($messageInReply->getInReplyTo());
$message = NewMessageData::fromRequest($this->account, 'recipient@domain.com', null, null, 'greetings', 'hello there', []);
$this->transmission->sendMessage($message, $messageInReply->getMessageId());
$this->chain->process($this->account, $this->message);
$this->assertMailboxExists('Sent');
$this->assertMessageCount(1, 'Sent');
@ -209,10 +219,10 @@ class MailTransmissionIntegrationTest extends TestCase {
$messageInReply->setUid($originalUID);
$messageInReply->setMessageId('message@server');
$messageInReply->setMailboxId($inbox->getId());
$this->message->setSubject('');
$this->message->setInReplyToMessageId($messageInReply->getInReplyTo());
$message = NewMessageData::fromRequest($this->account, 'recipient@domain.com', null, null, '', 'hello there', []);
$reply = new RepliedMessageData($this->account, $messageInReply);
$this->transmission->sendMessage($message, $messageInReply->getMessageId());
$this->chain->process($this->account, $this->message);
$this->assertMailboxExists('Sent');
$this->assertMessageCount(1, 'Sent');
@ -235,10 +245,10 @@ class MailTransmissionIntegrationTest extends TestCase {
$messageInReply->setUid($originalUID);
$messageInReply->setMessageId('message@server');
$messageInReply->setMailboxId($inbox->getId());
$this->message->setSubject('Re: reply test');
$this->message->setInReplyToMessageId($messageInReply->getInReplyTo());
$message = NewMessageData::fromRequest($this->account, 'recipient@domain.com', null, null, 'Re: reply test', 'hello there', []);
$reply = new RepliedMessageData($this->account, $messageInReply);
$this->transmission->sendMessage($message, $messageInReply->getMessageId());
$this->chain->process($this->account, $this->message);
$this->assertMailboxExists('Sent');
$this->assertMessageCount(1, 'Sent');
@ -265,23 +275,4 @@ class MailTransmissionIntegrationTest extends TestCase {
$this->assertMessageCount(1, 'Drafts');
}
public function testSendLocalMessage(): void {
$localMessage = new LocalMessage();
$to = new Recipient();
$to->setLabel('Penny');
$to->setEmail('library@stardewvalley.edu');
$to->setType(Recipient::TYPE_TO);
$localMessage->setType(LocalMessage::TYPE_OUTGOING);
$localMessage->setSubject('hello');
$localMessage->setBody('This is a test');
$localMessage->setHtml(false);
$localMessage->setRecipients([$to]);
$localMessage->setAttachments([]);
$this->transmission->sendLocalMessage($this->account, $localMessage);
$this->assertMailboxExists('Sent');
$this->assertMessageCount(1, 'Sent');
}
}

Просмотреть файл

@ -37,6 +37,7 @@ use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Send\Chain;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\Attachment\AttachmentStorage;
@ -93,6 +94,8 @@ class OutboxServiceIntegrationTest extends TestCase {
/** @var ITimeFactory */
private $timeFactory;
private \PHPUnit\Framework\MockObject\MockObject|Chain $chain;
private \OCP\IDBConnection $db;
protected function setUp(): void {
parent::setUp();
@ -121,14 +124,14 @@ class OutboxServiceIntegrationTest extends TestCase {
$this->clientFactory = Server::get(IMAPClientFactory::class);
$this->accountService = Server::get(AccountService::class);
$this->timeFactory = Server::get(ITimeFactory::class);
$this->chain = Server::get(Chain::class);
$this->db = OC::$server->getDatabaseConnection();
$qb = $this->db->getQueryBuilder();
$delete = $qb->delete($this->mapper->getTableName());
$delete->execute();
$this->outbox = new OutboxService(
$this->transmission,
$this->outbox = new OutboxService($this->transmission,
$this->mapper,
$this->attachmentService,
$this->eventDispatcher,
@ -136,7 +139,8 @@ class OutboxServiceIntegrationTest extends TestCase {
$mailManager,
$this->accountService,
$this->timeFactory,
$this->createMock(LoggerInterface::class)
$this->createMock(LoggerInterface::class),
$this->chain
);
}
@ -357,7 +361,7 @@ class OutboxServiceIntegrationTest extends TestCase {
$this->assertCount(1, $saved->getRecipients());
$this->assertEmpty($message->getAttachments());
$this->outbox->sendMessage($saved, new Account($this->account));
$actual = $this->outbox->sendMessage($saved, new Account($this->account));
$this->expectException(DoesNotExistException::class);
$this->outbox->getMessage($message->getId(), $this->user->getUID());

Просмотреть файл

@ -144,6 +144,10 @@ class OutboxControllerTest extends TestCase {
$message = new LocalMessage();
$message->setId(1);
$message->setAccountId(1);
$newMessage = new LocalMessage();
$newMessage->setId(1);
$newMessage->setAccountId(1);
$newMessage->setStatus(LocalMessage::STATUS_PROCESSED);
$account = new Account(new MailAccount());
$this->service->expects(self::once())
@ -156,7 +160,8 @@ class OutboxControllerTest extends TestCase {
->willReturn($account);
$this->service->expects(self::once())
->method('sendMessage')
->with($message, $account);
->with($message, $account)
->willReturn($newMessage);
$expected = JsonResponse::success('Message sent', Http::STATUS_ACCEPTED);
$actual = $this->controller->send($message->getId());

Просмотреть файл

@ -26,17 +26,17 @@ declare(strict_types=1);
namespace OCA\Mail\Tests\Unit\Listener;
use ChristophWurst\Nextcloud\Testing\TestCase;
use Horde_Mime_Mail;
use OCA\Mail\Account;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
use OCA\Mail\Contracts\IUserPreferences;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Events\MessageSentEvent;
use OCA\Mail\Listener\AddressCollectionListener;
use OCA\Mail\Model\IMessage;
use OCA\Mail\Model\NewMessageData;
use OCA\Mail\Model\RepliedMessageData;
use OCA\Mail\Service\AutoCompletion\AddressCollector;
use OCA\Mail\Service\TransmissionService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use PHPUnit\Framework\MockObject\MockObject;
@ -55,17 +55,21 @@ class AddressCollectionListenerTest extends TestCase {
/** @var IEventListener */
private $listener;
private MockObject|TransmissionService $transmission;
protected function setUp(): void {
parent::setUp();
$this->preferences = $this->createMock(IUserPreferences::class);
$this->addressCollector = $this->createMock(AddressCollector::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->transmission = $this->createMock(TransmissionService::class);
$this->listener = new AddressCollectionListener(
$this->preferences,
$this->addressCollector,
$this->logger
$this->logger,
$this->transmission,
);
}
@ -102,44 +106,47 @@ class AddressCollectionListenerTest extends TestCase {
$account = $this->createConfiguredMock(Account::class, [
'getUserId' => 'test'
]);
/** @var NewMessageData|MockObject $newMessageData */
$newMessageData = $this->createMock(NewMessageData::class);
/** @var RepliedMessageData|MockObject $repliedMessageData */
$repliedMessageData = $this->createMock(RepliedMessageData::class);
/** @var IMessage|MockObject $message */
$message = $this->createMock(IMessage::class);
$message = $this->createMock(LocalMessage::class);
$message->setRecipients([
Recipient::fromParams([
'email' => 'to@email',
'type' => Recipient::TYPE_TO,
]),
Recipient::fromParams([
'email' => 'cc@email',
'type' => Recipient::TYPE_CC,
]),
Recipient::fromParams([
'email' => 'bcc@email',
'type' => Recipient::TYPE_BCC,
])
]);
$event = new MessageSentEvent(
$account,
new LocalMessage(),
);
$to = new AddressList([Address::fromRaw('to', 'to@email')]);
$cc = new AddressList([Address::fromRaw('cc', 'cc@email')]);
$bcc = new AddressList([Address::fromRaw('bcc', 'bcc@email')]);
$addresses = $to->merge($cc)->merge($bcc);
$this->preferences->expects($this->once())
->method('getPreference')
->with('test', 'collect-data', 'true')
->willReturn('true');
/** @var Horde_Mime_Mail|MockObject $mail */
$mail = $this->createMock(Horde_Mime_Mail::class);
$event = new MessageSentEvent(
$account,
$newMessageData,
'abc123',
null,
$message,
$mail
);
$message->expects($this->once())
->method('getTo')
->willReturn(new AddressList([Address::fromRaw('to', 'to@email')]));
$message->expects($this->once())
->method('getCC')
->willReturn(new AddressList([Address::fromRaw('cc', 'cc@email')]));
$message->expects($this->once())
->method('getBCC')
->willReturn(new AddressList([Address::fromRaw('bcc', 'bcc@email')]));
$this->transmission->expects($this->exactly(3))
->method('getAddressList')
->willReturnOnConsecutiveCalls(
$to,
$cc,
$bcc,
);
$this->addressCollector->expects($this->once())
->method('addAddresses')
->with(
'test',
$this->equalTo(new AddressList([
Address::fromRaw('to', 'to@email'),
Address::fromRaw('cc', 'cc@email'),
Address::fromRaw('bcc', 'bcc@email'),
]))
$account->getUserId(),
$this->equalTo($addresses)
);
$this->logger->expects($this->never())->method($this->anything());

Просмотреть файл

@ -1,175 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\Test\Listener;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
use OCA\Mail\Db\MessageMapper as DbMessageMapper;
use OCA\Mail\Events\MessageSentEvent;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCA\Mail\Listener\FlagRepliedMessageListener;
use OCA\Mail\Model\IMessage;
use OCA\Mail\Model\NewMessageData;
use OCA\Mail\Model\RepliedMessageData;
use OCP\EventDispatcher\Event;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class FlagRepliedMessageListenerTest extends TestCase {
/** @var IMAPClientFactory|MockObject */
private $imapClientFactory;
/** @var MailboxMapper|MockObject */
private $mailboxMapper;
/** @var MessageMapper|MockObject */
private $messageMapper;
/** @var LoggerInterface|MockObject */
private $logger;
/** @var FlagRepliedMessageListener */
private $listener;
/** @var DbMessageMapper|MockObject */
private $dbMessageMapper;
protected function setUp(): void {
parent::setUp();
$this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
$this->dbMessageMapper = $this->createMock(DbMessageMapper::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->listener = new FlagRepliedMessageListener(
$this->imapClientFactory,
$this->mailboxMapper,
$this->dbMessageMapper,
$this->messageMapper,
$this->logger
);
}
public function testHandleUnrelated(): void {
$event = new Event();
$this->listener->handle($event);
$this->addToAssertionCount(1);
}
public function testHandleMessageSentEventMailboxNotAReply(): void {
/** @var Account|MockObject $account */
$account = $this->createMock(Account::class);
/** @var NewMessageData|MockObject $newMessageData */
$newMessageData = $this->createMock(NewMessageData::class);
/** @var IMessage|MockObject $message */
$message = $this->createMock(IMessage::class);
/** @var \Horde_Mime_Mail|MockObject $mail */
$mail = $this->createMock(\Horde_Mime_Mail::class);
$draft = new Message();
$draft->setUid(123);
$event = new MessageSentEvent(
$account,
$newMessageData,
null,
$draft,
$message,
$mail
);
$this->dbMessageMapper->expects($this->never())
->method('findByMessageId');
$this->mailboxMapper->expects($this->never())
->method('find');
$this->logger->expects($this->never())
->method('warning');
$this->listener->handle($event);
}
public function testHandleMessageSentEvent(): void {
/** @var Account|MockObject $account */
$account = $this->createMock(Account::class);
/** @var NewMessageData|MockObject $newMessageData */
$newMessageData = $this->createMock(NewMessageData::class);
/** @var RepliedMessageData|MockObject $repliedMessageData */
$repliedMessageData = $this->createMock(RepliedMessageData::class);
/** @var IMessage|MockObject $message */
$message = $this->createMock(IMessage::class);
/** @var \Horde_Mime_Mail|MockObject $mail */
$mail = $this->createMock(\Horde_Mime_Mail::class);
$draft = new Message();
$draft->setUid(123);
$event = new MessageSentEvent(
$account,
$newMessageData,
'<abc123@123.com>',
$draft,
$message,
$mail
);
$messageInReply = new Message();
$messageInReply->setUid(321);
$messageInReply->setMailboxId(654);
$messageInReply->setMessageId('abc123@123.com');
$repliedMessageData->method('getMessage')
->willReturn($messageInReply);
$this->dbMessageMapper->expects($this->once())
->method('findByMessageId')
->with(
$event->getAccount(),
'<abc123@123.com>'
)->willReturn([$messageInReply]);
$mailbox = new Mailbox();
$this->mailboxMapper->expects($this->once())
->method('findById')
->with(654)
->willReturn($mailbox);
$client = $this->createMock(\Horde_Imap_Client_Socket::class);
$this->imapClientFactory->expects($this->once())
->method('getClient')
->with($account)
->willReturn($client);
$this->messageMapper->expects($this->once())
->method('addFlag')
->with(
$client,
$mailbox,
[321],
\Horde_Imap_Client::FLAG_ANSWERED
);
$this->logger->expects($this->never())
->method('warning');
$this->listener->handle($event);
}
}

Просмотреть файл

@ -27,11 +27,11 @@ namespace OCA\Mail\Tests\Unit\Listener;
use ChristophWurst\Nextcloud\Testing\ServiceMockObject;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Events\MessageSentEvent;
use OCA\Mail\Listener\InteractionListener;
use OCA\Mail\Model\IMessage;
use OCP\Contacts\Events\ContactInteractedWithEvent;
use OCP\EventDispatcher\Event;
use OCP\IUser;
@ -66,31 +66,38 @@ class InteractionListenerTest extends TestCase {
return;
}
$to = new AddressList([
Address::fromRaw('rec 1', 'u1@domain.tld'),
Address::fromRaw('rec 1', 'u2@domain.tld'),
$message = new LocalMessage();
$message->setRecipients([
Recipient::fromParams([
'label' => 'rec 1',
'email' => 'u1@domain.tld',
'type' => Recipient::TYPE_TO,
]),
Recipient::fromParams([
'label' => 'rec 1',
'email' => 'u2@domain.tld',
'type' => Recipient::TYPE_TO,
]),
Recipient::fromParams([
'label' => 'rec 1',
'email' => 'u3@domain.tld',
'type' => Recipient::TYPE_CC,
]),
Recipient::fromParams([
'label' => 'rec 1',
'email' => 'u4@domain.tld',
'type' => Recipient::TYPE_BCC,
]),
Recipient::fromParams([
'label' => 'rec 1',
'email' => 'u2@domain.tld',
'type' => Recipient::TYPE_CC,
]),
]);
$cc = new AddressList([
Address::fromRaw('rec 1', 'u3@domain.tld'),
]);
$bcc = new AddressList([
Address::fromRaw('rec 1', 'u4@domain.tld'),
Address::fromRaw('rec 1', 'u2@domain.tld'), // intentional duplicate
]);
$event = $this->createMock(MessageSentEvent::class);
$message = $this->createMock(IMessage::class);
$event
->method('getMessage')
->willReturn($message);
$message
->method('getTo')
->willReturn($to);
$message
->method('getCC')
->willReturn($cc);
$message
->method('getBCC')
->willReturn($bcc);
$event = new MessageSentEvent(
$this->createMock(Account::class),
$message,
);
$user = $this->createMock(IUser::class);
$this->serviceMock->getParameter('userSession')
->method('getUser')

Просмотреть файл

@ -1,230 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\Tests\Unit\Listener;
use ChristophWurst\Nextcloud\Testing\TestCase;
use Horde_Imap_Client_Exception;
use OCA\Mail\Account;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
use OCA\Mail\Events\MessageSentEvent;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCA\Mail\Listener\SaveSentMessageListener;
use OCA\Mail\Model\IMessage;
use OCA\Mail\Model\NewMessageData;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class SaveSentMessageListenerTest extends TestCase {
/** @var MailboxMapper|MockObject */
private $mailboxMapper;
/** @var IMAPClientFactory|MockObject */
private $imapClientFactory;
/** @var MessageMapper|MockObject */
private $messageMapper;
/** @var LoggerInterface|MockObject */
private $logger;
/** @var IEventListener */
private $listener;
protected function setUp(): void {
parent::setUp();
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
$this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->listener = new SaveSentMessageListener(
$this->mailboxMapper,
$this->imapClientFactory,
$this->messageMapper,
$this->logger
);
}
public function testHandleUnrelated(): void {
$event = new Event();
$this->listener->handle($event);
$this->addToAssertionCount(1);
}
public function testHandleMessageSentMailboxNotSet(): void {
/** @var Account|MockObject $account */
$account = $this->createMock(Account::class);
$mailAccount = new MailAccount();
$account->method('getMailAccount')->willReturn($mailAccount);
/** @var NewMessageData|MockObject $newMessageData */
$newMessageData = $this->createMock(NewMessageData::class);
/** @var IMessage|MockObject $message */
$message = $this->createMock(IMessage::class);
/** @var \Horde_Mime_Mail|MockObject $mail */
$mail = $this->createMock(\Horde_Mime_Mail::class);
$draft = new Message();
$draft->setUid(123);
$event = new MessageSentEvent(
$account,
$newMessageData,
'abc123',
$draft,
$message,
$mail
);
$this->mailboxMapper->expects($this->never())
->method('findById');
$this->logger->expects($this->once())
->method('warning');
$this->listener->handle($event);
}
public function testHandleMessageSentMailboxDoesNotExist(): void {
/** @var Account|MockObject $account */
$account = $this->createMock(Account::class);
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(123);
$account->method('getMailAccount')->willReturn($mailAccount);
/** @var NewMessageData|MockObject $newMessageData */
$newMessageData = $this->createMock(NewMessageData::class);
/** @var IMessage|MockObject $message */
$message = $this->createMock(IMessage::class);
/** @var \Horde_Mime_Mail|MockObject $mail */
$mail = $this->createMock(\Horde_Mime_Mail::class);
$draft = new Message();
$draft->setUid(123);
$event = new MessageSentEvent(
$account,
$newMessageData,
'abc123',
$draft,
$message,
$mail
);
$this->mailboxMapper->expects($this->once())
->method('findById')
->with(123)
->willThrowException(new DoesNotExistException(''));
$this->messageMapper->expects($this->never())
->method('save');
$this->logger->expects($this->once())
->method('error');
$this->listener->handle($event);
}
public function testHandleMessageSentSavingError(): void {
/** @var Account|MockObject $account */
$account = $this->createMock(Account::class);
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(123);
$account->method('getMailAccount')->willReturn($mailAccount);
/** @var NewMessageData|MockObject $newMessageData */
$newMessageData = $this->createMock(NewMessageData::class);
/** @var IMessage|MockObject $message */
$message = $this->createMock(IMessage::class);
/** @var \Horde_Mime_Mail|MockObject $mail */
$mail = $this->createMock(\Horde_Mime_Mail::class);
$draft = new Message();
$draft->setUid(123);
$event = new MessageSentEvent(
$account,
$newMessageData,
'abc123',
$draft,
$message,
$mail
);
$mailbox = new Mailbox();
$this->mailboxMapper->expects($this->once())
->method('findById')
->with(123)
->willReturn($mailbox);
$this->messageMapper->expects($this->once())
->method('save')
->with(
$this->anything(),
$mailbox,
$mail
)
->willThrowException(new Horde_Imap_Client_Exception('', 0));
$this->expectException(ServiceException::class);
$this->listener->handle($event);
}
public function testHandleMessageSent(): void {
/** @var Account|MockObject $account */
$account = $this->createMock(Account::class);
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(123);
$account->method('getMailAccount')->willReturn($mailAccount);
/** @var NewMessageData|MockObject $newMessageData */
$newMessageData = $this->createMock(NewMessageData::class);
/** @var IMessage|MockObject $message */
$message = $this->createMock(IMessage::class);
/** @var \Horde_Mime_Mail|MockObject $mail */
$mail = $this->createMock(\Horde_Mime_Mail::class);
$draft = new Message();
$draft->setUid(123);
$event = new MessageSentEvent(
$account,
$newMessageData,
'abc123',
$draft,
$message,
$mail
);
$mailbox = new Mailbox();
$this->mailboxMapper->expects($this->once())
->method('findById')
->with(123)
->willReturn($mailbox);
$this->messageMapper->expects($this->once())
->method('save')
->with(
$this->anything(),
$mailbox,
$mail
);
$this->logger->expects($this->never())->method('warning');
$this->logger->expects($this->never())->method('error');
$this->listener->handle($event);
}
}

Просмотреть файл

@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Unit\Send;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Send\AntiAbuseHandler;
use OCA\Mail\Send\SendHandler;
use OCA\Mail\Service\AntiAbuseService;
use OCP\IUser;
use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class AntiAbuseHandlerTest extends TestCase {
private IUserManager|MockObject $userManager;
private MockObject|AntiAbuseService $antiAbuseService;
private LoggerInterface|MockObject $logger;
private SendHandler|MockObject $sendHandler;
private AntiAbuseHandler $handler;
protected function setUp(): void {
$this->userManager = $this->createMock(IUserManager::class);
$this->antiAbuseService = $this->createMock(AntiAbuseService::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->sendHandler = $this->createMock(SendHandler::class);
$this->handler = new AntiAbuseHandler(
$this->userManager,
$this->antiAbuseService,
$this->logger,
);
$this->handler->setNext($this->sendHandler);
}
public function testProcess(): void {
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$localMessage = new LocalMessage();
$localMessage->setStatus(LocalMessage::STATUS_RAW);
$this->userManager->expects(self::once())
->method('get')
->willReturn($this->createMock(IUser::class));
$this->logger->expects(self::never())
->method('error');
$this->antiAbuseService->expects(self::once())
->method('onBeforeMessageSent');
$this->sendHandler->expects(self::once())
->method('process');
$this->handler->process($account, $localMessage);
}
public function testProcessNoUser(): void {
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$mailAccount->setId(123);
$account = new Account($mailAccount);
$localMessage = new LocalMessage();
$localMessage->setStatus(LocalMessage::STATUS_RAW);
$this->userManager->expects(self::once())
->method('get')
->willReturn(null);
$this->logger->expects(self::once())
->method('error');
$this->antiAbuseService->expects(self::never())
->method('onBeforeMessageSent');
$this->sendHandler->expects(self::never())
->method('process');
$this->handler->process($account, $localMessage);
}
public function testProcessAlreadyProcessed(): void {
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$mailAccount->setId(123);
$account = new Account($mailAccount);
$localMessage = new LocalMessage();
$localMessage->setStatus(LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL);
$this->userManager->expects(self::never())
->method('get');
$this->logger->expects(self::never())
->method('error');
$this->antiAbuseService->expects(self::never())
->method('onBeforeMessageSent');
$this->sendHandler->expects(self::once())
->method('process');
$this->handler->process($account, $localMessage);
}
}

Просмотреть файл

@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Unit\Send;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\Send\AntiAbuseHandler;
use OCA\Mail\Send\Chain;
use OCA\Mail\Send\CopySentMessageHandler;
use OCA\Mail\Send\FlagRepliedMessageHandler;
use OCA\Mail\Send\SendHandler;
use OCA\Mail\Send\SentMailboxHandler;
use OCA\Mail\Service\Attachment\AttachmentService;
use PHPUnit\Framework\MockObject\MockObject;
class ChainTest extends TestCase {
private Chain $chain;
private SentMailboxHandler|MockObject $sentMailboxHandler;
private MockObject|AntiAbuseHandler $antiAbuseHandler;
private SendHandler|MockObject $sendHandler;
private MockObject|CopySentMessageHandler $copySentMessageHandler;
private MockObject|FlagRepliedMessageHandler $flagRepliedMessageHandler;
private MockObject|MessageMapper $messageMapper;
private AttachmentService|MockObject $attachmentService;
private MockObject|LocalMessageMapper $localMessageMapper;
protected function setUp(): void {
$this->sentMailboxHandler = $this->createMock(SentMailboxHandler::class);
$this->antiAbuseHandler = $this->createMock(AntiAbuseHandler::class);
$this->sendHandler = $this->createMock(SendHandler::class);
$this->copySentMessageHandler = $this->createMock(CopySentMessageHandler::class);
$this->flagRepliedMessageHandler = $this->createMock(FlagRepliedMessageHandler::class);
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->localMessageMapper = $this->createMock(LocalMessageMapper::class);
$this->chain = new Chain($this->sentMailboxHandler,
$this->antiAbuseHandler,
$this->sendHandler,
$this->copySentMessageHandler,
$this->flagRepliedMessageHandler,
$this->attachmentService,
$this->localMessageMapper,
);
}
public function testProcess(): void {
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(1);
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$localMessage = new LocalMessage();
$localMessage->setId(100);
$localMessage->setStatus(LocalMessage::STATUS_RAW);
$expected = new LocalMessage();
$expected->setStatus(LocalMessage::STATUS_PROCESSED);
$expected->setId(100);
$this->sentMailboxHandler->expects(self::any())
->method('setNext')
->withConsecutive(
[$this->antiAbuseHandler],
[$this->sentMailboxHandler],
[$this->copySentMessageHandler],
[$this->flagRepliedMessageHandler],
);
$this->sentMailboxHandler->expects(self::once())
->method('process')
->with($account, $localMessage)
->willReturn($expected);
$this->attachmentService->expects(self::once())
->method('deleteLocalMessageAttachments')
->with($account->getUserId(), $expected->getId());
$this->localMessageMapper->expects(self::once())
->method('deleteWithRecipients')
->with($expected);
$this->localMessageMapper->expects(self::never())
->method('update');
$this->chain->process($account, $localMessage);
}
public function testProcessNotProcessed() {
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(1);
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$localMessage = new LocalMessage();
$localMessage->setId(100);
$localMessage->setStatus(LocalMessage::STATUS_RAW);
$expected = new LocalMessage();
$expected->setStatus(LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL);
$expected->setId(100);
$this->sentMailboxHandler->expects(self::any())
->method('setNext')
->withConsecutive(
[$this->antiAbuseHandler],
[$this->sentMailboxHandler],
[$this->copySentMessageHandler],
[$this->flagRepliedMessageHandler],
);
$this->sentMailboxHandler->expects(self::once())
->method('process')
->with($account, $localMessage)
->willReturn($expected);
$this->attachmentService->expects(self::never())
->method('deleteLocalMessageAttachments');
$this->localMessageMapper->expects(self::never())
->method('deleteWithRecipients');
$this->localMessageMapper->expects(self::once())
->method('update')
->with($expected);
$this->chain->process($account, $localMessage);
}
}

Просмотреть файл

@ -0,0 +1,232 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Unit\Send;
use ChristophWurst\Nextcloud\Testing\TestCase;
use Horde_Imap_Client_Exception;
use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCA\Mail\Send\CopySentMessageHandler;
use OCA\Mail\Send\FlagRepliedMessageHandler;
use OCP\AppFramework\Db\DoesNotExistException;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class CopySendMessageHandlerTest extends TestCase {
private IMAPClientFactory|MockObject $imapClientFactory;
private MailboxMapper|MockObject $mailboxMapper;
private LoggerInterface|MockObject $loggerInterface;
private MockObject|MessageMapper $messageMapper;
private MockObject|FlagRepliedMessageHandler $flagRepliedMessageHandler;
private CopySentMessageHandler $handler;
protected function setUp(): void {
$this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
$this->loggerInterface = $this->createMock(LoggerInterface::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
$this->flagRepliedMessageHandler = $this->createMock(FlagRepliedMessageHandler::class);
$this->handler = new CopySentMessageHandler(
$this->imapClientFactory,
$this->mailboxMapper,
$this->loggerInterface,
$this->messageMapper,
);
$this->handler->setNext($this->flagRepliedMessageHandler);
}
public function testProcess(): void {
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(1);
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$localMessage = $this->getMockBuilder(LocalMessage::class);
$localMessage->addMethods(['getStatus','setStatus', 'getRaw']);
$mock = $localMessage->getMock();
$mailbox = new Mailbox();
$client = $this->createMock(Horde_Imap_Client_Socket::class);
$mock->expects(self::once())
->method('getStatus')
->willReturn(LocalMessage::STATUS_RAW);
$this->loggerInterface->expects(self::never())
->method('warning');
$this->loggerInterface->expects(self::never())
->method('error');
$this->mailboxMapper->expects(self::once())
->method('findById')
->willReturn($mailbox);
$this->imapClientFactory->expects(self::once())
->method('getClient')
->willReturn($client);
$mock->expects(self::once())
->method('getRaw')
->willReturn('Test');
$this->messageMapper->expects(self::once())
->method('save');
$mock->expects(self::once())
->method('setStatus')
->willReturn(LocalMessage::STATUS_PROCESSED);
$this->flagRepliedMessageHandler->expects(self::once())
->method('process')
->with($account, $mock);
$this->handler->process($account, $mock);
}
public function testProcessNoSentMailbox(): void {
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$localMessage = $this->getMockBuilder(LocalMessage::class);
$localMessage->addMethods(['getStatus','setStatus']);
$mock = $localMessage->getMock();
$this->loggerInterface->expects(self::once())
->method('warning');
$mock->expects(self::once())
->method('getStatus')
->willReturn(LocalMessage::STATUS_RAW);
$mock->expects(self::once())
->method('setStatus')
->with(LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL);
$this->loggerInterface->expects(self::never())
->method('error');
$this->mailboxMapper->expects(self::never())
->method('findById');
$this->imapClientFactory->expects(self::never())
->method('getClient');
$this->messageMapper->expects(self::never())
->method('save');
$this->flagRepliedMessageHandler->expects(self::never())
->method('process');
$this->handler->process($account, $mock);
}
public function testProcessNoSentMailboxFound(): void {
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$mailAccount->setSentMailboxId(1);
$account = new Account($mailAccount);
$localMessage = $this->getMockBuilder(LocalMessage::class);
$localMessage->addMethods(['getStatus','setStatus']);
$mock = $localMessage->getMock();
$this->loggerInterface->expects(self::never())
->method('warning');
$mock->expects(self::once())
->method('getStatus')
->willReturn(LocalMessage::STATUS_RAW);
$mock->expects(self::once())
->method('setStatus')
->with(LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL);
$this->mailboxMapper->expects(self::once())
->method('findById')
->willThrowException(new DoesNotExistException(''));
$this->loggerInterface->expects(self::once())
->method('error');
$this->imapClientFactory->expects(self::never())
->method('getClient');
$this->messageMapper->expects(self::never())
->method('save');
$this->flagRepliedMessageHandler->expects(self::never())
->method('process');
$this->handler->process($account, $mock);
}
public function testProcessCouldNotCopy(): void {
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(1);
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$localMessage = $this->getMockBuilder(LocalMessage::class);
$localMessage->addMethods(['getStatus','setStatus', 'getRaw']);
$mock = $localMessage->getMock();
$mailbox = new Mailbox();
$client = $this->createMock(Horde_Imap_Client_Socket::class);
$mock->expects(self::once())
->method('getStatus')
->willReturn(LocalMessage::STATUS_RAW);
$this->loggerInterface->expects(self::never())
->method('warning');
$this->mailboxMapper->expects(self::once())
->method('findById')
->willReturn($mailbox);
$this->imapClientFactory->expects(self::once())
->method('getClient')
->willReturn($client);
$mock->expects(self::once())
->method('getRaw')
->willReturn('123 Content');
$this->messageMapper->expects(self::once())
->method('save')
->willThrowException(new Horde_Imap_Client_Exception());
$mock->expects(self::once())
->method('setStatus')
->with(LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL);
$this->loggerInterface->expects(self::once())
->method('error');
$this->flagRepliedMessageHandler->expects(self::never())
->method('process');
$this->handler->process($account, $mock);
}
public function testProcessAlreadyProcessed(): void {
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$localMessage = $this->getMockBuilder(LocalMessage::class);
$localMessage->addMethods(['getStatus']);
$mock = $localMessage->getMock();
$this->loggerInterface->expects(self::never())
->method('warning');
$mock->expects(self::once())
->method('getStatus')
->willReturn(LocalMessage::STATUS_PROCESSED);
$this->loggerInterface->expects(self::never())
->method('error');
$this->mailboxMapper->expects(self::never())
->method('findById');
$this->imapClientFactory->expects(self::never())
->method('getClient');
$this->messageMapper->expects(self::never())
->method('save');
$this->flagRepliedMessageHandler->expects(self::once())
->method('process');
$this->handler->process($account, $mock);
}
}

Просмотреть файл

@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Unit\Send;
use ChristophWurst\Nextcloud\Testing\TestCase;
use Horde_Imap_Client_Socket;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MailboxMapper;
use OCA\Mail\Db\Message;
use OCA\Mail\Db\MessageMapper as DbMessageMapper;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper;
use OCA\Mail\Send\FlagRepliedMessageHandler;
use OCP\AppFramework\Db\DoesNotExistException;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class FlagRepliedMessageHandlerTest extends TestCase {
private IMAPClientFactory|MockObject $imapClientFactory;
private MailboxMapper|MockObject $mailboxMapper;
private LoggerInterface|MockObject $loggerInterface;
private MockObject|MessageMapper $messageMapper;
private FlagRepliedMessageHandler $handler;
private MockObject|DbMessageMapper $dbMessageMapper;
protected function setUp(): void {
$this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
$this->mailboxMapper = $this->createMock(MailboxMapper::class);
$this->loggerInterface = $this->createMock(LoggerInterface::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
$this->dbMessageMapper = $this->createMock(DbMessageMapper::class);
$this->handler = new FlagRepliedMessageHandler(
$this->imapClientFactory,
$this->mailboxMapper,
$this->loggerInterface,
$this->messageMapper,
$this->dbMessageMapper,
);
}
public function testProcess(): void {
$account = new Account(new MailAccount());
$localMessage = new LocalMessage();
$localMessage->setInReplyToMessageId('ab123');
$localMessage->setStatus(LocalMessage::STATUS_PROCESSED);
$dbMessage = new Message();
$dbMessage->setUid(99);
$dbMessage->setMailboxId(1);
$mailbox = new Mailbox();
$mailbox->setMyAcls('rw');
$client = $this->createMock(Horde_Imap_Client_Socket::class);
$this->dbMessageMapper->expects(self::once())
->method('findByMessageId')
->willReturn([$dbMessage]);
$this->mailboxMapper->expects(self::once())
->method('findById')
->willReturn($mailbox);
$this->loggerInterface->expects(self::never())
->method('warning');
$this->imapClientFactory->expects(self::once())
->method('getClient')
->willReturn($client);
$this->messageMapper->expects(self::once())
->method('addFlag');
$this->dbMessageMapper->expects(self::once())
->method('update');
$this->handler->process($account, $localMessage);
}
public function testProcessError(): void {
$account = new Account(new MailAccount());
$localMessage = new LocalMessage();
$localMessage->setInReplyToMessageId('ab123');
$localMessage->setStatus(LocalMessage::STATUS_PROCESSED);
$dbMessage = new Message();
$dbMessage->setUid(99);
$dbMessage->setMailboxId(1);
$mailbox = new Mailbox();
$mailbox->setMyAcls('rw');
$client = $this->createMock(Horde_Imap_Client_Socket::class);
$this->dbMessageMapper->expects(self::once())
->method('findByMessageId')
->willReturn([$dbMessage]);
$this->mailboxMapper->expects(self::once())
->method('findById')
->willReturn($mailbox);
$this->imapClientFactory->expects(self::once())
->method('getClient')
->willReturn($client);
$this->messageMapper->expects(self::once())
->method('addFlag')
->willThrowException(new DoesNotExistException(''));
$this->loggerInterface->expects(self::once())
->method('warning');
$this->dbMessageMapper->expects(self::never())
->method('update');
$this->handler->process($account, $localMessage);
}
public function testProcessReadOnly(): void {
$account = new Account(new MailAccount());
$localMessage = new LocalMessage();
$localMessage->setInReplyToMessageId('ab123');
$localMessage->setStatus(LocalMessage::STATUS_PROCESSED);
$dbMessage = new Message();
$dbMessage->setUid(99);
$dbMessage->setMailboxId(1);
$mailbox = new Mailbox();
$mailbox->setMyAcls('r');
$client = $this->createMock(Horde_Imap_Client_Socket::class);
$this->dbMessageMapper->expects(self::once())
->method('findByMessageId')
->willReturn([$dbMessage]);
$this->mailboxMapper->expects(self::once())
->method('findById')
->willReturn($mailbox);
$this->loggerInterface->expects(self::never())
->method('warning');
$this->imapClientFactory->expects(self::once())
->method('getClient')
->willReturn($client);
$this->messageMapper->expects(self::never())
->method('addFlag');
$this->dbMessageMapper->expects(self::never())
->method('update');
$this->handler->process($account, $localMessage);
}
public function testProcessNotFound(): void {
$account = new Account(new MailAccount());
$localMessage = new LocalMessage();
$localMessage->setInReplyToMessageId('ab123');
$localMessage->setStatus(LocalMessage::STATUS_PROCESSED);
$this->dbMessageMapper->expects(self::once())
->method('findByMessageId')
->willReturn([]);
$this->mailboxMapper->expects(self::never())
->method('findById');
$this->loggerInterface->expects(self::never())
->method('warning');
$this->imapClientFactory->expects(self::never())
->method('getClient');
$this->messageMapper->expects(self::never())
->method('addFlag');
$this->dbMessageMapper->expects(self::never())
->method('update');
$this->handler->process($account, $localMessage);
}
public function testProcessNoRepliedMessageId(): void {
$account = new Account(new MailAccount());
$localMessage = new LocalMessage();
$localMessage->setStatus(LocalMessage::STATUS_PROCESSED);
$this->dbMessageMapper->expects(self::never())
->method('findByMessageId');
$this->mailboxMapper->expects(self::never())
->method('findById');
$this->loggerInterface->expects(self::never())
->method('warning');
$this->imapClientFactory->expects(self::never())
->method('getClient');
$this->messageMapper->expects(self::never())
->method('addFlag');
$this->dbMessageMapper->expects(self::never())
->method('update');
$this->handler->process($account, $localMessage);
}
}

Просмотреть файл

@ -0,0 +1,122 @@
<?php
/*
* @copyright 2023 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Unit\Send;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Send\CopySentMessageHandler;
use OCA\Mail\Send\FlagRepliedMessageHandler;
use OCA\Mail\Send\SendHandler;
use PHPUnit\Framework\MockObject\MockObject;
class SendHandlerTest extends TestCase {
private MockObject|IMailTransmission $transmission;
private MockObject|CopySentMessageHandler $copySentMessageHandler;
private MockObject|FlagRepliedMessageHandler $flagRepliedMessageHandler;
private SendHandler $handler;
protected function setUp(): void {
$this->transmission = $this->createMock(IMailTransmission::class);
$this->copySentMessageHandler = $this->createMock(CopySentMessageHandler::class);
$this->flagRepliedMessageHandler = $this->createMock(FlagRepliedMessageHandler::class);
$this->handler = new SendHandler($this->transmission);
$this->handler->setNext($this->copySentMessageHandler)
->setNext($this->flagRepliedMessageHandler);
}
public function testProcess(): void {
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(1);
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$localMessage = new LocalMessage();
$localMessage->setId(100);
$localMessage->setStatus(LocalMessage::STATUS_RAW);
$this->transmission->expects(self::once())
->method('sendMessage')
->with($account, $localMessage);
$this->copySentMessageHandler->expects(self::once())
->method('process');
$this->handler->process($account, $localMessage);
}
public function testProcessAlreadyProcessed(): void {
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(1);
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$localMessage = new LocalMessage();
$localMessage->setId(100);
$localMessage->setStatus(LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL);
$this->transmission->expects(self::never())
->method('sendMessage');
$this->copySentMessageHandler->expects(self::once())
->method('process');
$this->handler->process($account, $localMessage);
}
public function testProcessError(): void {
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(1);
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$localMessage = $this->getMockBuilder(LocalMessage::class);
$localMessage->addMethods(['getStatus']);
$mock = $localMessage->getMock();
$mock->setStatus(10);
$mock->expects(self::any())
->method('getStatus')
->willReturn(LocalMessage::STATUS_SMPT_SEND_FAIL);
$this->transmission->expects(self::once())
->method('sendMessage');
$this->copySentMessageHandler->expects(self::never())
->method('process');
$this->handler->process($account, $mock);
}
}

Просмотреть файл

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Unit\Send;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Send\AntiAbuseHandler;
use OCA\Mail\Send\SentMailboxHandler;
use PHPUnit\Framework\MockObject\MockObject;
class SentMailboxHandlerTest extends TestCase {
private AntiAbuseHandler|MockObject $antiAbuseHandler;
private SentMailboxHandler $handler;
protected function setUp(): void {
$this->antiAbuseHandler = $this->createMock(AntiAbuseHandler::class);
$this->handler = new SentMailboxHandler();
$this->handler->setNext($this->antiAbuseHandler);
}
public function testProcess(): void {
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$mailAccount->setSentMailboxId(1);
$account = new Account($mailAccount);
$localMessage = new LocalMessage();
$localMessage->setStatus(LocalMessage::STATUS_RAW);
$this->antiAbuseHandler->expects(self::once())
->method('process');
$this->handler->process($account, $localMessage);
}
public function testNoSentMailbox(): void {
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$mailAccount->setId(123);
$account = new Account($mailAccount);
$localMessage = $this->getMockBuilder(LocalMessage::class);
$localMessage->addMethods(['setStatus']);
$mock = $localMessage->getMock();
$mock->expects(self::once())
->method('setStatus')
->with(LocalMessage::STATUS_NO_SENT_MAILBOX);
$this->antiAbuseHandler->expects(self::never())
->method('process');
$this->handler->process($account, $mock);
}
}

Просмотреть файл

@ -27,10 +27,8 @@ namespace OCA\Mail\Tests\Unit\Service;
use ChristophWurst\Nextcloud\Testing\ServiceMockObject;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
use OCA\Mail\Model\NewMessageData;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Service\AntiAbuseService;
use OCP\IMemcache;
use OCP\IUser;
@ -54,15 +52,7 @@ class AntiAbuseServiceTest extends TestCase {
public function testThresholdDisabled(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user123');
$account = $this->createMock(Account::class);
$messageData = new NewMessageData(
$account,
new AddressList([]),
new AddressList([]),
new AddressList([]),
'subject',
'henlo',
);
$messageData = new LocalMessage();
$this->serviceMock->getParameter('config')
->expects(self::once())
->method('getAppValue')
@ -84,30 +74,16 @@ class AntiAbuseServiceTest extends TestCase {
public function testThresholdReached(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user123');
$account = $this->createMock(Account::class);
$messageData = new NewMessageData(
$account,
new AddressList(array_map(static function (int $i) {
return Address::fromRaw(
"user$i@domain.tld",
"user$i@domain.tld",
);
}, range(1, 50))),
new AddressList(array_map(static function (int $i) {
return Address::fromRaw(
"user$i@domain.tld",
"user$i@domain.tld",
);
}, range(51, 60))),
new AddressList(array_map(static function (int $i) {
return Address::fromRaw(
"user$i@domain.tld",
"user$i@domain.tld",
);
}, range(51, 70))),
'subject',
'henlo',
);
$messageData = new LocalMessage();
$recipients = array_map(static function (int $i) {
return Recipient::fromParams([
'label' => 'rec ' . $i,
'email' => $i . '@domain.tld',
'type' => Recipient::TYPE_TO,
]);
}, range(0, 70));
$messageData->setRecipients($recipients);
$this->serviceMock->getParameter('config')
->method('getAppValue')
->withConsecutive(
@ -123,7 +99,7 @@ class AntiAbuseServiceTest extends TestCase {
->with(self::anything(), [
'user' => 'user123',
'expected' => 50,
'actual' => 80,
'actual' => 71,
]);
$this->service->onBeforeMessageSent(
@ -135,20 +111,14 @@ class AntiAbuseServiceTest extends TestCase {
public function test15mThreshold(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user123');
$account = $this->createMock(Account::class);
$messageData = new NewMessageData(
$account,
new AddressList([
Address::fromRaw(
"user@domain.tld",
"user@domain.tld",
)
]),
new AddressList([]),
new AddressList([]),
'subject',
'henlo',
);
$messageData = new LocalMessage();
$recipients = Recipient::fromParams([
'label' => 'rec 1',
'email' => 'u1@domain.tld',
'type' => Recipient::TYPE_TO,
]);
$messageData->setRecipients([$recipients]);
$this->serviceMock->getParameter('config')
->method('getAppValue')
->withConsecutive(

Просмотреть файл

@ -26,39 +26,51 @@ namespace OCA\Mail\Tests\Unit\Service;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Mailbox;
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\Db\Message as DbMessage;
use OCA\Mail\Db\MessageMapper as DbMessageMapper;
use OCA\Mail\Events\MessageFlaggedEvent;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\IMAP\MessageMapper as ImapMessageMapper;
use OCA\Mail\Model\NewMessageData;
use OCA\Mail\Service\AntiSpamService;
use OCA\Mail\Service\MailManager;
use OCA\Mail\SMTP\SmtpClientFactory;
use OCP\IConfig;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class AntiSpamServiceTest extends TestCase {
/** @var AntiSpamService */
private $service;
/** @var IConfig|MockObject */
private $config;
/** @var MessageMapper|MockObject */
private $messageMapper;
/** @var IMailTransmission|MockObject */
private $transmission;
private AntiSpamService $service;
private IConfig|MockObject $config;
private DbMessageMapper|MockObject $dbMessageMapper;
private IMAPClientFactory|MockObject $imapClientFactory;
private SmtpClientFactory|MockObject $smtpClientFactory;
private MockObject|ImapMessageMapper $imapMessageMapper;
private LoggerInterface|MockObject $logger;
protected function setUp(): void {
parent::setUp();
$this->config = $this->createMock(IConfig::class);
$this->messageMapper = $this->createMock(MessageMapper::class);
$this->dbMessageMapper = $this->createMock(DbMessageMapper::class);
$this->transmission = $this->createMock(IMailTransmission::class);
$this->mailManager = $this->createMock(MailManager::class);
$this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
$this->smtpClientFactory = $this->createMock(SmtpClientFactory::class);
$this->imapMessageMapper = $this->createMock(ImapMessageMapper::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new AntiSpamService(
$this->config,
$this->messageMapper,
$this->transmission
$this->dbMessageMapper,
$this->mailManager,
$this->imapClientFactory,
$this->smtpClientFactory,
$this->imapMessageMapper,
$this->logger,
);
}
@ -73,10 +85,8 @@ class AntiSpamServiceTest extends TestCase {
->method('getAppValue')
->with('mail', 'antispam_reporting_spam')
->willReturn('');
$this->messageMapper->expects(self::never())
$this->dbMessageMapper->expects(self::never())
->method('getIdForUid');
$this->transmission->expects(self::never())
->method('sendMessage');
$this->service->sendReportEmail($event->getAccount(), $event->getMailbox(), 123, $event->getFlag());
}
@ -92,13 +102,11 @@ class AntiSpamServiceTest extends TestCase {
->method('getAppValue')
->with('mail', 'antispam_reporting_spam')
->willReturn('test@test.com');
$this->messageMapper->expects(self::once())
$this->dbMessageMapper->expects(self::once())
->method('getIdForUid')
->with($event->getMailbox(), 123)
->willReturn(null);
$this->expectException(ServiceException::class);
$this->transmission->expects(self::never())
->method('sendMessage');
$this->service->sendReportEmail($event->getAccount(), $event->getMailbox(), 123, $event->getFlag());
}
@ -124,79 +132,124 @@ class AntiSpamServiceTest extends TestCase {
[['id' => 123, 'type' => 'message/rfc822']]
);
$this->messageMapper->expects(self::once())
$this->dbMessageMapper->expects(self::once())
->method('getIdForUid')
->with($event->getMailbox(), 123)
->willReturn(123);
$this->transmission->expects(self::once())
->method('sendMessage')
->with($messageData)
->willThrowException(new ServiceException());
$this->expectException(ServiceException::class);
$this->expectException(ServiceException::class);
$this->service->sendReportEmail($event->getAccount(), $event->getMailbox(), 123, $event->getFlag());
}
public function testSendReportEmail(): void {
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(10);
$mailAccount->setName('Test');
$mailAccount->setEmail('test@test.com');
$mailAccount->setUserId('test');
$account = new Account($mailAccount);
$dbMessage = new DbMessage();
$dbMessage->setMailboxId(55);
$dbMessage->setUid(123);
$dbMessage->setSubject('Spam Spam and Eggs and Spam');
$mailbox = new Mailbox();
$mailbox->setName('INBOX');
$event = $this->createConfiguredMock(MessageFlaggedEvent::class, [
'getAccount' => $this->createMock(Account::class),
'getAccount' => $account,
'getMailbox' => $this->createMock(Mailbox::class),
'getFlag' => '$junk'
]);
$client = $this->createMock(\Horde_Imap_Client_Socket::class);
$this->config->expects(self::once())
->method('getAppValue')
->with('mail', 'antispam_reporting_spam')
->willReturn('test@test.com');
$messageData = NewMessageData::fromRequest(
$event->getAccount(),
'test@test.com',
null,
null,
'Learn as Junk',
'Learn as Junk',
[['id' => 123, 'type' => 'message/rfc822']]
);
$this->messageMapper->expects(self::once())
$this->dbMessageMapper->expects(self::once())
->method('getIdForUid')
->with($event->getMailbox(), 123)
->willReturn(123);
$this->transmission->expects(self::once())
->method('sendMessage')
->with($messageData);
$this->mailManager->expects(self::once())
->method('getMessage')
->with('test', 123)
->willReturn($dbMessage);
$this->mailManager->expects(self::exactly(2))
->method('getMailbox')
->willReturn($mailbox);
$this->imapClientFactory->expects(self::exactly(2))
->method('getClient')
->willReturn($client);
$client->expects(self::exactly(2))
->method('logout');
$this->imapMessageMapper->expects(self::once())
->method('getFullText')
->with($client, $mailbox->getName(), $dbMessage->getUid(), 'test')
->willReturn('Test');
$this->smtpClientFactory->expects(self::once())
->method('create')
->with($account)
->willReturn($this->createMock(\Horde_Mail_Transport::class));
$this->imapMessageMapper->expects(self::once())
->method('save');
$this->logger->expects(self::never())
->method(self::anything());
$this->service->sendReportEmail($event->getAccount(), $event->getMailbox(), 123, $event->getFlag());
}
public function testSendReportEmailForHam(): void {
public function testSendReportEmailNoSentCopy(): void {
$mailAccount = new MailAccount();
$mailAccount->setSentMailboxId(10);
$mailAccount->setName('Test');
$mailAccount->setEmail('test@test.com');
$mailAccount->setUserId('test');
$account = new Account($mailAccount);
$dbMessage = new DbMessage();
$dbMessage->setMailboxId(55);
$dbMessage->setUid(123);
$dbMessage->setSubject('Spam Spam and Eggs and Spam');
$mailbox = new Mailbox();
$mailbox->setName('INBOX');
$event = $this->createConfiguredMock(MessageFlaggedEvent::class, [
'getAccount' => $this->createMock(Account::class),
'getAccount' => $account,
'getMailbox' => $this->createMock(Mailbox::class),
'getFlag' => '$notjunk'
'getFlag' => '$junk'
]);
$client = $this->createMock(\Horde_Imap_Client_Socket::class);
$this->config->expects(self::once())
->method('getAppValue')
->with('mail', 'antispam_reporting_ham')
->with('mail', 'antispam_reporting_spam')
->willReturn('test@test.com');
$messageData = NewMessageData::fromRequest(
$event->getAccount(),
'test@test.com',
null,
null,
'Learn as Not Junk',
'Learn as Not Junk',
[['id' => 123, 'type' => 'message/rfc822']]
);
$this->messageMapper->expects(self::once())
$this->dbMessageMapper->expects(self::once())
->method('getIdForUid')
->with($event->getMailbox(), 123)
->willReturn(123);
$this->transmission->expects(self::once())
->method('sendMessage')
->with($messageData);
$this->mailManager->expects(self::once())
->method('getMessage')
->with('test', 123)
->willReturn($dbMessage);
$this->mailManager->expects(self::exactly(2))
->method('getMailbox')
->willReturn($mailbox);
$this->imapClientFactory->expects(self::exactly(2))
->method('getClient')
->willReturn($client);
$client->expects(self::exactly(2))
->method('logout');
$this->imapMessageMapper->expects(self::once())
->method('getFullText')
->with($client, $mailbox->getName(), $dbMessage->getUid(), 'test')
->willReturn('Test');
$this->smtpClientFactory->expects(self::once())
->method('create')
->with($account)
->willReturn($this->createMock(\Horde_Mail_Transport::class));
$this->imapMessageMapper->expects(self::once())
->method('save')
->willThrowException(new \Horde_Imap_Client_Exception());
$this->logger->expects(self::once())
->method('error');
$this->service->sendReportEmail($event->getAccount(), $event->getMailbox(), 123, $event->getFlag());
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -36,6 +36,7 @@ use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Send\Chain;
use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\MailTransmission;
@ -91,6 +92,7 @@ class OutboxServiceTest extends TestCase {
$this->accountService = $this->createMock(AccountService::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->chain = $this->createMock(Chain::class);
$this->outboxService = new OutboxService(
$this->transmission,
$this->mapper,
@ -101,6 +103,7 @@ class OutboxServiceTest extends TestCase {
$this->accountService,
$this->timeFactory,
$this->logger,
$this->chain,
);
$this->userId = 'linus';
$this->time = $this->createMock(ITimeFactory::class);
@ -121,8 +124,9 @@ class OutboxServiceTest extends TestCase {
'body' => 'Test',
'html' => false,
'reply_to_id' => null,
'draft_id' => 99
'draft_id' => 99,
'status' => 0,
'raw' => 'Test',
],
[
'id' => 2,
@ -134,7 +138,9 @@ class OutboxServiceTest extends TestCase {
'body' => 'Second Test',
'html' => true,
'reply_to_id' => null,
'draft_id' => null
'draft_id' => null,
'status' => 0,
'raw' => 'Second Test',
]
]);
@ -385,6 +391,7 @@ class OutboxServiceTest extends TestCase {
$account = $this->createConfiguredMock(Account::class, [
'getUserId' => $this->userId
]);
$this->mapper->expects(self::once())
->method('updateWithRecipients')
->with($message, [$rTo], $cc, $bcc)
@ -396,6 +403,7 @@ class OutboxServiceTest extends TestCase {
->method('getClient');
$this->attachmentService->expects(self::never())
->method('handleAttachments');
$result = $this->outboxService->updateMessage($account, $message, $to, $cc, $bcc, $attachments);
$this->assertEmpty($result->getAttachments());
}
@ -436,6 +444,7 @@ class OutboxServiceTest extends TestCase {
public function testSendMessage(): void {
$message = new LocalMessage();
$message->setId(1);
$message->setStatus(LocalMessage::STATUS_RAW);
$recipient = new Recipient();
$recipient->setEmail('museum@startdewvalley.com');
$recipient->setLabel('Gunther');
@ -452,15 +461,36 @@ class OutboxServiceTest extends TestCase {
'getUserId' => $this->userId
]);
$this->transmission->expects(self::once())
->method('sendLocalMessage')
$this->chain->expects(self::once())
->method('process')
->with($account, $message);
$this->outboxService->sendMessage($message, $account);
}
public function testSendMessageAlreadyProcessed(): void {
$message = new LocalMessage();
$message->setId(1);
$message->setStatus(LocalMessage::STATUS_PROCESSED);
$recipient = new Recipient();
$recipient->setEmail('museum@startdewvalley.com');
$recipient->setLabel('Gunther');
$recipient->setType(Recipient::TYPE_TO);
$recipients = [$recipient];
$attachment = new LocalAttachment();
$attachment->setMimeType('image/png');
$attachment->setFileName('SlimesInTheMines.png');
$attachment->setCreatedAt($this->time->getTime());
$attachments = [$attachment];
$message->setRecipients($recipients);
$message->setAttachments($attachments);
$account = $this->createConfiguredMock(Account::class, [
'getUserId' => $this->userId
]);
$this->chain->expects(self::once())
->method('process')
->with($account, $message);
$this->attachmentService->expects(self::once())
->method('deleteLocalMessageAttachments')
->with($account->getUserId(), $message->getId());
$this->mapper->expects(self::once())
->method('deleteWithRecipients')
->with($message);
$this->outboxService->sendMessage($message, $account);
}
@ -468,6 +498,7 @@ class OutboxServiceTest extends TestCase {
public function testSendMessageTransmissionError(): void {
$message = new LocalMessage();
$message->setId(1);
$message->setStatus(LocalMessage::STATUS_NO_SENT_MAILBOX);
$recipient = new Recipient();
$recipient->setEmail('museum@startdewvalley.com');
$recipient->setLabel('Gunther');
@ -484,17 +515,12 @@ class OutboxServiceTest extends TestCase {
'getUserId' => $this->userId
]);
$this->transmission->expects(self::once())
->method('sendLocalMessage')
->with($account, $message)
->willThrowException(new ClientException());
$this->attachmentService->expects(self::never())
->method('deleteLocalMessageAttachments');
$this->mapper->expects(self::never())
->method('deleteWithRecipients');
$this->chain->expects(self::once())
->method('process')
->with($account, $message);
$this->expectException(ClientException::class);
$this->outboxService->sendMessage($message, $account);
$this->assertEquals(LocalMessage::STATUS_NO_SENT_MAILBOX, $message->getStatus());
}
public function testConvertToOutboxMessageNoRecipients(): void {

Просмотреть файл

@ -0,0 +1,375 @@
<?php
declare(strict_types=1);
/*
* @copyright 2024 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\Mail\Tests\Unit\Service;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Account;
use OCA\Mail\Address;
use OCA\Mail\AddressList;
use OCA\Mail\Db\LocalAttachment;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\MailAccount;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Db\SmimeCertificate;
use OCA\Mail\Exception\AttachmentNotFoundException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Exception\SmimeSignException;
use OCA\Mail\Model\Message;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\GroupsIntegration;
use OCA\Mail\Service\SmimeService;
use OCA\Mail\Service\TransmissionService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\Files\SimpleFS\ISimpleFile;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class TransmissionServiceTest extends TestCase {
private GroupsIntegration|MockObject $groupsIntegration;
private AttachmentService|MockObject $attachmentService;
private LoggerInterface|MockObject $logger;
private SmimeService|MockObject $smimeService;
private MockObject|TransmissionService $transmissionService;
protected function setUp(): void {
parent::setUp();
$this->attachmentService = $this->createMock(AttachmentService::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->smimeService = $this->createMock(SmimeService::class);
$this->groupsIntegration = $this->createMock(GroupsIntegration::class);
$this->transmissionService = new TransmissionService(
$this->groupsIntegration,
$this->attachmentService,
$this->logger,
$this->smimeService,
);
}
public function testGetAddressList() {
$expected = new AddressList([Address::fromRaw('Bob', 'bob@test.com')]);
$recipient = new Recipient();
$recipient->setLabel('Bob');
$recipient->setEmail('bob@test.com');
$recipient->setType(Recipient::TYPE_TO);
$localMessage = new LocalMessage();
$localMessage->setRecipients([$recipient]);
$this->groupsIntegration->expects(self::once())
->method('expand')
->willReturn([$recipient]);
$actual = $this->transmissionService->getAddressList($localMessage, Recipient::TYPE_TO);
$this->assertEquals($expected, $actual);
}
public function testGetAttachments() {
$id = 1;
$expected = [[
'type' => 'local',
'id' => $id
]];
$attachment = new LocalAttachment();
$attachment->setId($id);
$localMessage = new LocalMessage();
$localMessage->setAttachments([$attachment]);
$actual = $this->transmissionService->getAttachments($localMessage);
$this->assertEquals($expected, $actual);
}
public function testHandleAttachment() {
$id = 1;
$expected = [
'type' => 'local',
'id' => $id
];
[$localAttachment, $file] = [
new LocalAttachment(),
$this->createMock(ISimpleFile::class)
];
$message = $this->createMock(Message::class);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$this->attachmentService->expects(self::once())
->method('getAttachment')
->willReturn([$localAttachment, $file]);
$this->logger->expects(self::never())
->method('warning');
$this->transmissionService->handleAttachment($account, $expected);
}
public function testHandleAttachmentNoId() {
$attachment = [[
'type' => 'local',
]];
$message = $this->createMock(Message::class);
$account = new Account(new MailAccount());
$this->logger->expects(self::once())
->method('warning');
$this->transmissionService->handleAttachment($account, $attachment);
}
public function testHandleAttachmentNotFound() {
$attachment = [
'id' => 1,
'type' => 'local',
];
$message = $this->createMock(Message::class);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$this->attachmentService->expects(self::once())
->method('getAttachment')
->willThrowException(new AttachmentNotFoundException());
$this->logger->expects(self::once())
->method('warning');
$this->transmissionService->handleAttachment($account, $attachment);
}
public function testGetSignMimePart() {
$send = new \Horde_Mime_Part();
$send->setContents('Test');
$localMessage = new LocalMessage();
$localMessage->setSmimeSign(true);
$localMessage->setSmimeCertificateId(1);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$smimeCertificate = new SmimeCertificate();
$smimeCertificate->setCertificate('123');
$this->smimeService->expects(self::once())
->method('findCertificate')
->willReturn($smimeCertificate);
$this->smimeService->expects(self::once())
->method('signMimePart');
$this->transmissionService->getSignMimePart($localMessage, $account, $send);
$this->assertEquals(LocalMessage::STATUS_RAW, $localMessage->getStatus());
}
public function testGetSignMimePartNoCertId() {
$send = new \Horde_Mime_Part();
$send->setContents('Test');
$localMessage = new LocalMessage();
$localMessage->setSmimeSign(true);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$this->smimeService->expects(self::never())
->method('findCertificate');
$this->smimeService->expects(self::never())
->method('signMimePart');
$this->expectException(ServiceException::class);
$this->transmissionService->getSignMimePart($localMessage, $account, $send);
$this->assertEquals(LocalMessage::STATUS_SMIME_SIGN_NO_CERT_ID, $localMessage->getStatus());
}
public function testGetSignMimePartNoCertFound() {
$send = new \Horde_Mime_Part();
$send->setContents('Test');
$localMessage = new LocalMessage();
$localMessage->setSmimeSign(true);
$localMessage->setSmimeCertificateId(1);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$this->smimeService->expects(self::once())
->method('findCertificate')
->willThrowException(new DoesNotExistException(''));
$this->smimeService->expects(self::never())
->method('signMimePart');
$this->expectException(ServiceException::class);
$this->transmissionService->getSignMimePart($localMessage, $account, $send);
$this->assertEquals(LocalMessage::STATUS_SMIME_SIGN_CERT, $localMessage->getStatus());
}
public function testGetSignMimePartFailedSigning() {
$send = new \Horde_Mime_Part();
$send->setContents('Test');
$localMessage = new LocalMessage();
$localMessage->setSmimeSign(true);
$localMessage->setSmimeCertificateId(1);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$smimeCertificate = new SmimeCertificate();
$smimeCertificate->setCertificate('123');
$this->smimeService->expects(self::once())
->method('findCertificate')
->willReturn($smimeCertificate);
$this->smimeService->expects(self::once())
->method('signMimePart')
->willThrowException(new SmimeSignException());
$this->expectException(ServiceException::class);
$this->transmissionService->getSignMimePart($localMessage, $account, $send);
$this->assertEquals(LocalMessage::STATUS_SMIME_SIGN_FAIL, $localMessage->getStatus());
}
public function testGetEncryptMimePart() {
$send = new \Horde_Mime_Part();
$send->setContents('Test');
$localMessage = new LocalMessage();
$localMessage->setSmimeEncrypt(true);
$localMessage->setSmimeCertificateId(1);
$to = new AddressList([Address::fromRaw('Bob', 'bob@test.com')]);
$cc = new AddressList([]);
$bcc = new AddressList([]);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$smimeCertificate = new SmimeCertificate();
$smimeCertificate->setCertificate('123');
$this->smimeService->expects(self::once())
->method('findCertificatesByAddressList')
->willReturn([$smimeCertificate]);
$this->smimeService->expects(self::once())
->method('findCertificate')
->willReturn($smimeCertificate);
$this->smimeService->expects(self::once())
->method('encryptMimePart');
$this->transmissionService->getEncryptMimePart($localMessage, $to, $cc, $bcc, $account, $send);
$this->assertEquals(LocalMessage::STATUS_RAW, $localMessage->getStatus());
}
public function testGetEncryptMimePartNoCertId() {
$send = new \Horde_Mime_Part();
$send->setContents('Test');
$localMessage = new LocalMessage();
$localMessage->setSmimeEncrypt(true);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$to = new AddressList([Address::fromRaw('Bob', 'bob@test.com')]);
$cc = new AddressList([]);
$bcc = new AddressList([]);
$this->expectException(ServiceException::class);
$this->transmissionService->getEncryptMimePart($localMessage, $to, $cc, $bcc, $account, $send);
$this->assertEquals(LocalMessage::STATUS_SMIME_ENCRYPT_NO_CERT_ID, $localMessage->getStatus());
}
public function testGetEncryptMimePartNoAddressCerts() {
$send = new \Horde_Mime_Part();
$send->setContents('Test');
$localMessage = new LocalMessage();
$localMessage->setSmimeEncrypt(true);
$localMessage->setSmimeCertificateId(1);
$to = new AddressList([Address::fromRaw('Bob', 'bob@test.com')]);
$cc = new AddressList([]);
$bcc = new AddressList([]);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$smimeCertificate = new SmimeCertificate();
$smimeCertificate->setCertificate('123');
$this->smimeService->expects(self::once())
->method('findCertificatesByAddressList')
->willThrowException(new ServiceException());
$this->smimeService->expects(self::never())
->method('findCertificate');
$this->smimeService->expects(self::never())
->method('encryptMimePart');
$this->expectException(ServiceException::class);
$this->transmissionService->getEncryptMimePart($localMessage, $to, $cc, $bcc, $account, $send);
$this->assertEquals(LocalMessage::STATUS_SMIME_ENCRYT_FAIL, $localMessage->getStatus());
}
public function testGetEncryptMimePartNoCert() {
$send = new \Horde_Mime_Part();
$send->setContents('Test');
$localMessage = new LocalMessage();
$localMessage->setSmimeEncrypt(true);
$localMessage->setSmimeCertificateId(1);
$to = new AddressList([Address::fromRaw('Bob', 'bob@test.com')]);
$cc = new AddressList([]);
$bcc = new AddressList([]);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$smimeCertificate = new SmimeCertificate();
$smimeCertificate->setCertificate('123');
$this->smimeService->expects(self::once())
->method('findCertificatesByAddressList')
->willReturn([$smimeCertificate]);
$this->smimeService->expects(self::once())
->method('findCertificate')
->willThrowException(new DoesNotExistException(''));
$this->smimeService->expects(self::never())
->method('encryptMimePart');
$this->expectException(ServiceException::class);
$this->transmissionService->getEncryptMimePart($localMessage, $to, $cc, $bcc, $account, $send);
$this->assertEquals(LocalMessage::STATUS_SMIME_ENCRYPT_CERT, $localMessage->getStatus());
}
public function testGetEncryptMimePartEncryptFail() {
$send = new \Horde_Mime_Part();
$send->setContents('Test');
$localMessage = new LocalMessage();
$localMessage->setSmimeEncrypt(true);
$localMessage->setSmimeCertificateId(1);
$to = new AddressList([Address::fromRaw('Bob', 'bob@test.com')]);
$cc = new AddressList([]);
$bcc = new AddressList([]);
$mailAccount = new MailAccount();
$mailAccount->setUserId('bob');
$account = new Account($mailAccount);
$smimeCertificate = new SmimeCertificate();
$smimeCertificate->setCertificate('123');
$this->smimeService->expects(self::once())
->method('findCertificatesByAddressList')
->willReturn([$smimeCertificate]);
$this->smimeService->expects(self::once())
->method('findCertificate')
->willReturn($smimeCertificate);
$this->smimeService->expects(self::once())
->method('encryptMimePart')
->willThrowException(new ServiceException());
$this->expectException(ServiceException::class);
$this->transmissionService->getEncryptMimePart($localMessage, $to, $cc, $bcc, $account, $send);
$this->assertEquals(LocalMessage::STATUS_SMIME_ENCRYT_FAIL, $localMessage->getStatus());
}
}