first commit

This commit is contained in:
2026-01-25 18:18:09 +08:00
commit 509312e604
8136 changed files with 2349298 additions and 0 deletions

View File

@ -0,0 +1,35 @@
Yii Framework 2 Symfony mailer extension Change Log
================================================
2.0.4 September 04, 2022
------------------------
- Enh #22: Added an exception if there is no transport configuration (Krakozaber)
2.0.3 February 10, 2022
-----------------------
- Bug #20: Remove final from Mailer and Message class (Krakozaber)
2.0.2 February 03, 2022
-----------------------
- Bug #15: Fix return value of Message::embed() and Message::embedContent() (Hyncica)
- Bug #17: Fix missing import for `\RuntimeException` in `Mailer` (samdark)
- Bug #18: Fix `Message` incompatibility with `MessageInterface` (samdark)
- Bug #18: Fix not calling `Message` constructor (samdark)
2.0.1 December 31, 2021
-----------------------
- Bug #12: Fix namespace import in Mailer.php (Krakozaber)
2.0.0 December 30, 2021
-----------------------
- Initial release.

View File

@ -0,0 +1,29 @@
Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Yii Software LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,111 @@
<p align="center">
<a href="https://github.com/yiisoft" target="_blank">
<img src="https://yiisoft.github.io/docs/images/yii_logo.svg" height="100px">
</a>
<h1 align="center">Yii Mailer Library - Symfony Mailer Extension</h1>
<br>
</p>
This extension provides a [Symfony Mailer](https://symfony.com/doc/5.4/mailer.html) mail solution for [Yii framework 2.0](http://www.yiiframework.com).
For license information check the [LICENSE](LICENSE.md)-file.
[![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2-symfonymailer/v/stable.png)](https://packagist.org/packages/yiisoft/yii2-symfonymailer)
[![Total Downloads](https://poser.pugx.org/yiisoft/yii2-symfonymailer/downloads.png)](https://packagist.org/packages/yiisoft/yii2-symfonymailer)
[![Build Status](https://github.com/yiisoft/yii2-symfonymailer/workflows/build/badge.svg)](https://github.com/yiisoft/yii2-symfonymailer/actions)
Installation
------------
The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
Either run
```
php composer.phar require --prefer-dist yiisoft/yii2-symfonymailer
```
or add
```json
"yiisoft/yii2-symfonymailer": "~2.0.0"
```
to the require section of your composer.json.
Usage
-----
To use this extension, simply add the following code in your application configuration:
```php
return [
//....
'components' => [
'mailer' => [
'class' => \yii\symfonymailer\Mailer::class,
'transport' => [
'scheme' => 'smtps',
'host' => '',
'username' => '',
'password' => '',
'port' => 465,
'dsn' => 'native://default',
],
'viewPath' => '@common/mail',
// send all mails to a file by default. You have to set
// 'useFileTransport' to false and configure transport
// for the mailer to send real emails.
'useFileTransport' => false,
],
],
];
```
or
```php
return [
//....
'components' => [
'mailer' => [
'class' => \yii\symfonymailer\Mailer::class,
'transport' => [
'dsn' => 'smtp://user:pass@smtp.example.com:25',
],
],
],
];
```
You can then send an email as follows:
```php
Yii::$app->mailer->compose('contact/html')
->setFrom('from@domain.com')
->setTo($form->email)
->setSubject($form->subject)
->send();
```
Migrating from yiisoft/yii2-swiftmailer
---------------------------------------
Follow these steps to migrate from the deprecated [yiisoft/yii2-swiftmailer](https://github.com/yiisoft/yii2-swiftmailer) to this extension:
1. Swiftmailer default transport was the `SendmailTransport`, while this extension will default to a `NullTransport` (sends no mail). You can use the swiftmailer default by defining in application config:
```php
'mailer' => [
'class' => yii\symfonymailer\Mailer::class,
'transport' => [
'dsn' => 'sendmail://default',
],
],
```
2. Codeceptions TestMailer specifies `swiftmailer\Message` as a default class in https://github.com/Codeception/module-yii2/blob/master/src/Codeception/Lib/Connector/Yii2/TestMailer.php#L8. This can also be changed by defining in test config:
```php
'mailer' => [
'class' => yii\symfonymailer\Mailer::class,
'messageClass' => \yii\symfonymailer\Message::class,
],
```

View File

@ -0,0 +1,56 @@
{
"name": "yiisoft/yii2-symfonymailer",
"description": "The SymfonyMailer integration for the Yii framework",
"keywords": [
"yii2",
"symfony",
"symfonymailer",
"mail",
"email",
"mailer"
],
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
"issues": "https://github.com/yiisoft/yii2-symfonymailer/issues",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
"source": "https://github.com/yiisoft/yii2-symfonymailer"
},
"authors": [
{
"name": "Kirill Petrov",
"email": "archibeardrinker@gmail.com"
}
],
"require": {
"php": ">=7.4.0",
"yiisoft/yii2": ">=2.0.4",
"symfony/mailer": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "9.5.10"
},
"repositories": [
{
"type": "composer",
"url": "https://asset-packagist.org"
}
],
"autoload": {
"psr-4": {
"yii\\symfonymailer\\": "src"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"config": {
"allow-plugins": {
"yiisoft/yii2-composer": true
}
}
}

View File

@ -0,0 +1,163 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\symfonymailer;
use Yii;
use Psr\Log\LoggerInterface;
final class Logger implements LoggerInterface
{
/**
* System is unusable.
*
* @param string|\Stringable $message
* @param mixed[] $context
*
* @return void
*/
public function emergency($message, array $context = []): void
{
Yii::getLogger()->log($message, \yii\log\Logger::LEVEL_ERROR, __METHOD__);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string|\Stringable $message
* @param mixed[] $context
*
* @return void
*/
public function alert($message, array $context = []): void
{
Yii::getLogger()->log($message, \yii\log\Logger::LEVEL_ERROR, __METHOD__);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string|\Stringable $message
* @param mixed[] $context
*
* @return void
*/
public function critical($message, array $context = []): void
{
Yii::getLogger()->log($message, \yii\log\Logger::LEVEL_ERROR, __METHOD__);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string|\Stringable $message
* @param mixed[] $context
*
* @return void
*/
public function error($message, array $context = []): void
{
Yii::getLogger()->log($message, \yii\log\Logger::LEVEL_ERROR, __METHOD__);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string|\Stringable $message
* @param mixed[] $context
*
* @return void
*/
public function warning($message, array $context = []): void
{
Yii::getLogger()->log($message, \yii\log\Logger::LEVEL_WARNING, __METHOD__);
}
/**
* Normal but significant events.
*
* @param string|\Stringable $message
* @param mixed[] $context
*
* @return void
*/
public function notice($message, array $context = []): void
{
Yii::getLogger()->log($message, \yii\log\Logger::LEVEL_WARNING, __METHOD__);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string|\Stringable $message
* @param mixed[] $context
*
* @return void
*/
public function info($message, array $context = []): void
{
Yii::getLogger()->log($message, \yii\log\Logger::LEVEL_INFO, __METHOD__);
}
/**
* Detailed debug information.
*
* @param string|\Stringable $message
* @param mixed[] $context
*
* @return void
*/
public function debug($message, array $context = []): void
{
Yii::getLogger()->log($message, \yii\log\Logger::LEVEL_INFO, __METHOD__);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string|\Stringable $message
* @param mixed[] $context
*
* @return void
*
* @throws \Psr\Log\InvalidArgumentException
*/
public function log($level, $message, array $context = []): void
{
switch ($level) {
case 'error':
case 'critical':
case 'alert':
case 'emergency':
$level = \yii\log\Logger::LEVEL_ERROR;
break;
case 'notice':
case 'warning':
$level = \yii\log\Logger::LEVEL_WARNING;
break;
case 'debug':
case 'info':
$level = \yii\log\Logger::LEVEL_INFO;
break;
default:
$level = \yii\log\Logger::LEVEL_INFO;
}
Yii::getLogger()->log($message, $level, __METHOD__);
}
}

View File

@ -0,0 +1,217 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\symfonymailer;
use RuntimeException;
use Symfony\Component\Mailer\Mailer as SymfonyMailer;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Mime\Crypto\DkimSigner;
use Symfony\Component\Mime\Crypto\SMimeEncrypter;
use Symfony\Component\Mime\Crypto\SMimeSigner;
use Yii;
use yii\base\InvalidConfigException;
use yii\mail\BaseMailer;
class Mailer extends BaseMailer
{
/**
* @var string message default class name.
*/
public $messageClass = Message::class;
private ?SymfonyMailer $symfonyMailer = null;
private ?SMimeEncrypter $encryptor = null;
/**
* @var DkimSigner|SMimeSigner|null
*/
private $signer = null;
private array $dkimSignerOptions = [];
/**
* @var TransportInterface|array Symfony transport instance or its array configuration.
*/
private $_transport = [];
/**
* @var bool whether to enable writing of the Mailer internal logs using Yii log mechanism.
* If enabled [[Logger]] plugin will be attached to the [[transport]] for this purpose.
* @see Logger
*/
public bool $enableMailerLogging = false;
/**
* Creates Symfony mailer instance.
* @return SymfonyMailer mailer instance.
*/
private function createSymfonyMailer(): SymfonyMailer
{
return new SymfonyMailer($this->getTransport());
}
/**
* @return SymfonyMailer Swift mailer instance
*/
public function getSymfonyMailer(): SymfonyMailer
{
if (!is_object($this->symfonyMailer)) {
$this->symfonyMailer = $this->createSymfonyMailer();
}
return $this->symfonyMailer;
}
/**
* @param array|TransportInterface $transport
* @throws InvalidConfigException on invalid argument.
*/
public function setTransport($transport): void
{
if (!is_array($transport) && !$transport instanceof TransportInterface) {
throw new InvalidConfigException('"' . get_class($this) . '::transport" should be either object or array, "' . gettype($transport) . '" given.');
}
if ($transport instanceof TransportInterface) {
$this->_transport = $transport;
} elseif (is_array($transport)) {
$this->_transport = $this->createTransport($transport);
}
$this->symfonyMailer = null;
}
/**
* @return TransportInterface
*/
public function getTransport(): TransportInterface
{
if (!is_object($this->_transport)) {
$this->_transport = $this->createTransport($this->_transport);
}
return $this->_transport;
}
private function createTransport(array $config = []): TransportInterface
{
if (array_key_exists('enableMailerLogging', $config)) {
$this->enableMailerLogging = $config['enableMailerLogging'];
unset($config['enableMailerLogging']);
}
$logger = null;
if ($this->enableMailerLogging) {
$logger = new Logger();
}
$defaultFactories = Transport::getDefaultFactories(null, null, $logger);
$transportObj = new Transport($defaultFactories);
if (array_key_exists('dsn', $config)) {
$transport = $transportObj->fromString($config['dsn']);
} elseif(array_key_exists('scheme', $config) && array_key_exists('host', $config)) {
$dsn = new Dsn(
$config['scheme'],
$config['host'],
$config['username'] ?? '',
$config['password'] ?? '',
$config['port'] ?? '',
$config['options'] ?? [],
);
$transport = $transportObj->fromDsnObject($dsn);
} else {
throw new InvalidConfigException('Transport configuration array must contain either "dsn", or "scheme" and "host" keys.');
}
return $transport;
}
/**
* Returns a new instance with the specified encryptor.
*
* @param SMimeEncrypter $encryptor The encryptor instance.
*
* @see https://symfony.com/doc/current/mailer.html#encrypting-messages
*
* @return self
*/
public function withEncryptor(SMimeEncrypter $encryptor): self
{
$new = clone $this;
$new->encryptor = $encryptor;
return $new;
}
/**
* Returns a new instance with the specified signer.
*
* @param DkimSigner|object|SMimeSigner $signer The signer instance.
* @param array $options The options for DKIM signer {@see DkimSigner}.
*
* @throws RuntimeException If the signer is not an instance of {@see DkimSigner} or {@see SMimeSigner}.
*
* @see https://symfony.com/doc/current/mailer.html#signing-messages
*
* @return self
*/
public function withSigner(object $signer, array $options = []): self
{
$new = clone $this;
if ($signer instanceof DkimSigner) {
$new->signer = $signer;
$new->dkimSignerOptions = $options;
return $new;
}
if ($signer instanceof SMimeSigner) {
$new->signer = $signer;
return $new;
}
throw new RuntimeException(sprintf(
'The signer must be an instance of "%s" or "%s". The "%s" instance is received.',
DkimSigner::class,
SMimeSigner::class,
get_class($signer),
));
}
/**
* {@inheritDoc}
*
* @throws TransportExceptionInterface If sending failed.
*/
protected function sendMessage($message): bool
{
if (!($message instanceof Message)) {
throw new RuntimeException(sprintf(
'The message must be an instance of "%s". The "%s" instance is received.',
Message::class,
get_class($message),
));
}
$message = $message->getSymfonyEmail();
if ($this->encryptor !== null) {
$message = $this->encryptor->encrypt($message);
}
if ($this->signer !== null) {
$message = $this->signer instanceof DkimSigner
? $this->signer->sign($message, $this->dkimSignerOptions)
: $this->signer->sign($message)
;
}
try {
$this->getSymfonyMailer()->send($message);
} catch (\Exception $exception) {
Yii::getLogger()->log($exception->getMessage(), \yii\log\Logger::LEVEL_ERROR, __METHOD__);
return false;
}
return true;
}
}

View File

@ -0,0 +1,373 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yii\symfonymailer;
use DateTimeImmutable;
use DateTimeInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Header\HeaderInterface;
use yii\mail\BaseMessage;
class Message extends BaseMessage
{
private Email $email;
private string $charset = 'utf-8';
public function __construct($config = [])
{
$this->email = new Email();
parent::__construct($config);
}
public function __clone()
{
$this->email = clone $this->email;
}
public function getCharset(): string
{
return $this->charset;
}
public function setCharset($charset): self
{
$this->charset = $charset;
return $this;
}
public function getFrom()
{
return $this->convertAddressesToStrings($this->email->getFrom());
}
public function setFrom($from): self
{
$this->email->from(...$this->convertStringsToAddresses($from));
return $this;
}
public function getTo()
{
return $this->convertAddressesToStrings($this->email->getTo());
}
public function setTo($to): self
{
$this->email->to(...$this->convertStringsToAddresses($to));
return $this;
}
public function getReplyTo()
{
return $this->convertAddressesToStrings($this->email->getReplyTo());
}
public function setReplyTo($replyTo): self
{
$this->email->replyTo(...$this->convertStringsToAddresses($replyTo));
return $this;
}
public function getCc()
{
return $this->convertAddressesToStrings($this->email->getCc());
}
public function setCc($cc): self
{
$this->email->cc(...$this->convertStringsToAddresses($cc));
return $this;
}
public function getBcc()
{
return $this->convertAddressesToStrings($this->email->getBcc());
}
public function setBcc($bcc): self
{
$this->email->bcc(...$this->convertStringsToAddresses($bcc));
return $this;
}
public function getSubject(): string
{
return (string) $this->email->getSubject();
}
public function setSubject($subject): self
{
$this->email->subject($subject);
return $this;
}
public function getDate(): ?DateTimeImmutable
{
return $this->email->getDate();
}
public function setDate(DateTimeInterface $date): self
{
$this->email->date($date);
return $this;
}
public function getPriority(): int
{
return $this->email->getPriority();
}
public function setPriority(int $priority): self
{
$this->email->priority($priority);
return $this;
}
public function getReturnPath(): string
{
$returnPath = $this->email->getReturnPath();
return $returnPath === null ? '' : $returnPath->getAddress();
}
public function setReturnPath(string $address): self
{
$this->email->returnPath($address);
return $this;
}
public function getSender(): string
{
$sender = $this->email->getSender();
return $sender === null ? '' : $sender->getAddress();
}
public function setSender(string $address): self
{
$this->email->sender($address);
return $this;
}
public function getTextBody(): string
{
return (string) $this->email->getTextBody();
}
public function setTextBody($text): self
{
$this->email->text($text, $this->charset);
return $this;
}
public function getHtmlBody(): string
{
return (string) $this->email->getHtmlBody();
}
public function setHtmlBody($html): self
{
$this->email->html($html, $this->charset);
return $this;
}
/**
* @inheritdoc
*/
public function attach($fileName, array $options = [])
{
$file = [];
if (!empty($options['fileName'])) {
$file['name'] = $options['fileName'];
} else {
$file['name'] = $fileName;
}
if (!empty($options['contentType'])) {
$file['contentType'] = $options['contentType'];
} else {
$file['contentType'] = mime_content_type($fileName);
}
$this->email->attachFromPath($fileName, $file['name'], $file['contentType']);
return $this;
}
/**
* @inheritdoc
*/
public function attachContent($content, array $options = [])
{
$file = [];
if (!empty($options['fileName'])) {
$file['name'] = $options['fileName'];
} else {
$file['name'] = null;
}
if (!empty($options['contentType'])) {
$file['contentType'] = $options['contentType'];
} else {
$file['contentType'] = null;
}
$this->email->attach($content, $file['name'], $file['contentType']);
return $this;
}
/**
* @inheritdoc
*/
public function embed($fileName, array $options = [])
{
$file = [];
if (!empty($options['fileName'])) {
$file['name'] = $options['fileName'];
} else {
$file['name'] = $fileName;
}
if (!empty($options['contentType'])) {
$file['contentType'] = $options['contentType'];
} else {
$file['contentType'] = mime_content_type($fileName);
}
$this->email->embedFromPath($fileName, $file['name'], $file['contentType']);
return 'cid:' . $file['name'];
}
/**
* @inheritdoc
*/
public function embedContent($content, array $options = [])
{
$file = [];
if (!empty($options['fileName'])) {
$file['name'] = $options['fileName'];
} else {
$file['name'] = null;
}
if (!empty($options['contentType'])) {
$file['contentType'] = $options['contentType'];
} else {
$file['contentType'] = null;
}
$this->email->embed($content, $file['name'], $file['contentType']);
return 'cid:' . $file['name'];
}
public function getHeader($name): array
{
$headers = $this->email->getHeaders();
if (!$headers->has($name)) {
return [];
}
$values = [];
/** @var HeaderInterface $header */
foreach ($headers->all($name) as $header) {
$values[] = $header->getBodyAsString();
}
return $values;
}
public function addHeader($name, $value): self
{
$this->email->getHeaders()->addTextHeader($name, $value);
return $this;
}
public function setHeader($name, $value): self
{
$headers = $this->email->getHeaders();
if ($headers->has($name)) {
$headers->remove($name);
}
foreach ((array) $value as $v) {
$headers->addTextHeader($name, $v);
}
return $this;
}
public function setHeaders($headers): self
{
foreach ($headers as $name => $value) {
$this->setHeader($name, $value);
}
return $this;
}
public function toString(): string
{
return $this->email->toString();
}
/**
* Returns a Symfony email instance.
*
* @return Email Symfony email instance.
*/
public function getSymfonyEmail(): Email
{
return $this->email;
}
/**
* Converts address instances to their string representations.
*
* @param Address[] $addresses
*
* @return array<string, string>|string
*/
private function convertAddressesToStrings(array $addresses)
{
$strings = [];
foreach ($addresses as $address) {
$strings[$address->getAddress()] = $address->getName();
}
return empty($strings) ? '' : $strings;
}
/**
* Converts string representations of address to their instances.
*
* @param array<int|string, string>|string $strings
*
* @return Address[]
*/
private function convertStringsToAddresses($strings): array
{
if (is_string($strings)) {
return [new Address($strings)];
}
$addresses = [];
foreach ($strings as $address => $name) {
if (!is_string($address)) {
// email address without name
$addresses[] = new Address($name);
continue;
}
$addresses[] = new Address($address, $name);
}
return $addresses;
}
}