email sending library change

This commit is contained in:
Yuri Kuznetsov
2025-03-31 19:13:17 +03:00
parent ef9795f6e4
commit 2f5b78f887
18 changed files with 850 additions and 579 deletions
@@ -192,17 +192,16 @@ class AfterFetch implements AfterFetchInterface
}
}
$message = new Message();
$sender = $this->emailSender->create();
$messageId = $email->getMessageId();
if ($messageId) {
$message->getHeaders()->addHeaderLine('In-Reply-To', $messageId);
if ($email->getMessageId()) {
$sender->withAddedHeader('In-Reply-To', $email->getMessageId());
}
$message->getHeaders()->addHeaderLine('Auto-Submitted', 'auto-replied');
$message->getHeaders()->addHeaderLine('X-Auto-Response-Suppress', 'All');
$message->getHeaders()->addHeaderLine('Precedence', 'auto_reply');
$sender
->withAddedHeader('Auto-Submitted', 'auto-replied')
->withAddedHeader('X-Auto-Response-Suppress', 'All')
->withAddedHeader('Precedence', 'auto_reply');
try {
$entityHash = [];
@@ -268,8 +267,6 @@ class AfterFetch implements AfterFetchInterface
$this->entityManager->saveEntity($reply);
$sender = $this->emailSender->create();
$senderParams = SenderParams::create();
if ($inboundEmail->isAvailableForSending()) {
@@ -304,13 +301,12 @@ class AfterFetch implements AfterFetchInterface
$sender
->withParams($senderParams)
->withMessage($message)
->withAttachments($replyData->getAttachmentList())
->send($reply);
$this->entityManager->saveEntity($reply);
} catch (Throwable $e) {
$this->log->error("Inbound Email: Auto-reply error: " . $e->getMessage());
$this->log->error("Inbound Email: Auto-reply error: " . $e->getMessage(), ['exception' => $e]);
}
}
+29 -2
View File
@@ -101,11 +101,23 @@ class EmailSender
return $this->createSender()->withAttachments($attachmentList);
}
/**
* With an envelope from address.
*
* @since 9.1.0
* @noinspection PhpUnused
*/
public function withEnvelopeFromAddress(string $fromAddress): void
{
$this->createSender()->withEnvelopeFromAddress($fromAddress);
}
/**
* With envelope options.
*
* @param array<string, mixed> $options
* @noinspection PhpUnused
* @param array{from: string} $options
* @deprecated As of v9.1.
* @todo Remove in v10.0. Use `withEnvelopeFromAddress`.
*/
public function withEnvelopeOptions(array $options): Sender
{
@@ -114,12 +126,27 @@ class EmailSender
/**
* Set a message instance.
*
* @deprecated As of v9.1. Use `withAddedHeader`.
* @todo Remove in v10.0.
*/
public function withMessage(Message $message): Sender
{
return $this->createSender()->withMessage($message);
}
/**
* Add a header.
*
* @param string $name A header name.
* @param string $value A header value.
* @since 9.1.0
*/
public function withAddedHeader(string $name, string $value): Sender
{
return $this->createSender()->withAddedHeader($name, $value);
}
/**
* Whether system SMTP is configured.
*/
@@ -1,115 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* 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 <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Mail\Mail\Header;
use Laminas\Mail\Header;
class XQueueItemId implements Header\HeaderInterface
{
private string $fieldName = 'X-Queue-Item-Id';
private ?string $id = null;
/**
* @param string $headerLine
* @return self
*/
public static function fromString($headerLine)
{
list($name, $value) = Header\GenericHeader::splitHeaderLine($headerLine);
$valueDecoded = Header\HeaderWrap::mimeDecodeValue($value);
if (strtolower($name) !== 'x-queue-item-id') {
throw new Header\Exception\InvalidArgumentException('Invalid header line for x-queue-item-id string');
}
$header = new self();
$header->setId($valueDecoded);
return $header;
}
/**
* @return string
*/
public function getFieldName()
{
return $this->fieldName;
}
/**
* @param string $value
* @return void
*/
public function setFieldName($value)
{
}
/**
* @param string $encoding
* @return self
*/
public function setEncoding($encoding)
{
return $this;
}
public function setId(?string $id): void
{
$this->id = $id;
}
/**
* @return string
*/
public function getEncoding()
{
return 'ASCII';
}
/**
* @return string
*/
public function toString()
{
return $this->fieldName . ': ' . $this->getFieldValue();
}
/**
* @param bool $format
* @return ?string
*/
public function getFieldValue($format = Header\HeaderInterface::FORMAT_RAW)
{
return $this->id;
}
}
+236 -254
View File
@@ -31,7 +31,6 @@ namespace Espo\Core\Mail;
use Espo\Core\FileStorage\Manager as FileStorageManager;
use Espo\Core\Mail\Exceptions\NoSmtp;
use Espo\Core\Mail\Smtp\TransportFactory;
use Espo\Core\ORM\Repository\Option\SaveOption;
use Espo\ORM\EntityCollection;
use Espo\Core\Field\DateTime;
@@ -43,17 +42,23 @@ use Espo\Entities\Attachment;
use Espo\Entities\Email;
use Espo\ORM\EntityManager;
use Laminas\Mail\Header\ContentType as ContentTypeHeader;
use Laminas\Mail\Header\MessageId as MessageIdHeader;
use Laminas\Mail\Header\Sender as SenderHeader;
use Laminas\Mail\Message;
use Laminas\Mail\Protocol\Exception\RuntimeException as ProtocolRuntimeException;
use Laminas\Mail\Transport\Envelope;
use Laminas\Mail\Transport\Smtp as SmtpTransport;
use Laminas\Mail\Transport\SmtpOptions;
use Laminas\Mime\Message as MimeMessage;
use Laminas\Mime\Mime as Mime;
use Laminas\Mime\Part as MimePart;
use Laminas\Mail\Headers;
use Laminas\Mail\Message as LaminasMessage;
use LogicException;
use RuntimeException;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\Smtp\Auth\CramMd5Authenticator;
use Symfony\Component\Mailer\Transport\Smtp\Auth\LoginAuthenticator;
use Symfony\Component\Mailer\Transport\Smtp\Auth\PlainAuthenticator;
use Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email as Message;
use Symfony\Component\Mime\Part\DataPart;
use Exception;
use InvalidArgumentException;
@@ -63,30 +68,25 @@ use InvalidArgumentException;
*/
class Sender
{
private ?SmtpTransport $transport = null;
private ?EsmtpTransport $transport = null;
private bool $isGlobal = false;
/** @var array<string, mixed> */
private array $params = [];
/** @var array<string, mixed> */
private array $overrideParams = [];
private ?Envelope $envelope = null;
private ?Message $message = null;
private ?string $envelopeFromAddress = null;
private ?LaminasMessage $laminasMessage = null;
/** @var ?iterable<Attachment> */
private $attachmentList = null;
/** @var array{string, string}[] */
private array $headers = [];
private const ATTACHMENT_ATTR_CONTENTS = 'contents';
private const MESSAGE_TYPE_MULTIPART_RELATED = 'multipart/related';
private const MESSAGE_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative';
private const MESSAGE_TYPE_MULTIPART_MIXED = 'multipart/mixed';
private const MESSAGE_TYPE_TEXT_PLAIN = 'text/plain';
private const MESSAGE_TYPE_TEXT_HTML = 'text/html';
public function __construct(
private Config $config,
private EntityManager $entityManager,
private Log $log,
private TransportFactory $transportFactory,
private SendingAccountProvider $accountProvider,
private FileStorageManager $fileStorageManager,
private ConfigDataProvider $configDataProvider,
@@ -97,10 +97,11 @@ class Sender
private function resetParams(): void
{
$this->params = [];
$this->envelope = null;
$this->message = null;
$this->envelopeFromAddress = null;
$this->laminasMessage = null;
$this->attachmentList = null;
$this->overrideParams = [];
$this->headers = [];
}
/**
@@ -163,10 +164,22 @@ class Sender
return $this;
}
/**
* With an envelope from address.
*
* @since 9.1.0
*/
public function withEnvelopeFromAddress(string $fromAddress): void
{
$this->envelopeFromAddress = $fromAddress;
}
/**
* With envelope options.
*
* @param array<string, mixed> $options
* @param array{from: string} $options
* @deprecated As of v9.1.
* @todo Remove in v10.0. Use `withEnvelopeFromAddress`.
*/
public function withEnvelopeOptions(array $options): self
{
@@ -176,10 +189,27 @@ class Sender
/**
* Set a message instance.
*
* @deprecated As of v9.1. Use `withAddedHeader`.
* @todo Remove in v10.0.
*/
public function withMessage(Message $message): self
public function withMessage(LaminasMessage $message): self
{
$this->message = $message;
$this->laminasMessage = $message;
return $this;
}
/**
* Add a header.
*
* @param string $name A header name.
* @param string $value A header value.
* @since 9.1.0
*/
public function withAddedHeader(string $name, string $value): self
{
$this->headers[] = [$name, $value];
return $this;
}
@@ -196,7 +226,6 @@ class Sender
return $this;
}
/**
* @deprecated As of 6.0. Use withSmtpParams.
* @param array<string, mixed> $params
@@ -224,53 +253,73 @@ class Sender
{
$this->params = $params;
$this->transport = $this->transportFactory->create();
$config = $this->config;
$localHostName = $config->get('smtpLocalHostName', gethostname());
$options = [
'name' => $localHostName,
'host' => $params['server'],
'port' => $params['port'],
'connectionConfig' => [],
];
$security = strtolower($params['security'] ?? '');
// SSL is treated as implicit SSL/TLS. TLS is treated as STARTTLS.
// STARTTLS is the most common method.
$scheme = $security == 'ssl' ? 'smtps' : 'smtp';
if ($security === 'starttls' && !defined('OPENSSL_VERSION_NUMBER')) {
throw new RuntimeException("OpenSSL is not available.");
}
// @todo Use `auto_tls=false` if no security when Symfony v7.1 is installed.
// @todo If starttls, it should be enforced.
$transport = (new EsmtpTransportFactory())
->create(
new Dsn(
scheme: $scheme,
host: $params['server'],
port: $params['port'],
)
);
if (!$transport instanceof EsmtpTransport) {
throw new RuntimeException();
}
$this->transport = $transport;
$this->transport->setLocalDomain($localHostName);
$authMechanism = null;
$connectionOptions = $params['connectionOptions'] ?? [];
$authString = $connectionOptions['authString'] ?? null;
foreach ($connectionOptions as $key => $value) {
$options['connectionConfig'][$key] = $value;
}
if ($authString) {
$decodedAuthString = base64_decode($authString);
if ($params['auth'] ?? false) {
$authMechanism = $params['authMechanism'] ?? $params['smtpAuthMechanism'] ?? null;
/** @noinspection RegExpRedundantEscape */
if (preg_match("/user=(.*?)\\\1auth=Bearer (.*?)\\\1\\\1/", $decodedAuthString, $matches) !== false) {
$username = $matches[1];
$token = $matches[2];
if ($authMechanism) {
$authMechanism = preg_replace("([.]{2,})", '', $authMechanism);
/** @noinspection SpellCheckingInspection */
if (in_array($authMechanism, ['login', 'crammd5', 'plain'])) {
$options['connectionClass'] = $authMechanism;
} else {
$options['connectionClass'] = 'login';
}
} else {
$options['connectionClass'] = 'login';
$this->transport->setUsername($username);
$this->transport->setPassword($token);
}
$options['connectionConfig']['username'] = $params['username'];
$options['connectionConfig']['password'] = $params['password'] ?? null;
$authMechanism = 'xoauth';
} else if ($params['auth'] ?? false) {
$authMechanism = ($params['authMechanism'] ?? $params['smtpAuthMechanism']) ?: 'login';
$this->transport->setUsername($params['username'] ?? '');
$this->transport->setPassword($params['password'] ?? '');
}
$authClassName = $params['authClassName'] ?? $params['smtpAuthClassName'] ?? null;
if ($authClassName) {
$options['connectionClass'] = $authClassName;
}
if ($params['security'] ?? null) {
$options['connectionConfig']['ssl'] = strtolower($params['security']);
if ($authMechanism === 'login') {
$this->transport->setAuthenticators([new LoginAuthenticator()]);
} else if ($authMechanism === 'crammd5') {
$this->transport->setAuthenticators([new CramMd5Authenticator()]);
} else if ($authMechanism === 'plain') {
$this->transport->setAuthenticators([new PlainAuthenticator()]);
} else if ($authMechanism === 'xoauth') {
$this->transport->setAuthenticators([new XOAuth2Authenticator()]);
}
if (array_key_exists('fromName', $params)) {
@@ -280,14 +329,6 @@ class Sender
if (array_key_exists('fromAddress', $params)) {
$this->params['fromAddress'] = $params['fromAddress'];
}
$this->transport->setOptions(
new SmtpOptions($options)
);
if ($this->envelope) {
$this->transport->setEnvelope($this->envelope);
}
}
/**
@@ -321,10 +362,11 @@ class Sender
$this->applyGlobal();
}
$message = $this->message ?? new Message();
$message = new Message();
$params = array_merge($this->params, $this->overrideParams);
$this->applyHeaders($message);
$this->applyFrom($email, $message, $params);
$this->applyReplyTo($email, $message, $params);
$this->addRecipientAddresses($email, $message);
@@ -332,13 +374,17 @@ class Sender
$this->applyBody($email, $message);
$this->applyMessageId($email, $message);
$message->setEncoding('UTF-8');
$this->applyLaminasMessageHeaders($message);
assert($this->transport !== null);
if (!$this->transport) {
throw new LogicException();
}
$envelope = $this->prepareEnvelope($message);
try {
$this->transport->send($message);
} catch (Exception $e) {
$this->transport->send($message, $envelope);
} catch (Exception|TransportExceptionInterface $e) {
$this->resetParams();
$this->useGlobal();
@@ -355,9 +401,50 @@ class Sender
}
/**
* @return MimePart[]
* @return DataPart[]
*/
private function getInlineAttachmentPartList(Email $email): array
private function getAttachmentParts(Email $email): array
{
/** @var EntityCollection<Attachment> $collection */
$collection = $this->entityManager
->getCollectionFactory()
->create(Attachment::ENTITY_TYPE);
if (!$email->isNew()) {
foreach ($email->getAttachments() as $attachment) {
$collection[] = $attachment;
}
}
if ($this->attachmentList !== null) {
foreach ($this->attachmentList as $attachment) {
$collection[] = $attachment;
}
}
$list = [];
foreach ($collection as $attachment) {
$contents = $attachment->has(self::ATTACHMENT_ATTR_CONTENTS) ?
$attachment->get(self::ATTACHMENT_ATTR_CONTENTS) :
$this->fileStorageManager->getContents($attachment);
$part = new DataPart(
body: $contents,
filename: $this->prepareAttachmentFileName($attachment),
contentType: $attachment->getType(),
);
$list[] = $part;
}
return $list;
}
/**
* @return DataPart[]
*/
private function getInlineAttachmentParts(Email $email): array
{
$list = [];
@@ -366,17 +453,11 @@ class Sender
$attachment->get(self::ATTACHMENT_ATTR_CONTENTS) :
$this->fileStorageManager->getContents($attachment);
$mimePart = new MimePart($contents);
$part = (new DataPart($contents, null, $attachment->getType()))
->asInline()
->setContentId($attachment->getId() . '@espo');
$mimePart->disposition = Mime::DISPOSITION_INLINE;
$mimePart->encoding = Mime::ENCODING_BASE64;
$mimePart->id = $attachment->getId();
if ($attachment->getType()) {
$mimePart->type = $attachment->getType();
}
$list[] = $mimePart;
$list[] = $part;
}
return $list;
@@ -385,20 +466,21 @@ class Sender
/**
* @throws SendingError
*/
private function handleException(Exception $e): never
private function handleException(Exception|TransportExceptionInterface $e): never
{
if ($e instanceof ProtocolRuntimeException) {
if ($e instanceof TransportExceptionInterface) {
$message = "unknownError";
if (
stripos($e->getMessage(), 'password') !== false ||
stripos($e->getMessage(), 'credentials') !== false ||
stripos($e->getMessage(), '5.7.8') !== false
stripos($e->getMessage(), '5.7.8') !== false ||
stripos($e->getMessage(), '5.7.3') !== false
) {
$message = 'invalidCredentials';
}
$this->log->error("Email sending error: " . $e->getMessage());
$this->log->error("Email sending error: " . $e->getMessage(), ['exception' => $e]);
throw new SendingError($message);
}
@@ -417,14 +499,14 @@ class Sender
}
/**
* @deprecated As of v6.0. Use withEnvelopeOptions.
* @deprecated As of v6.0.
*
* @param array<string, mixed> $options
* @todo Remove in v10.0.
* @param array{from: string} $options
* @todo Make private in v10.0. Use `withEnvelopeFromAddress`.
*/
public function setEnvelopeOptions(array $options): self
{
$this->envelope = new Envelope($options);
$this->envelopeFromAddress = $options['from'];
return $this;
}
@@ -464,50 +546,7 @@ class Sender
}
}
/**
* @return MimePart[]
*/
private function getAttachmentParts(Email $email): array
{
/** @var EntityCollection<Attachment> $collection */
$collection = $this->entityManager
->getCollectionFactory()
->create(Attachment::ENTITY_TYPE);
if (!$email->isNew()) {
foreach ($email->getAttachments() as $attachment) {
$collection[] = $attachment;
}
}
if ($this->attachmentList !== null) {
foreach ($this->attachmentList as $attachment) {
$collection[] = $attachment;
}
}
$attachmentPartList = [];
foreach ($collection as $attachment) {
$contents = $attachment->has(self::ATTACHMENT_ATTR_CONTENTS) ?
$attachment->get(self::ATTACHMENT_ATTR_CONTENTS) :
$this->fileStorageManager->getContents($attachment);
$mimePart = new MimePart($contents);
$mimePart->disposition = Mime::DISPOSITION_ATTACHMENT;
$mimePart->encoding = Mime::ENCODING_BASE64;
$mimePart->filename = $this->prepareAttachmentFileName($attachment);
if ($attachment->getType()) {
$mimePart->type = $attachment->getType();
}
$attachmentPartList[] = $mimePart;
}
return $attachmentPartList;
}
private function prepareAttachmentFileName(mixed $attachment): string
{
@@ -541,7 +580,7 @@ class Sender
$email->setFromAddress($fromAddress);
}
$message->addFrom($fromAddress, $fromName);
$message->addFrom(new Address($fromAddress, $fromName ?? ''));
$fromString = '<' . $fromAddress . '>';
@@ -551,11 +590,7 @@ class Sender
$email->set('fromString', $fromString);
$senderHeader = new SenderHeader();
$senderHeader->setAddress($fromAddress);
$message->getHeaders()->addHeader($senderHeader);
$message->sender($fromAddress);
}
/**
@@ -570,7 +605,7 @@ class Sender
return;
}
$message->setReplyTo($address, $name);
$message->replyTo(new Address($address, $name ?? ''));
$email->setReplyToAddressList([$address]);
}
@@ -595,128 +630,75 @@ class Sender
$messageId = substr($messageId, 1, strlen($messageId) - 2);
}
$header = (new MessageIdHeader())->setId($messageId);
$message->getHeaders()->addHeader($header);
$message->getHeaders()->addIdHeader('Message-ID', $messageId);
}
private function applyBody(Email $email, Message $message): void
{
$attachmentPartList = $this->getAttachmentParts($email);
$inlineAttachmentPartList = $this->getInlineAttachmentPartList($email);
$message->text($email->getBodyPlainForSending());
$body = new MimeMessage();
$textPart = (new MimePart($email->getBodyPlainForSending()))
->setType(self::MESSAGE_TYPE_TEXT_PLAIN)
->setEncoding(Mime::ENCODING_QUOTEDPRINTABLE)
->setCharset('utf-8');
$htmlPart = $email->isHtml() ?
(new MimePart($email->getBodyForSending()))
->setEncoding(Mime::ENCODING_QUOTEDPRINTABLE)
->setType(self::MESSAGE_TYPE_TEXT_HTML)
->setCharset('utf-8') :
null;
$messageType = null;
$hasAttachments = count($attachmentPartList) !== 0;
$hasInlineAttachments = count($inlineAttachmentPartList) !== 0;
if ($hasAttachments || $hasInlineAttachments) {
if ($htmlPart) {
$messageType = self::MESSAGE_TYPE_MULTIPART_MIXED;
$alternative = (new MimeMessage())
->addPart($textPart)
->addPart($htmlPart);
$alternativePart = (new MimePart($alternative->generateMessage()))
->setType(self::MESSAGE_TYPE_MULTIPART_ALTERNATIVE)
->setBoundary($alternative->getMime()->boundary());
if ($hasInlineAttachments && $hasAttachments) {
$related = (new MimeMessage())->addPart($alternativePart);
foreach ($inlineAttachmentPartList as $attachmentPart) {
$related->addPart($attachmentPart);
}
$body->addPart(
(new MimePart($related->generateMessage()))
->setType(self::MESSAGE_TYPE_MULTIPART_RELATED)
->setBoundary($related->getMime()->boundary())
);
}
if ($hasInlineAttachments && !$hasAttachments) {
$messageType = self::MESSAGE_TYPE_MULTIPART_RELATED;
$body->addPart($alternativePart);
foreach ($inlineAttachmentPartList as $attachmentPart) {
$body->addPart($attachmentPart);
}
}
if (!$hasInlineAttachments) {
$body->addPart($alternativePart);
}
}
if (!$htmlPart) {
$messageType = self::MESSAGE_TYPE_MULTIPART_RELATED;
$body->addPart($textPart);
foreach ($inlineAttachmentPartList as $attachmentPart) {
$body->addPart($attachmentPart);
}
}
foreach ($attachmentPartList as $attachmentPart) {
$body->addPart($attachmentPart);
}
} else {
if ($email->isHtml()) {
$body->setParts([$textPart, $htmlPart]);
$messageType = self::MESSAGE_TYPE_MULTIPART_ALTERNATIVE;
} else {
$body = $email->getBodyPlainForSending();
$messageType = self::MESSAGE_TYPE_TEXT_PLAIN;
}
if ($email->isHtml()) {
$message->html($email->getBodyForSending());
}
$message->setBody($body);
if ($messageType === self::MESSAGE_TYPE_TEXT_PLAIN) {
if ($message->getHeaders()->has('content-type')) {
$message->getHeaders()->removeHeader('content-type');
}
$message->getHeaders()->addHeaderLine('Content-Type', 'text/plain; charset=UTF-8');
return;
foreach ($this->getAttachmentParts($email) as $part) {
$message->addPart($part);
}
if (!$message->getHeaders()->has('content-type')) {
$contentTypeHeader = new ContentTypeHeader();
$message->getHeaders()->addHeader($contentTypeHeader);
foreach ($this->getInlineAttachmentParts($email) as $part) {
$message->addPart($part);
}
$contentTypeHeader = $message->getHeaders()->get('content-type');
assert($contentTypeHeader instanceof ContentTypeHeader);
$contentTypeHeader->setType($messageType);
}
private function applySubject(Email $email, Message $message): void
{
$message->setSubject($email->getSubject() ?? '');
$message->subject($email->getSubject() ?? '');
}
private function applyHeaders(Message $message): void
{
foreach ($this->headers as $item) {
$message->getHeaders()->addTextHeader($item[0], $item[1]);
}
if ($this->laminasMessage) {
// For bc.
foreach ($this->laminasMessage->getHeaders() as $it) {
if ($it->getFieldName() === 'Date') {
continue;
}
$message->getHeaders()->addTextHeader($it->getFieldName(), $it->getFieldValue());
}
}
}
private function prepareEnvelope(Message $message): ?Envelope
{
if (!$this->envelopeFromAddress) {
return null;
}
$recipients = [
...$message->getTo(),
...$message->getCc(),
...$message->getBcc(),
];
return new Envelope(new Address($this->envelopeFromAddress), $recipients);
}
private function applyLaminasMessageHeaders(Message $message): void
{
if (!$this->laminasMessage) {
return;
}
/** @noinspection PhpMultipleClassDeclarationsInspection */
$this->laminasMessage
->setHeaders(
Headers::fromString($message->getPreparedHeaders()->toString())
)
->setBody($message->getBody()->toString());
}
}
@@ -152,6 +152,8 @@ class SmtpParams
/**
* @return ?array<string, mixed>
* @deprecated As of v9.1.0.
* @todo Remove in v10.0.
*/
public function getConnectionOptions(): ?array
{
@@ -209,6 +211,8 @@ class SmtpParams
/**
* @param ?array<string, mixed> $connectionOptions
* @deprecated As of v9.1.
* @todo Remove in v10.0.
*/
public function withConnectionOptions(?array $connectionOptions): self
{
+2 -1
View File
@@ -253,10 +253,11 @@ class Email extends Entity
foreach ($attachmentList as $attachment) {
$id = $attachment->getId();
$partId = $id . '@espo';
$body = str_replace(
"\"?entryPoint=attachment&amp;id=$id\"",
"\"cid:$id\"",
"\"cid:$partId\"",
$body
);
}
@@ -29,11 +29,10 @@
namespace Espo\Modules\Crm\Tools\MassEmail;
use Espo\Core\Mail\Mail\Header\XQueueItemId;
use Espo\Core\Utils\Config;
use Espo\Modules\Crm\Entities\Campaign;
use Espo\Modules\Crm\Tools\MassEmail\MessagePreparator\Data;
use Laminas\Mail\Headers;
use Espo\Modules\Crm\Tools\MassEmail\MessagePreparator\Headers;
class DefaultMessageHeadersPreparator implements MessageHeadersPreparator
{
@@ -42,13 +41,8 @@ class DefaultMessageHeadersPreparator implements MessageHeadersPreparator
public function prepare(Headers $headers, Data $data): void
{
$id = $data->getId();
$header = new XQueueItemId();
$header->setId($id);
$headers->addHeader($header);
$headers->addHeaderLine('Precedence', 'bulk');
$headers->addTextHeader('X-Queue-Item-Id', $data->getId());
$headers->addTextHeader('Precedence', 'bulk');
$this->addMandatoryOptOut($headers, $data);
}
@@ -76,7 +70,7 @@ class DefaultMessageHeadersPreparator implements MessageHeadersPreparator
$url = "{$this->getSiteUrl()}/api/v1/Campaign/unsubscribe/$id";
$headers->addHeaderLine('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
$headers->addHeaderLine('List-Unsubscribe', "<$url>");
$headers->addTextHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
$headers->addTextHeader('List-Unsubscribe', "<$url>");
}
}
@@ -30,7 +30,7 @@
namespace Espo\Modules\Crm\Tools\MassEmail;
use Espo\Modules\Crm\Tools\MassEmail\MessagePreparator\Data;
use Laminas\Mail\Headers;
use Espo\Modules\Crm\Tools\MassEmail\MessagePreparator\Headers;
/**
* Applies additional headers to a mass email message.
@@ -27,14 +27,18 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Mail\Smtp;
namespace Espo\Modules\Crm\Tools\MassEmail\MessagePreparator;
use Laminas\Mail\Transport\Smtp as SmtpTransport;
use Espo\Core\Mail\Sender;
class TransportFactory
class Headers
{
public function create(): SmtpTransport
public function __construct(
private Sender $sender,
) {}
public function addTextHeader(string $name, string $value): void
{
return new SmtpTransport();
$this->sender->withAddedHeader($name, $value);
}
}
@@ -29,6 +29,7 @@
namespace Espo\Modules\Crm\Tools\MassEmail;
use Espo\Modules\Crm\Tools\MassEmail\MessagePreparator\Headers;
use Laminas\Mail\Message;
use Espo\Core\Field\DateTime;
@@ -193,13 +194,14 @@ class SendingProcessor
private function prepareQueueItemMessage(
EmailQueueItem $queueItem,
Sender $sender,
Message $message,
SenderParams $senderParams
SenderParams $senderParams,
): void {
$id = $queueItem->getId();
$this->headersPreparator->prepare($message->getHeaders(), new Data($id, $senderParams, $queueItem));
$headers = new Headers($sender);
$this->headersPreparator->prepare($headers, new Data($id, $senderParams, $queueItem));
$fromAddress = $senderParams->getFromAddress();
@@ -211,7 +213,7 @@ class SendingProcessor
$bounceAddress = explode('@', $fromAddress)[0] . '+bounce-qid-' . $id .
'@' . explode('@', $fromAddress)[1];
$sender->withEnvelopeOptions(['from' => $bounceAddress]);
$sender->withEnvelopeFromAddress($bounceAddress);
}
}
@@ -300,14 +302,11 @@ class SendingProcessor
$sender->withSmtpParams($smtpParams);
}
$message = new Message();
try {
$this->prepareQueueItemMessage($queueItem, $sender, $message, $senderParams);
$this->prepareQueueItemMessage($queueItem, $sender, $senderParams);
$sender
->withParams($senderParams)
->withMessage($message)
->withAttachments($attachmentList)
->send($email);
} catch (Exception $e) {
@@ -38,6 +38,10 @@
"plain": "PLAIN",
"login": "LOGIN",
"crammd5": "CRAM-MD5"
},
"smtpSecurity": {
"SSL": "SSL/TLS",
"TLS": "STARTTLS"
}
},
"labels": {
@@ -83,6 +83,10 @@
"plain": "PLAIN",
"login": "LOGIN",
"crammd5": "CRAM-MD5"
},
"smtpSecurity": {
"SSL": "SSL/TLS",
"TLS": "STARTTLS"
}
},
"labels": {
@@ -202,6 +202,10 @@
"Totp": "TOTP",
"Email": "Email",
"Sms": "SMS"
},
"smtpSecurity": {
"SSL": "SSL/TLS",
"TLS": "STARTTLS"
}
},
"tooltips": {
+19 -12
View File
@@ -48,6 +48,7 @@ use Espo\Core\Mail\ConfigDataProvider;
use Espo\Core\Mail\EmailSender;
use Espo\Core\Mail\Exceptions\NoSmtp;
use Espo\Core\Mail\Exceptions\SendingError;
use Espo\Core\Mail\Sender;
use Espo\Core\Mail\SenderParams;
use Espo\Core\Mail\Smtp\HandlerProcessor;
use Espo\Core\Mail\SmtpParams;
@@ -130,7 +131,7 @@ class SendService
$systemFromName = $this->config->get('outboundEmailFromName');
$systemFromAddress = $this->configDataProvider->getSystemOutboundAddress();
$emailSender = $this->emailSender->create();
$sender = $this->emailSender->create();
$userAddressList = [];
@@ -167,14 +168,14 @@ class SendService
}
if ($user && $smtpParams) {
$emailSender->withSmtpParams($smtpParams);
$sender->withSmtpParams($smtpParams);
}
if (!$smtpParams) {
[$smtpParams, $groupAccount] = $this->getGroupAccount($user, $originalFromAddress);
if ($smtpParams) {
$emailSender->withSmtpParams($smtpParams);
$sender->withSmtpParams($smtpParams);
}
}
@@ -213,19 +214,25 @@ class SendService
$message = new Message();
$this->applyReplied($entity, $message);
if (
$groupAccount instanceof GroupAccount && $groupAccount->storeSentEmails() ||
$personalAccount instanceof PersonalAccount && $personalAccount->storeSentEmails()
) {
$sender->withMessage($message);
}
$this->applyReplied($entity, $sender);
$sender->withParams($params);
try {
$emailSender
->withParams($params)
->withMessage($message)
->send($entity);
$sender->send($entity);
} catch (Exception $e) {
$entity->setStatus(Email::STATUS_DRAFT);
$this->entityManager->saveEntity($entity, [SaveOption::SILENT => true]);
$this->log->error("Email sending:" . $e->getMessage() . "; " . $e->getCode());
$this->log->error("Email sending: " . $e->getMessage(), ['exception' => $e]);
$errorData = [
'id' => $entity->getId(),
@@ -569,13 +576,13 @@ class SendService
return $this->config->get('smtpPassword');
}
private function applyReplied(Email $entity, Message $message): void
private function applyReplied(Email $entity, Sender $sender): void
{
$replied = $entity->getReplied();
if ($replied && $replied->getMessageId()) {
$message->getHeaders()->addHeaderLine('In-Reply-To', $replied->getMessageId());
$message->getHeaders()->addHeaderLine('References', $replied->getMessageId());
$sender->withAddedHeader('In-Reply-To', $replied->getMessageId());
$sender->withAddedHeader('References', $replied->getMessageId());
}
if ($replied && $replied->getGroupFolder()) {
+2 -1
View File
@@ -52,7 +52,8 @@
"ext-ctype": "*",
"lasserafn/php-initial-avatar-generator": "^4.4",
"tholu/php-cidr-match": "^0.4",
"league/oauth2-client": "^2.8"
"league/oauth2-client": "^2.8",
"symfony/mailer": "^6"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
Generated
+515 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6403890abb22db2e1629883a09a3c6c7",
"content-hash": "cca59898a92b6c9f0723465a59dd8c9f",
"packages": [
{
"name": "async-aws/core",
@@ -964,6 +964,83 @@
],
"time": "2024-05-22T20:47:39+00:00"
},
{
"name": "doctrine/lexer",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/lexer.git",
"reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
"reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"doctrine/coding-standard": "^12",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.5",
"psalm/plugin-phpunit": "^0.18.3",
"vimeo/psalm": "^5.21"
},
"type": "library",
"autoload": {
"psr-4": {
"Doctrine\\Common\\Lexer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
"homepage": "https://www.doctrine-project.org/projects/lexer.html",
"keywords": [
"annotations",
"docblock",
"lexer",
"parser",
"php"
],
"support": {
"issues": "https://github.com/doctrine/lexer/issues",
"source": "https://github.com/doctrine/lexer/tree/3.0.1"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer",
"type": "tidelift"
}
],
"time": "2024-02-05T11:56:58+00:00"
},
{
"name": "dompdf/dompdf",
"version": "v3.0.0",
@@ -1184,6 +1261,73 @@
],
"time": "2024-10-09T13:47:03+00:00"
},
{
"name": "egulias/email-validator",
"version": "4.0.4",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
"reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa",
"reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa",
"shasum": ""
},
"require": {
"doctrine/lexer": "^2.0 || ^3.0",
"php": ">=8.1",
"symfony/polyfill-intl-idn": "^1.26"
},
"require-dev": {
"phpunit/phpunit": "^10.2",
"vimeo/psalm": "^5.12"
},
"suggest": {
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Egulias\\EmailValidator\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Eduardo Gulias Davis"
}
],
"description": "A library for validating emails against several RFCs",
"homepage": "https://github.com/egulias/EmailValidator",
"keywords": [
"email",
"emailvalidation",
"emailvalidator",
"validation",
"validator"
],
"support": {
"issues": "https://github.com/egulias/EmailValidator/issues",
"source": "https://github.com/egulias/EmailValidator/tree/4.0.4"
},
"funding": [
{
"url": "https://github.com/egulias",
"type": "github"
}
],
"time": "2025-03-06T22:45:56+00:00"
},
{
"name": "evenement/evenement",
"version": "v3.0.2",
@@ -4498,6 +4642,56 @@
},
"time": "2021-11-05T16:50:12+00:00"
},
{
"name": "psr/event-dispatcher",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/event-dispatcher.git",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\EventDispatcher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Standard interfaces for event handling.",
"keywords": [
"events",
"psr",
"psr-14"
],
"support": {
"issues": "https://github.com/php-fig/event-dispatcher/issues",
"source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
},
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.3",
@@ -5936,6 +6130,162 @@
],
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v7.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1",
"reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/event-dispatcher-contracts": "^2.5|^3"
},
"conflict": {
"symfony/dependency-injection": "<6.4",
"symfony/service-contracts": "<2.5"
},
"provide": {
"psr/event-dispatcher-implementation": "1.0",
"symfony/event-dispatcher-implementation": "2.0|3.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
"symfony/config": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/error-handler": "^6.4|^7.0",
"symfony/expression-language": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/stopwatch": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\EventDispatcher\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
"reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f",
"reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/event-dispatcher": "^1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.5-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\EventDispatcher\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to dispatching event",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/http-client",
"version": "v7.2.0",
@@ -6186,6 +6536,170 @@
],
"time": "2025-01-09T15:48:56+00:00"
},
{
"name": "symfony/mailer",
"version": "v6.4.18",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
"reference": "e93a6ae2767d7f7578c2b7961d9d8e27580b2b11"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/e93a6ae2767d7f7578c2b7961d9d8e27580b2b11",
"reference": "e93a6ae2767d7f7578c2b7961d9d8e27580b2b11",
"shasum": ""
},
"require": {
"egulias/email-validator": "^2.1.10|^3|^4",
"php": ">=8.1",
"psr/event-dispatcher": "^1",
"psr/log": "^1|^2|^3",
"symfony/event-dispatcher": "^5.4|^6.0|^7.0",
"symfony/mime": "^6.2|^7.0",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"symfony/http-client-contracts": "<2.5",
"symfony/http-kernel": "<5.4",
"symfony/messenger": "<6.2",
"symfony/mime": "<6.2",
"symfony/twig-bridge": "<6.2.1"
},
"require-dev": {
"symfony/console": "^5.4|^6.0|^7.0",
"symfony/http-client": "^5.4|^6.0|^7.0",
"symfony/messenger": "^6.2|^7.0",
"symfony/twig-bridge": "^6.2|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mailer\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailer/tree/v6.4.18"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-01-24T15:27:15+00:00"
},
{
"name": "symfony/mime",
"version": "v7.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "87ca22046b78c3feaff04b337f33b38510fd686b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b",
"reference": "87ca22046b78c3feaff04b337f33b38510fd686b",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/mailer": "<6.4",
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
},
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/property-access": "^6.4|^7.0",
"symfony/property-info": "^6.4|^7.0",
"symfony/serializer": "^6.4.3|^7.0.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mime\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows manipulating MIME messages",
"homepage": "https://symfony.com",
"keywords": [
"mime",
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v7.2.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-02-19T08:51:20+00:00"
},
{
"name": "symfony/polyfill-iconv",
"version": "v1.31.0",
@@ -1,155 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* 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 <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace tests\unit\Espo\Core\Mail;
use Laminas\Mail\Transport\Smtp as SmtpTransport;
use Espo\Core\InjectableFactory;
use Espo\Entities\Email;
use Espo\Core\FileStorage\Manager;
use Espo\Core\Mail\Account\Account;
use Espo\Core\Mail\Account\SendingAccountProvider;
use Espo\Core\Mail\ConfigDataProvider;
use Espo\Core\Mail\EmailSender;
use Espo\Core\Mail\Sender;
use Espo\Core\Mail\Smtp\TransportFactory;
use Espo\Core\Mail\SmtpParams;
use Espo\Core\ORM\EntityManager;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
use PHPUnit\Framework\TestCase;
class EmailSenderTest extends TestCase
{
public function setUp(): void
{
$this->config = $this->createMock(Config::class);
$entityManager = $this->createMock(EntityManager::class);
$injectableFactory = $this->createMock(InjectableFactory::class);
$transportFactory = $this->createMock(TransportFactory::class);
$this->transport = $this->createMock(SmtpTransport::class);
$accountProvider = $this->createMock(SendingAccountProvider::class);
$log = $this->createMock(Log::class);
$emailSender = new EmailSender(
$this->config,
$accountProvider,
$injectableFactory
);
$configDataProvider = $this->createMock(ConfigDataProvider::class);
$sender = new Sender(
$this->config,
$entityManager,
$log,
$transportFactory,
$accountProvider,
$this->createMock(Manager::class),
$configDataProvider
);
$this->emailSender = $emailSender;
$injectableFactory
->expects($this->any())
->method('createWithBinding')
->willReturn($sender);
$transportFactory
->expects($this->any())
->method('create')
->willReturn($this->transport);
$account = $this->createMock(Account::class);
$account
->expects($this->once())
->method('getSmtpParams')
->willReturn(
SmtpParams::create('test-server', 85)
);
$accountProvider
->expects($this->once())
->method('getSystem')
->willReturn($account);
$configDataProvider
->expects($this->any())
->method('getSystemOutboundAddress')
->willReturn(null);
}
protected function createEmail(array $data) : Email
{
$email = $this->getMockBuilder(Email::class)->disableOriginalConstructor()->getMock();
$email
->expects($this->any())
->method('get')
->will(
$this->returnCallback(
function ($name) use ($data) {
return $data[$name] ?? null;
}
)
);
$email
->expects($this->any())
->method('getBodyPlainForSending')
->willReturn('test');
$email
->expects($this->any())
->method('isNew')
->willReturn(true);
return $email;
}
public function testSend1()
{
$email = $this->createEmail([
'name' => 'test',
'from' => 'test@tester.com',
]);
$this->transport
->expects($this->once())
->method('send');
$this->emailSender->send($email);
}
}
+1 -1
View File
@@ -473,7 +473,7 @@ class EmailTest extends \PHPUnit\Framework\TestCase
->will($this->returnValue($attachment));
$body = $this->email->getBodyForSending();
$this->assertEquals('test <img src="cid:Id01">', $body);
$this->assertEquals('test <img src="cid:Id01@espo">', $body);
}
public function testBodyPlain(): void