Skip to content

Commit

Permalink
Merge pull request #16187 from craftcms/feature/cms-437-per-site-emai…
Browse files Browse the repository at this point in the history
…l-settings

Site-specific email setting overrides
  • Loading branch information
brandonkelly authored Nov 22, 2024
2 parents 92e7a5e + 2937e24 commit 9154bf9
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 46 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
### Administration
- Added the “Show the ‘URL Suffix’ field” setting to Link fields. ([#15813](https://github.com/craftcms/cms/discussions/15813))
- Added the “Affiliated Site” native user field. ([#16174](https://github.com/craftcms/cms/pull/16174))
- Added support for setting site-specific email setting overrides. ([#16187](https://github.com/craftcms/cms/pull/16187))

### Development
- Added support for fallback element partial templates, e.g. `_partials/entry.twig` as opposed to `_partials/entry/typeHandle.twig`. ([#16125](https://github.com/craftcms/cms/pull/16125))
Expand All @@ -28,6 +29,8 @@
- Added `craft\fields\data\LinkData::$urlSuffix`.
- Added `craft\fields\data\LinkData::getUrl()`.
- Added `craft\mail\Mailer::$siteId`.
- Added `craft\mail\Mailer::$siteOverrides`.
- Added `craft\models\MailSettings::setSiteOverrides()`.
- `craft\models\Site` now implements `craft\base\Chippable`.

### System
Expand Down
1 change: 1 addition & 0 deletions src/controllers/SystemSettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ private function _createMailSettingsFromPost(): MailSettings
$settings->template = $this->request->getBodyParam('template');
$settings->transportType = $this->request->getBodyParam('transportType');
$settings->transportSettings = Component::cleanseConfig($this->request->getBodyParam(sprintf('transportTypes.%s', Html::id($settings->transportType))) ?? []);
$settings->setSiteOverrides($this->request->getBodyParam('siteOverrides') ?? []);

return $settings;
}
Expand Down
1 change: 1 addition & 0 deletions src/helpers/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,7 @@ public static function mailerConfig(?MailSettings $settings = null): array
],
'replyTo' => App::parseEnv($settings->replyToEmail),
'template' => App::parseEnv($settings->template),
'siteOverrides' => $settings->getSiteOverrides(),
'transport' => $adapter->defineTransport(),
];
}
Expand Down
120 changes: 74 additions & 46 deletions src/mail/Mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use craft\enums\CmsEdition;
use craft\helpers\App;
use craft\helpers\Template;
use craft\models\Site;
use craft\web\View;
use Throwable;
use yii\base\InvalidConfigException;
Expand Down Expand Up @@ -49,6 +50,12 @@ class Mailer extends \yii\symfonymailer\Mailer
*/
public User|string|array|null $replyTo = null;

/**
* @var array Site overrides
* @since 5.6.0
*/
public array $siteOverrides = [];

/**
* Composes a new email based on a given key.
*
Expand Down Expand Up @@ -93,40 +100,59 @@ public function send($message): bool
]));

$generalConfig = Craft::$app->getConfig()->getGeneral();

if ($message instanceof Message && $message->key !== null) {
$sitesService = Craft::$app->getSites();
$currentSite = $messageSite = null;

if (isset($message->siteId)) {
$sitesService = Craft::$app->getSites();
$currentSite = $messageSite = null;
$language = Craft::$app->language;
$generateTransformsBeforePageLoad = $generalConfig->generateTransformsBeforePageLoad;
$originalSettings = [];

try {
if ($message instanceof Message && isset($message->siteId)) {
$messageSite = $sitesService->getSiteById($message->siteId);
if ($messageSite) {
$currentSite = $sitesService->getCurrentSite();
$sitesService->setCurrentSite($messageSite);
}
}

if ($message->language === null) {
// If a site was specified, go with its language
if ($messageSite) {
$message->language = $messageSite->language;
} else {
// Default to the current language
$message->language = Craft::$app->getRequest()->getIsSiteRequest()
? Craft::$app->language
: Craft::$app->getSites()->getPrimarySite()->language;
}
$overrides = $this->siteOverrides[$sitesService->getCurrentSite()->uid] ?? [];
if (isset($overrides['fromEmail']) || isset($overrides['fromName'])) {
$originalSettings['from'] = $this->from;
$fromEmail = $overrides['fromEmail'] ?? array_key_first($this->from);
$fromName = $overrides['fromName'] ?? reset($this->from);
/** @phpstan-ignore-next-line */
$this->from = [
App::parseEnv($fromEmail) => App::parseEnv($fromName),
];
}
if (isset($overrides['replyToEmail'])) {
$originalSettings['replyTo'] = $this->replyTo;
$this->replyTo = App::parseEnv($overrides['replyToEmail']);
}
if (isset($overrides['template'])) {
$originalSettings['template'] = $this->template;
$this->template = App::parseEnv($overrides['template']);
}

if ($message instanceof Message && $message->key !== null) {
if ($message->language === null) {
// If a site was specified, go with its language
if ($messageSite) {
$message->language = $messageSite->language;
} else {
// Default to the current language
$message->language = Craft::$app->getRequest()->getIsSiteRequest()
? Craft::$app->language
: Craft::$app->getSites()->getPrimarySite()->language;
}
}

// Use the message language
$language = Craft::$app->language;
Craft::$app->language = $message->language;
// Use the message language
Craft::$app->language = $message->language;

// Temporarily disable lazy transform generation
$generateTransformsBeforePageLoad = $generalConfig->generateTransformsBeforePageLoad;
$generalConfig->generateTransformsBeforePageLoad = true;
// Temporarily disable lazy transform generation
$generalConfig->generateTransformsBeforePageLoad = true;

try {
$systemMessage = Craft::$app->getSystemMessages()->getMessage($message->key, $message->language);

$settings = App::mailSettings();
Expand Down Expand Up @@ -169,34 +195,36 @@ public function send($message): bool
Craft::warning('Error rendering email template: ' . $e->getMessage(), __METHOD__);
Craft::$app->getErrorHandler()->logException($e);
}
} finally {
// Set things back to normal
Craft::$app->language = $language;
$generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad;
}

if ($currentSite) {
$sitesService->setCurrentSite($currentSite);
}
// Set the default sender if there isn't one already
if (!$message->getFrom()) {
$message->setFrom($this->from);
}
}

// Set the default sender if there isn't one already
if (!$message->getFrom()) {
$message->setFrom($this->from);
}
if ($this->replyTo && !$message->getReplyTo()) {
$message->setReplyTo($this->replyTo);
}

if ($this->replyTo && !$message->getReplyTo()) {
$message->setReplyTo($this->replyTo);
}
// Apply the testToEmailAddress config setting
$testToEmailAddress = $generalConfig->getTestToEmailAddress();
if (!empty($testToEmailAddress)) {
$message->setTo($testToEmailAddress);
$message->setCc([]);
$message->setBcc([]);
}

// Apply the testToEmailAddress config setting
$testToEmailAddress = $generalConfig->getTestToEmailAddress();
if (!empty($testToEmailAddress)) {
$message->setTo($testToEmailAddress);
$message->setCc([]);
$message->setBcc([]);
}
return parent::send($message);
} finally {
// Set things back to normal
Craft::$app->language = $language;
$generalConfig->generateTransformsBeforePageLoad = $generateTransformsBeforePageLoad;

return parent::send($message);
if ($currentSite) {
$sitesService->setCurrentSite($currentSite);
}

Craft::configure($this, $originalSettings);
}
}
}
77 changes: 77 additions & 0 deletions src/models/MailSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use craft\mail\transportadapters\Sendmail;
use craft\mail\transportadapters\TransportAdapterInterface;
use craft\validators\TemplateValidator;
use Illuminate\Support\Collection;
use yii\validators\EmailValidator;

