Initial commit: Grav CMS setup with HTML reference material
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,613 @@
|
||||
<?php
|
||||
namespace Grav\Plugin\Email;
|
||||
|
||||
use Grav\Common\Config\Config;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Common\Language\Language;
|
||||
use Grav\Common\Markdown\Parsedown;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Framework\Form\Interfaces\FormInterface;
|
||||
use \Monolog\Logger;
|
||||
use \Monolog\Handler\StreamHandler;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\Exception\HttpTransportException;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\Header\MetadataHeader;
|
||||
use Symfony\Component\Mailer\Header\TagHeader;
|
||||
use Symfony\Component\Mailer\Mailer;
|
||||
use Symfony\Component\Mailer\Transport;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
|
||||
class Email
|
||||
{
|
||||
/** @var Mailer */
|
||||
protected $mailer;
|
||||
|
||||
/** @var TransportInterface */
|
||||
protected $transport;
|
||||
|
||||
protected $log;
|
||||
|
||||
protected $message;
|
||||
protected $debug;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->initMailer();
|
||||
$this->initLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if emails have been enabled in the system.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function enabled(): bool
|
||||
{
|
||||
return Grav::instance()['config']->get('plugins.email.mailer.engine') !== 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if debugging on emails has been enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function debug(): bool
|
||||
{
|
||||
return Grav::instance()['config']->get('plugins.email.debug') == 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an email message.
|
||||
*
|
||||
* @param string|null $subject
|
||||
* @param string|null $body
|
||||
* @param string|null $contentType
|
||||
* @param string|null $charset @deprecated
|
||||
* @return Message
|
||||
*/
|
||||
public function message(?string $subject = null, ?string $body = null, ?string $contentType = null, ?string $charset = null): Message
|
||||
{
|
||||
$message = new Message();
|
||||
$message->subject($subject);
|
||||
if ($contentType === 'text/html') {
|
||||
$message->html($body);
|
||||
} else {
|
||||
$message->text($body);
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email.
|
||||
*
|
||||
* @param Message $message
|
||||
* @param Envelope|null $envelope
|
||||
* @return int
|
||||
*/
|
||||
public function send(Message $message, ?Envelope $envelope = null): int
|
||||
{
|
||||
try {
|
||||
$sent_msg = $this->transport->send($message->getEmail(), $envelope);
|
||||
$status = 1;
|
||||
$this->message = '✅';
|
||||
$this->debug = $sent_msg->getDebug();
|
||||
} catch (TransportExceptionInterface $e) {
|
||||
$status = 0;
|
||||
$this->message = '🛑 ' . $e->getMessage();
|
||||
$this->debug = $e->getDebug();
|
||||
|
||||
// Capture HTTP transport errors with the raw response body for easier debugging (e.g., MailerSend 4xx/5xx).
|
||||
if ($e instanceof HttpTransportException) {
|
||||
try {
|
||||
$response = $e->getResponse();
|
||||
$statusCode = $response->getStatusCode();
|
||||
$body = $response->getContent(false);
|
||||
|
||||
if (!empty($body)) {
|
||||
$decoded = json_decode($body, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$body = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
$this->debug = trim((string)$this->debug) . "\n-- HTTP response body (status {$statusCode}) --\n" . $body;
|
||||
|
||||
// If the exception message was empty, include a short summary in the user-facing message.
|
||||
if (trim($e->getMessage()) === '') {
|
||||
$this->message = sprintf('🛑 HTTP %d error while sending email. See debug for response body.', $statusCode);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $httpError) {
|
||||
$this->debug = trim((string)$this->debug) . "\n-- Failed to read HTTP error response --\n" . $httpError->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->debug()) {
|
||||
$log_msg = "Email sent to %s at %s -> %s\n%s";
|
||||
$to = $this->jsonifyRecipients($message->getEmail()->getTo());
|
||||
$message = sprintf($log_msg, $to, date('Y-m-d H:i:s'), $this->message, $this->debug);
|
||||
$this->log->info($message);
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build e-mail message.
|
||||
*
|
||||
* @param array $params
|
||||
* @param array $vars
|
||||
* @return Message
|
||||
*/
|
||||
public function buildMessage(array $params, array $vars = []): Message
|
||||
{
|
||||
/** @var Twig $twig */
|
||||
$twig = Grav::instance()['twig'];
|
||||
$twig->init();
|
||||
|
||||
/** @var Config $config */
|
||||
$config = Grav::instance()['config'];
|
||||
|
||||
/** @var Language $language */
|
||||
$language = Grav::instance()['language'];
|
||||
|
||||
// Create message object.
|
||||
$message = new Message();
|
||||
$headers = $message->getEmail()->getHeaders();
|
||||
$email = $message->getEmail();
|
||||
|
||||
// Extend parameters with defaults.
|
||||
$defaults = [
|
||||
'bcc' => $config->get('plugins.email.bcc', []),
|
||||
'bcc_name' => $config->get('plugins.email.bcc_name'),
|
||||
'body' => $config->get('plugins.email.body', '{% include "forms/data.html.twig" %}'),
|
||||
'cc' => $config->get('plugins.email.cc', []),
|
||||
'cc_name' => $config->get('plugins.email.cc_name'),
|
||||
'charset' => $config->get('plugins.email.charset', 'utf-8'),
|
||||
'from' => $config->get('plugins.email.from'),
|
||||
'from_name' => $config->get('plugins.email.from_name'),
|
||||
'content_type' => $config->get('plugins.email.content_type', 'text/html'),
|
||||
'reply_to' => $config->get('plugins.email.reply_to', []),
|
||||
'reply_to_name' => $config->get('plugins.email.reply_to_name'),
|
||||
'subject' => !empty($vars['form']) && $vars['form'] instanceof FormInterface ? $vars['form']->page()->title() : null,
|
||||
'to' => $config->get('plugins.email.to'),
|
||||
'to_name' => $config->get('plugins.email.to_name'),
|
||||
'process_markdown' => false,
|
||||
'template' => false,
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
foreach ($defaults as $key => $value) {
|
||||
if (!key_exists($key, $params)) {
|
||||
$params[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$params['to']) {
|
||||
throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_TO_ADDRESS'));
|
||||
}
|
||||
if (!$params['from']) {
|
||||
throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_FROM_ADDRESS'));
|
||||
}
|
||||
|
||||
|
||||
// make email configuration available to templates
|
||||
$vars += [
|
||||
'email' => $params,
|
||||
];
|
||||
|
||||
$params = $this->processParams($params, $vars);
|
||||
|
||||
// Process parameters.
|
||||
foreach ($params as $key => $value) {
|
||||
switch ($key) {
|
||||
case 'body':
|
||||
if (is_string($value)) {
|
||||
$this->processBody($message, $params, $vars, $twig, $value);
|
||||
} elseif (is_array($value)) {
|
||||
foreach ($value as $body_part) {
|
||||
$params_part = $params;
|
||||
if (isset($body_part['content_type'])) {
|
||||
$params_part['content_type'] = $body_part['content_type'];
|
||||
}
|
||||
if (isset($body_part['template'])) {
|
||||
$params_part['template'] = $body_part['template'];
|
||||
}
|
||||
if (isset($body_part['body'])) {
|
||||
$this->processBody($message, $params_part, $vars, $twig, $body_part['body']);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'subject':
|
||||
if ($value) {
|
||||
$message->subject($language->translate($value));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'to':
|
||||
case 'from':
|
||||
case 'cc':
|
||||
case 'bcc':
|
||||
case 'reply_to':
|
||||
if ($recipients = $this->processRecipients($key, $params)) {
|
||||
$key = $key === 'reply_to' ? 'replyTo' : $key;
|
||||
$email->$key(...$recipients);
|
||||
}
|
||||
break;
|
||||
case 'tags':
|
||||
foreach ((array) $value as $tag) {
|
||||
if (is_string($tag)) {
|
||||
$headers->add(new TagHeader($tag));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'metadata':
|
||||
foreach ((array) $value as $k => $v) {
|
||||
if (is_string($k) && is_string($v)) {
|
||||
$headers->add(new MetadataHeader($k, $v));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
protected function processRecipients(string $type, array $params): array
|
||||
{
|
||||
if (array_key_exists($type, $params) && $params[$type] === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$recipients = $params[$type] ?? Grav::instance()['config']->get('plugins.email.'.$type) ?? [];
|
||||
|
||||
$list = [];
|
||||
|
||||
if (!empty($recipients)) {
|
||||
if (is_array($recipients)) {
|
||||
if (Utils::isAssoc($recipients) || (count($recipients) ===2 && $this->isValidEmail($recipients[0]) && !$this->isValidEmail($recipients[1]))) {
|
||||
$address = $this->createAddress($recipients);
|
||||
if ($address !== null) {
|
||||
$list[] = $address;
|
||||
}
|
||||
} else {
|
||||
foreach ($recipients as $recipient) {
|
||||
$address = $this->createAddress($recipient);
|
||||
if ($address !== null) {
|
||||
$list[] = $address;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (is_string($recipients) && Utils::contains($recipients, ',')) {
|
||||
$recipients = array_map('trim', explode(',', $recipients));
|
||||
foreach ($recipients as $recipient) {
|
||||
$address = $this->createAddress($recipient);
|
||||
if ($address !== null) {
|
||||
$list[] = $address;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!Utils::contains($recipients, ['<','>']) && (isset($params[$type."_name"]))) {
|
||||
$recipients = [$recipients, $params[$type."_name"]];
|
||||
}
|
||||
$address = $this->createAddress($recipients);
|
||||
if ($address !== null) {
|
||||
$list[] = $address;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @return Address|null
|
||||
*/
|
||||
protected function createAddress($data): ?Address
|
||||
{
|
||||
if (is_string($data)) {
|
||||
preg_match('/^(.*)\<(.*)\>$/', $data, $matches);
|
||||
if (isset($matches[2])) {
|
||||
$email = trim($matches[2]);
|
||||
$name = trim($matches[1]);
|
||||
} else {
|
||||
$email = $data;
|
||||
$name = '';
|
||||
}
|
||||
} elseif (Utils::isAssoc($data)) {
|
||||
$first_key = array_key_first($data);
|
||||
if (filter_var($first_key, FILTER_VALIDATE_EMAIL)) {
|
||||
$email = $first_key;
|
||||
$name = $data[$first_key];
|
||||
} else {
|
||||
$email = $data['email'] ?? $data['mail'] ?? $data['address'] ?? '';
|
||||
$name = $data['name'] ?? $data['fullname'] ?? '';
|
||||
}
|
||||
} else {
|
||||
$email = $data[0] ?? '';
|
||||
$name = $data[1] ?? '';
|
||||
}
|
||||
|
||||
// Skip empty or invalid email addresses
|
||||
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Address($email, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|Mailer
|
||||
* @internal
|
||||
*/
|
||||
protected function initMailer(): ?Mailer
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
return null;
|
||||
}
|
||||
if (!$this->mailer) {
|
||||
$this->transport = $this->getTransport();
|
||||
// Create the Mailer using your created Transport
|
||||
$this->mailer = new Mailer($this->transport);
|
||||
}
|
||||
return $this->mailer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function initLog()
|
||||
{
|
||||
$log_file = Grav::instance()['locator']->findResource('log://email.log', true, true);
|
||||
$this->log = new Logger('email');
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$this->log->pushHandler(new StreamHandler($log_file, Logger::DEBUG));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @param array $vars
|
||||
* @return array
|
||||
*/
|
||||
protected function processParams(array $params, array $vars = []): array
|
||||
{
|
||||
$twig = Grav::instance()['twig'];
|
||||
$twig->init();
|
||||
|
||||
// Add twig vars to the context
|
||||
$vars += $twig->twig_vars;
|
||||
|
||||
array_walk_recursive($params, function(&$value) use ($twig, $vars) {
|
||||
if (is_string($value)) {
|
||||
// Process Twig strings WITHOUT security filtering
|
||||
// Email params come from trusted YAML config, not user input
|
||||
$value = $this->processTwigString($twig, $value, $vars);
|
||||
}
|
||||
});
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a Twig string without security filtering.
|
||||
* Used for trusted email configuration strings.
|
||||
*
|
||||
* @param Twig $twig
|
||||
* @param string $string
|
||||
* @param array $vars
|
||||
* @return string
|
||||
*/
|
||||
protected function processTwigString(Twig $twig, string $string, array $vars): string
|
||||
{
|
||||
// Skip if no Twig syntax
|
||||
if (strpos($string, '{{') === false && strpos($string, '{%') === false) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use Grav's setTemplate method which uses the loaderArray
|
||||
$name = '@EmailVar:' . md5($string);
|
||||
$twig->setTemplate($name, $string);
|
||||
|
||||
return $twig->twig->render($name, $vars);
|
||||
} catch (\Exception $e) {
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $message
|
||||
* @param $params
|
||||
* @param $vars
|
||||
* @param $twig
|
||||
* @param $body
|
||||
* @return void
|
||||
*/
|
||||
protected function processBody($message, $params, $vars, $twig, $body)
|
||||
{
|
||||
if ($params['process_markdown'] && $params['content_type'] === 'text/html') {
|
||||
$body = (new Parsedown())->text($body);
|
||||
}
|
||||
|
||||
if ($params['template']) {
|
||||
$body = $twig->processTemplate($params['template'], ['content' => $body] + $vars);
|
||||
}
|
||||
|
||||
$content_type = !empty($params['content_type']) ? $twig->processString($params['content_type'], $vars) : null;
|
||||
|
||||
if ($content_type === 'text/html') {
|
||||
$message->html($body);
|
||||
} else {
|
||||
$message->text($body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TransportInterface
|
||||
*/
|
||||
protected static function getTransport(): Transport\TransportInterface
|
||||
{
|
||||
/** @var Config $config */
|
||||
$config = Grav::instance()['config'];
|
||||
$engine = $config->get('plugins.email.mailer.engine');
|
||||
$dsn = 'null://default';
|
||||
|
||||
|
||||
// Create the Transport and initialize it.
|
||||
switch ($engine) {
|
||||
case 'smtps':
|
||||
case 'smtp':
|
||||
$options = $config->get('plugins.email.mailer.smtp');
|
||||
$dsn = $engine . '://';
|
||||
$auth = '';
|
||||
|
||||
if (isset($options['encryption']) && $options['encryption'] === 'none') {
|
||||
$options['options']['verify_peer'] = 0;
|
||||
}
|
||||
if (isset($options['user'])) {
|
||||
$auth .= urlencode($options['user']);
|
||||
}
|
||||
if (isset($options['password'])) {
|
||||
$auth .= ':'. urlencode($options['password']);
|
||||
}
|
||||
if (!empty($auth)) {
|
||||
$dsn .= "$auth@";
|
||||
}
|
||||
if (isset($options['server'])) {
|
||||
$dsn .= urlencode($options['server']);
|
||||
}
|
||||
if (isset($options['port'])) {
|
||||
$dsn .= ":{$options['port']}";
|
||||
}
|
||||
if (isset($options['options'])) {
|
||||
$dsn .= '?' . http_build_query($options['options']);
|
||||
}
|
||||
break;
|
||||
case 'mail':
|
||||
case 'native':
|
||||
$dsn = 'native://default';
|
||||
break;
|
||||
case 'sendmail':
|
||||
$dsn = 'sendmail://default';
|
||||
$bin = $config->get('plugins.email.mailer.sendmail.bin');
|
||||
if (isset($bin)) {
|
||||
$dsn .= '?command=' . urlencode($bin);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$e = new Event(['engine' => $engine, ]);
|
||||
Grav::instance()->fireEvent('onEmailTransportDsn', $e);
|
||||
if (isset($e['dsn'])) {
|
||||
$dsn = $e['dsn'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if ($dsn instanceof TransportInterface) {
|
||||
$transport = $dsn;
|
||||
} else {
|
||||
$transport = Transport::fromDsn($dsn) ;
|
||||
}
|
||||
|
||||
return $transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any message from the last send attempt
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLastSendMessage(): ?string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any debug information from the last send attempt
|
||||
* @return string|null
|
||||
*/
|
||||
public function getLastSendDebug(): ?string
|
||||
{
|
||||
return $this->debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $recipients
|
||||
* @return string
|
||||
*/
|
||||
protected function jsonifyRecipients(array $recipients): string
|
||||
{
|
||||
$json = [];
|
||||
foreach ($recipients as $recipient) {
|
||||
$json[] = str_replace('"', "", $recipient->toString());
|
||||
}
|
||||
return json_encode($json);
|
||||
}
|
||||
|
||||
protected function isValidEmail($email): bool
|
||||
{
|
||||
return is_string($email) && filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
|
||||
*/
|
||||
public static function flushQueue() {}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
|
||||
*/
|
||||
public static function clearQueueFailures() {}
|
||||
|
||||
/**
|
||||
* Creates an attachment.
|
||||
*
|
||||
* @param string $data
|
||||
* @param string $filename
|
||||
* @param string $contentType
|
||||
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
|
||||
* @return void
|
||||
*/
|
||||
public function attachment($data = null, $filename = null, $contentType = null) {}
|
||||
|
||||
/**
|
||||
* Creates an embedded attachment.
|
||||
*
|
||||
* @param string $data
|
||||
* @param string $filename
|
||||
* @param string $contentType
|
||||
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
|
||||
* @return void
|
||||
*/
|
||||
public function embedded($data = null, $filename = null, $contentType = null) {}
|
||||
|
||||
|
||||
/**
|
||||
* Creates an image attachment.
|
||||
*
|
||||
* @param string $data
|
||||
* @param string $filename
|
||||
* @param string $contentType
|
||||
* @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
|
||||
* @return void
|
||||
*/
|
||||
public function image($data = null, $filename = null, $contentType = null) {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Grav\Plugin\Email;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Plugin\Api\Controllers\AbstractApiController;
|
||||
use Grav\Plugin\Api\Response\ApiResponse;
|
||||
use Grav\Plugin\Api\Response\ErrorResponse;
|
||||
use Grav\Plugin\Api\Exceptions\ValidationException;
|
||||
use Grav\Plugin\Api\Exceptions\ApiException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class EmailApiController extends AbstractApiController
|
||||
{
|
||||
/**
|
||||
* POST /email/send - Send an ad-hoc email.
|
||||
*
|
||||
* Required body fields: to, subject, body
|
||||
* Optional: from, cc, bcc, reply_to, content_type (default: text/html)
|
||||
*/
|
||||
public function send(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requirePermission($request, 'api.system.write');
|
||||
|
||||
$body = $this->getRequestBody($request);
|
||||
$this->requireFields($body, ['to', 'subject', 'body']);
|
||||
|
||||
$email = $this->getEmailService();
|
||||
|
||||
$params = [
|
||||
'to' => $body['to'],
|
||||
'subject' => $body['subject'],
|
||||
'body' => $body['body'],
|
||||
'content_type' => $body['content_type'] ?? 'text/html',
|
||||
];
|
||||
|
||||
// Use configured from address, allow override
|
||||
if (!empty($body['from'])) {
|
||||
$params['from'] = $body['from'];
|
||||
} else {
|
||||
$from = $this->config->get('plugins.email.from');
|
||||
$fromName = $this->config->get('plugins.email.from_name');
|
||||
$params['from'] = $fromName ? "{$fromName} <{$from}>" : $from;
|
||||
}
|
||||
|
||||
foreach (['cc', 'bcc', 'reply_to'] as $field) {
|
||||
if (!empty($body[$field])) {
|
||||
$params[$field] = $body[$field];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$message = $email->buildMessage($params, []);
|
||||
$sent = $email->send($message);
|
||||
} catch (\Throwable $e) {
|
||||
throw new ApiException(500, 'Internal Server Error', 'Failed to send email: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
if ($sent < 1) {
|
||||
$error = $email->getLastSendMessage() ?? 'Unknown error';
|
||||
throw new ApiException(500, 'Internal Server Error', 'Email send failed: ' . $error);
|
||||
}
|
||||
|
||||
return ApiResponse::create([
|
||||
'message' => 'Email sent successfully.',
|
||||
'to' => $body['to'],
|
||||
'subject' => $body['subject'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /email/test - Send a test email to verify configuration.
|
||||
*
|
||||
* Optional body field: to (defaults to configured recipient)
|
||||
*/
|
||||
public function test(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->requirePermission($request, 'api.system.write');
|
||||
|
||||
$body = $this->getRequestBody($request);
|
||||
$to = $body['to'] ?? $this->config->get('plugins.email.to');
|
||||
|
||||
if (!$to) {
|
||||
throw new ValidationException(
|
||||
'No recipient specified. Pass "to" or configure a default in email plugin settings.'
|
||||
);
|
||||
}
|
||||
|
||||
$email = $this->getEmailService();
|
||||
|
||||
$from = $this->config->get('plugins.email.from');
|
||||
$fromName = $this->config->get('plugins.email.from_name');
|
||||
|
||||
try {
|
||||
$message = $email->buildMessage([
|
||||
'to' => $to,
|
||||
'from' => $fromName ? "{$fromName} <{$from}>" : $from,
|
||||
'subject' => 'Grav API - Test Email',
|
||||
'body' => '<h1>Test Email</h1><p>This test email was sent via the Grav API at ' . date('c') . '.</p><p>If you are reading this, your email configuration is working correctly.</p>',
|
||||
'content_type' => 'text/html',
|
||||
], []);
|
||||
|
||||
$sent = $email->send($message);
|
||||
} catch (\Throwable $e) {
|
||||
throw new ApiException(500, 'Internal Server Error', 'Test email failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
if ($sent < 1) {
|
||||
$error = $email->getLastSendMessage() ?? 'Unknown error';
|
||||
throw new ApiException(500, 'Internal Server Error', 'Test email failed: ' . $error);
|
||||
}
|
||||
|
||||
return ApiResponse::create([
|
||||
'message' => 'Test email sent successfully.',
|
||||
'to' => $to,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getEmailService(): Email
|
||||
{
|
||||
$email = $this->grav['Email'] ?? null;
|
||||
|
||||
if (!$email || !Email::enabled()) {
|
||||
throw new ApiException(503, 'Service Unavailable', 'Email plugin is not enabled or configured.');
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
namespace Grav\Plugin\Email;
|
||||
|
||||
use Symfony\Component\Mime\Email as SymfonyEmail;
|
||||
|
||||
class Message
|
||||
{
|
||||
/** @var SymfonyEmail */
|
||||
protected $email;
|
||||
|
||||
public function __construct() {
|
||||
$this->email = new SymfonyEmail();
|
||||
}
|
||||
|
||||
public function subject($subject): self
|
||||
{
|
||||
$this->email->subject($subject);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSubject($subject): self
|
||||
{
|
||||
$this->subject($subject);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function to($to): self
|
||||
{
|
||||
$this->email->to($to);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function from($from): self
|
||||
{
|
||||
$this->email->from($from);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function cc($cc): self
|
||||
{
|
||||
$this->email->cc($cc);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function bcc($bcc): self
|
||||
{
|
||||
$this->email->bcc($bcc);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function replyTo($reply_to): self
|
||||
{
|
||||
$this->email->replyTo($reply_to);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function text($text): self
|
||||
{
|
||||
$this->email->text($text);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function html($html): self
|
||||
{
|
||||
$this->email->html($html);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function attachFromPath($path): self
|
||||
{
|
||||
$this->email->attachFromPath($path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function embedFromPath($path): self
|
||||
{
|
||||
$this->email->embedFromPath($path);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function reply_to($reply_to): self
|
||||
{
|
||||
$this->replyTo($reply_to);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFrom($from): self
|
||||
{
|
||||
$this->from($from);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setTo($to): self
|
||||
{
|
||||
$this->to($to);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmail(): SymfonyEmail
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Plugin\Email;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Twig\Twig;
|
||||
use Grav\Common\Utils as GravUtils;
|
||||
|
||||
/**
|
||||
* Class Utils
|
||||
* @package Grav\Plugin\Email
|
||||
*/
|
||||
class Utils
|
||||
{
|
||||
/**
|
||||
* Quick utility method to send an HTML email.
|
||||
*
|
||||
* @param array<int,mixed> $params
|
||||
*
|
||||
* @return bool True if the action was performed.
|
||||
*/
|
||||
public static function sendEmail(...$params)
|
||||
{
|
||||
if (is_array($params[0])) {
|
||||
$params = array_shift($params);
|
||||
} else {
|
||||
$keys = ['subject', 'body', 'to', 'from', 'content_type'];
|
||||
$params = GravUtils::arrayCombine($keys, $params);
|
||||
}
|
||||
|
||||
//Initialize twig if not yet initialized
|
||||
/** @var Twig $twig */
|
||||
$twig = Grav::instance()['twig']->init();
|
||||
|
||||
/** @var Email $email */
|
||||
$email = Grav::instance()['Email'];
|
||||
|
||||
if (empty($params['to']) || empty($params['subject']) || empty($params['body'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$params['body'] = $twig->processTemplate('email/base.html.twig', ['content' => $params['body']]);
|
||||
|
||||
$message = $email->buildMessage($params);
|
||||
|
||||
return $email->send($message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user