f7cloud_client/apps/mail/lib/Service/DraftsService.php
root 8b6a0139db f7cloud_client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:59:26 +00:00

241 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2022 F7cloud GmbH and F7cloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Service;
use OCA\Mail\Account;
use OCA\Mail\Contracts\IMailManager;
use OCA\Mail\Contracts\IMailTransmission;
use OCA\Mail\Db\LocalMessage;
use OCA\Mail\Db\LocalMessageMapper;
use OCA\Mail\Db\Recipient;
use OCA\Mail\Events\DraftMessageCreatedEvent;
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\IMAP\IMAPClientFactory;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use Psr\Log\LoggerInterface;
use Throwable;
class DraftsService {
private IMailTransmission $transmission;
private LocalMessageMapper $mapper;
private AttachmentService $attachmentService;
private IEventDispatcher $eventDispatcher;
private IMAPClientFactory $clientFactory;
private IMailManager $mailManager;
private LoggerInterface $logger;
private AccountService $accountService;
private ITimeFactory $time;
public function __construct(IMailTransmission $transmission,
LocalMessageMapper $mapper,
AttachmentService $attachmentService,
IEventDispatcher $eventDispatcher,
IMAPClientFactory $clientFactory,
IMailManager $mailManager,
LoggerInterface $logger,
AccountService $accountService,
ITimeFactory $time) {
$this->transmission = $transmission;
$this->mapper = $mapper;
$this->attachmentService = $attachmentService;
$this->eventDispatcher = $eventDispatcher;
$this->clientFactory = $clientFactory;
$this->mailManager = $mailManager;
$this->logger = $logger;
$this->accountService = $accountService;
$this->time = $time;
}
/**
* @param array $recipients
* @param int $type
* @return Recipient[]
*/
private static function convertToRecipient(array $recipients, int $type): array {
return array_map(static function ($recipient) use ($type) {
$r = new Recipient();
$r->setType($type);
$r->setLabel($recipient['label'] ?? $recipient['email']);
$r->setEmail($recipient['email']);
return $r;
}, $recipients);
}
/**
* @param string $userId
* @param LocalMessage $message
* @return void
*/
public function deleteMessage(string $userId, LocalMessage $message): void {
$this->attachmentService->deleteLocalMessageAttachments($userId, $message->getId());
$this->mapper->deleteWithRecipients($message);
}
/**
* @param Account $account
* @param LocalMessage $message
* @param array<int, string[]> $to
* @param array<int, string[]> $cc
* @param array<int, string[]> $bcc
* @param array $attachments
* @return LocalMessage
*/
public function saveMessage(Account $account, LocalMessage $message, array $to, array $cc, array $bcc, array $attachments = []): LocalMessage {
$toRecipients = self::convertToRecipient($to, Recipient::TYPE_TO);
$ccRecipients = self::convertToRecipient($cc, Recipient::TYPE_CC);
$bccRecipients = self::convertToRecipient($bcc, Recipient::TYPE_BCC);
// if this message has a "sent at" datestamp and the type is set as Type Outbox
// check for a valid recipient
if ($message->getType() === LocalMessage::TYPE_OUTGOING && $toRecipients === []) {
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 === []) {
$message->setAttachments($attachments);
return $message;
}
$client = $this->clientFactory->getClient($account);
try {
$attachmentIds = $this->attachmentService->handleAttachments($account, $attachments, $client);
} finally {
$client->logout();
}
$message->setAttachments($this->attachmentService->saveLocalMessageAttachments($account->getUserId(), $message->getId(), $attachmentIds));
return $message;
}
/**
* @param Account $account
* @param LocalMessage $message
* @param array<int, string[]> $to
* @param array<int, string[]> $cc
* @param array<int, string[]> $bcc
* @param array $attachments
* @return LocalMessage
*/
public function updateMessage(Account $account, LocalMessage $message, array $to, array $cc, array $bcc, array $attachments = []): LocalMessage {
$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 === []) {
$message->setAttachments($this->attachmentService->updateLocalMessageAttachments($account->getUserId(), $message, []));
return $message;
}
$client = $this->clientFactory->getClient($account);
try {
$attachmentIds = $this->attachmentService->handleAttachments($account, $attachments, $client);
} finally {
$client->logout();
}
$message->setAttachments($this->attachmentService->updateLocalMessageAttachments($account->getUserId(), $message, $attachmentIds));
return $message;
}
public function handleDraft(Account $account, int $draftId): void {
$message = $this->mailManager->getMessage($account->getUserId(), $draftId);
$this->eventDispatcher->dispatchTyped(new DraftMessageCreatedEvent($account, $message));
}
/**
* "Send" the message
*
* @param LocalMessage $message
* @param Account $account
* @return void
*/
public function sendMessage(LocalMessage $message, Account $account): void {
try {
$this->transmission->saveLocalDraft($account, $message);
} catch (ClientException|ServiceException $e) {
$this->logger->error('Could not move draft to IMAP', ['exception' => $e]);
// Mark as failed so the message is not moved repeatedly in background
$message->setFailed(true);
$this->mapper->update($message);
throw $e;
}
$this->attachmentService->deleteLocalMessageAttachments($account->getUserId(), $message->getId());
$this->mapper->deleteWithRecipients($message);
}
/**
*
* @param int $id
* @param string $userId
* @return LocalMessage
* @throws DoesNotExistException
*/
public function getMessage(int $id, string $userId): LocalMessage {
return $this->mapper->findById($id, $userId, LocalMessage::TYPE_DRAFT);
}
/**
* @return void
*/
public function flush() {
$messages = $this->mapper->findDueDrafts($this->time->getTime());
if ($messages === []) {
return;
}
$accountIds = array_unique(array_map(static fn ($message) => $message->getAccountId(), $messages));
$accounts = array_combine($accountIds, array_map(function ($accountId) {
try {
return $this->accountService->findById($accountId);
} catch (DoesNotExistException $e) {
// The message belongs to a deleted account
return null;
}
}, $accountIds));
foreach ($messages as $message) {
try {
$account = $accounts[$message->getAccountId()];
if ($account === null) {
// Ignore message of non-existent account
continue;
}
$this->sendMessage(
$message,
$account,
);
$this->logger->debug('Draft {id} moved to IMAP', [
'id' => $message->getId(),
]);
} catch (Throwable $e) {
// Failure of one message should not stop other messages
// Log and continue
$this->logger->warning('Could not move draft {id} to IMAP: ' . $e->getMessage(), [
'id' => $message->getId(),
'exception' => $e,
]);
}
}
}
}