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

195 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 F7cloud GmbH and F7cloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Service;
use Horde_Mime_Part;
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 fn ($recipient) => Address::fromRaw($recipient->getLabel() ?? $recipient->getEmail(), $recipient->getEmail()),
$this->groupsIntegration->expand(
array_filter($message->getRecipients(), static fn (Recipient $recipient) => $recipient->getType() === $type)
)
)
);
}
/**
* @param LocalMessage $message
* @return array|array[]
*/
public function getAttachments(LocalMessage $message): array {
if (empty($message->getAttachments())) {
return [];
}
return array_map(static fn (LocalAttachment $attachment)
// Convert to the untyped nested array used in \OCA\Mail\Controller\AccountsController::send
=> [
'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');
if (!empty($localAttachment->getFileName())) {
$part->setDisposition('attachment');
$part->setName($localAttachment->getFileName());
}
$part->setContents($file->getContent());
/*
* Horde_Mime_Part.setType takes the mimetype (e.g. text/calendar)
* and discards additional parameters (like method=REQUEST).
*
* $part->setType('text/calendar; method=REQUEST')
* $part->getType() => text/calendar
*/
$contentTypeHeader = \Horde_Mime_Headers_ContentParam_ContentType::create();
$contentTypeHeader->decode($localAttachment->getMimeType());
$part->setType($contentTypeHeader->value);
foreach ($contentTypeHeader->params as $label => $data) {
$part->setContentTypeParameter($label, $data);
}
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;
}
}