diff --git a/application/Espo/Core/Mail/Account/GroupAccount/Hooks/AfterFetch.php b/application/Espo/Core/Mail/Account/GroupAccount/Hooks/AfterFetch.php index 77e700c84e..e356db7d06 100644 --- a/application/Espo/Core/Mail/Account/GroupAccount/Hooks/AfterFetch.php +++ b/application/Espo/Core/Mail/Account/GroupAccount/Hooks/AfterFetch.php @@ -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]); } } diff --git a/application/Espo/Core/Mail/EmailSender.php b/application/Espo/Core/Mail/EmailSender.php index e8ac021a33..1675c7cf51 100644 --- a/application/Espo/Core/Mail/EmailSender.php +++ b/application/Espo/Core/Mail/EmailSender.php @@ -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 $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. */ diff --git a/application/Espo/Core/Mail/Mail/Header/XQueueItemId.php b/application/Espo/Core/Mail/Mail/Header/XQueueItemId.php deleted file mode 100644 index 38dc6c1855..0000000000 --- a/application/Espo/Core/Mail/Mail/Header/XQueueItemId.php +++ /dev/null @@ -1,115 +0,0 @@ -. - * - * 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; - } -} diff --git a/application/Espo/Core/Mail/Sender.php b/application/Espo/Core/Mail/Sender.php index 6a14f9096c..490c18606d 100644 --- a/application/Espo/Core/Mail/Sender.php +++ b/application/Espo/Core/Mail/Sender.php @@ -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 */ private array $params = []; /** @var array */ private array $overrideParams = []; - private ?Envelope $envelope = null; - private ?Message $message = null; + private ?string $envelopeFromAddress = null; + private ?LaminasMessage $laminasMessage = null; /** @var ?iterable */ 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 $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 $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 $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 $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 $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()); } } diff --git a/application/Espo/Core/Mail/SmtpParams.php b/application/Espo/Core/Mail/SmtpParams.php index 2640ef3cc5..b95e02ae93 100644 --- a/application/Espo/Core/Mail/SmtpParams.php +++ b/application/Espo/Core/Mail/SmtpParams.php @@ -152,6 +152,8 @@ class SmtpParams /** * @return ?array + * @deprecated As of v9.1.0. + * @todo Remove in v10.0. */ public function getConnectionOptions(): ?array { @@ -209,6 +211,8 @@ class SmtpParams /** * @param ?array $connectionOptions + * @deprecated As of v9.1. + * @todo Remove in v10.0. */ public function withConnectionOptions(?array $connectionOptions): self { diff --git a/application/Espo/Entities/Email.php b/application/Espo/Entities/Email.php index 5ef9f00032..47875aad09 100644 --- a/application/Espo/Entities/Email.php +++ b/application/Espo/Entities/Email.php @@ -253,10 +253,11 @@ class Email extends Entity foreach ($attachmentList as $attachment) { $id = $attachment->getId(); + $partId = $id . '@espo'; $body = str_replace( "\"?entryPoint=attachment&id=$id\"", - "\"cid:$id\"", + "\"cid:$partId\"", $body ); } diff --git a/application/Espo/Modules/Crm/Tools/MassEmail/DefaultMessageHeadersPreparator.php b/application/Espo/Modules/Crm/Tools/MassEmail/DefaultMessageHeadersPreparator.php index ee61682cb3..f06a1bec79 100644 --- a/application/Espo/Modules/Crm/Tools/MassEmail/DefaultMessageHeadersPreparator.php +++ b/application/Espo/Modules/Crm/Tools/MassEmail/DefaultMessageHeadersPreparator.php @@ -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>"); } } diff --git a/application/Espo/Modules/Crm/Tools/MassEmail/MessageHeadersPreparator.php b/application/Espo/Modules/Crm/Tools/MassEmail/MessageHeadersPreparator.php index b8447fbc1c..640d99b4bd 100644 --- a/application/Espo/Modules/Crm/Tools/MassEmail/MessageHeadersPreparator.php +++ b/application/Espo/Modules/Crm/Tools/MassEmail/MessageHeadersPreparator.php @@ -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. diff --git a/application/Espo/Core/Mail/Smtp/TransportFactory.php b/application/Espo/Modules/Crm/Tools/MassEmail/MessagePreparator/Headers.php similarity index 82% rename from application/Espo/Core/Mail/Smtp/TransportFactory.php rename to application/Espo/Modules/Crm/Tools/MassEmail/MessagePreparator/Headers.php index cdab1477ba..b7a3d376e0 100644 --- a/application/Espo/Core/Mail/Smtp/TransportFactory.php +++ b/application/Espo/Modules/Crm/Tools/MassEmail/MessagePreparator/Headers.php @@ -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); } } diff --git a/application/Espo/Modules/Crm/Tools/MassEmail/SendingProcessor.php b/application/Espo/Modules/Crm/Tools/MassEmail/SendingProcessor.php index e52fb84bb9..48a5170625 100644 --- a/application/Espo/Modules/Crm/Tools/MassEmail/SendingProcessor.php +++ b/application/Espo/Modules/Crm/Tools/MassEmail/SendingProcessor.php @@ -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) { diff --git a/application/Espo/Resources/i18n/en_US/EmailAccount.json b/application/Espo/Resources/i18n/en_US/EmailAccount.json index 138f017e1a..4c8e0f7060 100644 --- a/application/Espo/Resources/i18n/en_US/EmailAccount.json +++ b/application/Espo/Resources/i18n/en_US/EmailAccount.json @@ -38,6 +38,10 @@ "plain": "PLAIN", "login": "LOGIN", "crammd5": "CRAM-MD5" + }, + "smtpSecurity": { + "SSL": "SSL/TLS", + "TLS": "STARTTLS" } }, "labels": { diff --git a/application/Espo/Resources/i18n/en_US/InboundEmail.json b/application/Espo/Resources/i18n/en_US/InboundEmail.json index 8ace0a9e70..5ce58bf9d3 100644 --- a/application/Espo/Resources/i18n/en_US/InboundEmail.json +++ b/application/Espo/Resources/i18n/en_US/InboundEmail.json @@ -83,6 +83,10 @@ "plain": "PLAIN", "login": "LOGIN", "crammd5": "CRAM-MD5" + }, + "smtpSecurity": { + "SSL": "SSL/TLS", + "TLS": "STARTTLS" } }, "labels": { diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index 4201dc737d..ecb50444fb 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -202,6 +202,10 @@ "Totp": "TOTP", "Email": "Email", "Sms": "SMS" + }, + "smtpSecurity": { + "SSL": "SSL/TLS", + "TLS": "STARTTLS" } }, "tooltips": { diff --git a/application/Espo/Tools/Email/SendService.php b/application/Espo/Tools/Email/SendService.php index 8fce5c5444..1de255407c 100644 --- a/application/Espo/Tools/Email/SendService.php +++ b/application/Espo/Tools/Email/SendService.php @@ -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()) { diff --git a/composer.json b/composer.json index 7375ad7244..0bf2a33fb5 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 29a07a581b..4c3b7284ad 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/tests/unit/Espo/Core/Mail/EmailSenderTest.php b/tests/unit/Espo/Core/Mail/EmailSenderTest.php deleted file mode 100644 index c953a94194..0000000000 --- a/tests/unit/Espo/Core/Mail/EmailSenderTest.php +++ /dev/null @@ -1,155 +0,0 @@ -. - * - * 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); - } -} diff --git a/tests/unit/Espo/Entities/EmailTest.php b/tests/unit/Espo/Entities/EmailTest.php index 6d2d6b82cd..facb0151e4 100644 --- a/tests/unit/Espo/Entities/EmailTest.php +++ b/tests/unit/Espo/Entities/EmailTest.php @@ -473,7 +473,7 @@ class EmailTest extends \PHPUnit\Framework\TestCase ->will($this->returnValue($attachment)); $body = $this->email->getBodyForSending(); - $this->assertEquals('test ', $body); + $this->assertEquals('test ', $body); } public function testBodyPlain(): void