diff --git a/src/controllers/SystemSettingsController.php b/src/controllers/SystemSettingsController.php index b8eb9f72f5b..f705586c1cf 100644 --- a/src/controllers/SystemSettingsController.php +++ b/src/controllers/SystemSettingsController.php @@ -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; } diff --git a/src/helpers/App.php b/src/helpers/App.php index 0ca7b5033e4..38c8dd45ea3 100644 --- a/src/helpers/App.php +++ b/src/helpers/App.php @@ -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(), ]; } diff --git a/src/mail/Mailer.php b/src/mail/Mailer.php index d4421ac0b96..366066d2e1a 100644 --- a/src/mail/Mailer.php +++ b/src/mail/Mailer.php @@ -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; @@ -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. * @@ -93,12 +100,14 @@ 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(); @@ -106,27 +115,44 @@ public function send($message): bool } } - 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(); @@ -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); + } } } diff --git a/src/models/MailSettings.php b/src/models/MailSettings.php index 1c22e6638d2..c47fb4acb34 100644 --- a/src/models/MailSettings.php +++ b/src/models/MailSettings.php @@ -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. @@ -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|null The transport type that should be used */ @@ -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 */ @@ -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'), ]; } @@ -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)); + } } diff --git a/src/templates/settings/email/_index.twig b/src/templates/settings/email/_index.twig index f5f9781c18c..7e5e1e3ffbc 100644 --- a/src/templates/settings/email/_index.twig +++ b/src/templates/settings/email/_index.twig @@ -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 %} +
{{ forms.selectField({ diff --git a/src/translations/en/app.php b/src/translations/en/app.php index d867da67eda..19946414d60 100644 --- a/src/translations/en/app.php +++ b/src/translations/en/app.php @@ -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', @@ -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',