/**
* MailSettings Model class.
Expand Down Expand Up @@ -43,6 +45,12 @@ class MailSettings extends Model
*/
public ?string $template = null;

/**
* @var array Site-specific overrides
* @since 5.6.0
*/
private array $siteOverrides = [];

/**
* @var class-string<TransportAdapterInterface>|null The transport type that should be used
*/
Expand Down Expand Up @@ -71,6 +79,27 @@ protected function defineBehaviors(): array
];
}

/**
* @inheritdoc
*/
public function attributes()
{
return [
...parent::attributes(),
'siteOverrides',
];
}

/**
* @inheritdoc
*/
public function fields(): array
{
$fields = parent::fields();
$fields['siteOverrides'] = fn() => $this->siteOverrides;
return $fields;
}

/**
* @inheritdoc
*/
Expand All @@ -82,6 +111,7 @@ public function attributeLabels(): array
'fromName' => Craft::t('app', 'Sender Name'),
'template' => Craft::t('app', 'HTML Email Template'),
'transportType' => Craft::t('app', 'Transport Type'),
'siteOverrides' => Craft::t('app', 'Site Overrides'),
];
}

Expand All @@ -95,6 +125,53 @@ protected function defineRules(): array
$rules[] = [['fromEmail', 'replyToEmail'], 'email'];
$rules[] = [['template'], TemplateValidator::class];

$rules[] = [['siteOverrides'], function() {
$sitesService = Craft::$app->getSites();
foreach ($this->siteOverrides as $siteUid => $overrides) {
foreach (['fromEmail', 'replyToEmail'] as $key) {
if (isset($overrides[$key])) {
$validator = new EmailValidator([
'message' => Craft::t('yii', '{attribute} is not a valid email address.', [
'attribute' => sprintf(
'%s - %s',
$sitesService->getSiteByUid($siteUid)->getUiLabel(),
$this->attributeLabels()[$key],
),
]),
]);
if (!$validator->validate($overrides[$key], $error)) {
$this->addError('siteOverrides', $error);
}
}
}
}
}];

return $rules;
}

/**
* Returns the site overrides.
*
* @return array
* @since 5.6.0
*/
public function getSiteOverrides(): array
{
return Collection::make(Craft::$app->getSites()->getAllSites())
->keyBy(fn(Site $site) => $site->uid)
->map(fn(Site $site) => $this->siteOverrides[$site->uid] ?? [])
->all();
}

/**
* Sets the site overrides.
*
* @param array $siteOverrides
* @since 5.6.0
*/
public function setSiteOverrides(array $siteOverrides): void
{
$this->siteOverrides = array_filter(array_map(fn(array $overrides) => array_filter($overrides), $siteOverrides));
}
}
46 changes: 46 additions & 0 deletions src/templates/settings/email/_index.twig
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,52 @@
}) }}
{% endif %}

{% if craft.app.isMultiSite %}
{{ forms.editableTableField({
label: 'Site Overrides'|t('app'),
instructions: 'Blank values will default to the settings above.'|t('app'),
id: 'site-overrides',
name: 'siteOverrides',
cols: {
heading: {
type: 'heading',
heading: 'Site'|t('app'),
suggestEnvVars: true,
thin: true,
},
fromEmail: {
type: 'autosuggest',
heading: 'System Email Address'|t('app'),
suggestEnvVars: true,
},
replyToEmail: {
type: 'autosuggest',
heading: 'Reply-To Address'|t('app'),
suggestEnvVars: true,
},
fromName: {
type: 'autosuggest',
heading: 'Sender Name'|t('app'),
suggestEnvVars: true,
},
template: CraftEdition >= CraftPro ? {
type: 'template',
heading: 'HTML Email Template'|t('app'),
suggestEnvVars: true,
code: true,
}
}|filter,
rows: settings.siteOverrides|map((overrides, siteUid) => overrides|merge({
heading: craft.app.sites.getSiteByUid(siteUid).getUiLabel(),
})),
fullWidth: true,
allowAdd: false,
allowDelete: false,
allowReorder: false,
errors: (freshSettings ? null : settings.getErrors('siteOverrides')|unique),
}) }}
{% endif %}

<hr>

{{ forms.selectField({
Expand Down
2 changes: 2 additions & 0 deletions src/translations/en/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
'Base URL' => 'Base URL',
'Before other {type}' => 'Before other {type}',
'Before…' => 'Before…',
'Blank values will default to the settings above.' => 'Blank values will default to the settings above.',
'Blue' => 'Blue',
'Body' => 'Body',
'Bottom-Center' => 'Bottom-Center',
Expand Down Expand Up @@ -1499,6 +1500,7 @@
'Singles' => 'Singles',
'Site Group' => 'Site Group',
'Site Icon' => 'Site Icon',
'Site Overrides' => 'Site Overrides',
'Site Settings' => 'Site Settings',
'Site saved.' => 'Site saved.',
'Site' => 'Site',
Expand Down

0 comments on commit 9154bf9

Please sign in to comment.