565 lines
19 KiB
PHP
565 lines
19 KiB
PHP
|
<?php
|
||
|
namespace Grav\Plugin\Email;
|
||
|
|
||
|
use Grav\Common\Config\Config;
|
||
|
use Grav\Common\Grav;
|
||
|
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;
|
||
|
|
||
|
class Email
|
||
|
{
|
||
|
/**
|
||
|
* @var \Swift_Transport
|
||
|
*/
|
||
|
protected $mailer;
|
||
|
|
||
|
/**
|
||
|
* @var \Swift_Plugins_LoggerPlugin
|
||
|
*/
|
||
|
protected $logger;
|
||
|
|
||
|
protected $queue_path;
|
||
|
|
||
|
/**
|
||
|
* Returns true if emails have been enabled in the system.
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
public static function enabled()
|
||
|
{
|
||
|
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()
|
||
|
{
|
||
|
return Grav::instance()['config']->get('plugins.email.debug') == 'true';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates an email message.
|
||
|
*
|
||
|
* @param string $subject
|
||
|
* @param string $body
|
||
|
* @param string $contentType
|
||
|
* @param string $charset
|
||
|
* @return \Swift_Message
|
||
|
*/
|
||
|
public function message($subject = null, $body = null, $contentType = null, $charset = null)
|
||
|
{
|
||
|
return new \Swift_Message($subject, $body, $contentType, $charset);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates an attachment.
|
||
|
*
|
||
|
* @param string $data
|
||
|
* @param string $filename
|
||
|
* @param string $contentType
|
||
|
* @return \Swift_Attachment
|
||
|
*/
|
||
|
public function attachment($data = null, $filename = null, $contentType = null)
|
||
|
{
|
||
|
return new \Swift_Attachment($data, $filename, $contentType);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates an embedded attachment.
|
||
|
*
|
||
|
* @param string $data
|
||
|
* @param string $filename
|
||
|
* @param string $contentType
|
||
|
* @return \Swift_EmbeddedFile
|
||
|
*/
|
||
|
public function embedded($data = null, $filename = null, $contentType = null)
|
||
|
{
|
||
|
return new \Swift_EmbeddedFile($data, $filename, $contentType);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates an image attachment.
|
||
|
*
|
||
|
* @param string $data
|
||
|
* @param string $filename
|
||
|
* @param string $contentType
|
||
|
* @return \Swift_Image
|
||
|
*/
|
||
|
public function image($data = null, $filename = null, $contentType = null)
|
||
|
{
|
||
|
return new \Swift_Image($data, $filename, $contentType);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send email.
|
||
|
*
|
||
|
* @param \Swift_Message $message
|
||
|
* @param array|null $failedRecipients
|
||
|
* @return int
|
||
|
*/
|
||
|
public function send($message, &$failedRecipients = null)
|
||
|
{
|
||
|
$mailer = $this->getMailer();
|
||
|
|
||
|
$result = $mailer ? $mailer->send($message, $failedRecipients) : 0;
|
||
|
|
||
|
// Check if emails and debugging are both enabled.
|
||
|
if ($mailer && $this->debug()) {
|
||
|
|
||
|
$log = new Logger('email');
|
||
|
$locator = Grav::instance()['locator'];
|
||
|
$log_file = $locator->findResource('log://email.log', true, true);
|
||
|
$log->pushHandler(new StreamHandler($log_file, Logger::DEBUG));
|
||
|
|
||
|
// Append the SwiftMailer log to the log.
|
||
|
$log->addDebug($this->getLogs());
|
||
|
}
|
||
|
|
||
|
return $result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Build e-mail message.
|
||
|
*
|
||
|
* @param array $params
|
||
|
* @param array $vars
|
||
|
* @return \Swift_Message
|
||
|
*/
|
||
|
public function buildMessage(array $params, array $vars = [])
|
||
|
{
|
||
|
/** @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 = $this->message();
|
||
|
|
||
|
// Extend parameters with defaults.
|
||
|
$params += [
|
||
|
'bcc' => $config->get('plugins.email.bcc', []),
|
||
|
'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
|
||
|
];
|
||
|
|
||
|
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,
|
||
|
];
|
||
|
|
||
|
// Process parameters.
|
||
|
foreach ($params as $key => $value) {
|
||
|
switch ($key) {
|
||
|
case 'body':
|
||
|
if (is_string($value)) {
|
||
|
if (strpos($value, '{{') !== false || strpos($value, '{%') !== false) {
|
||
|
$body = $twig->processString($value, $vars);
|
||
|
} else {
|
||
|
$body = $value;
|
||
|
}
|
||
|
|
||
|
if ($params['process_markdown'] && $params['content_type'] === 'text/html') {
|
||
|
$parsedown = new Parsedown();
|
||
|
$body = $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;
|
||
|
$charset = !empty($params['charset']) ? $twig->processString($params['charset'], $vars) : null;
|
||
|
|
||
|
$message->setBody($body, $content_type, $charset);
|
||
|
} elseif (is_array($value)) {
|
||
|
foreach ($value as $body_part) {
|
||
|
$body_part += [
|
||
|
'charset' => $params['charset'],
|
||
|
'content_type' => $params['content_type'],
|
||
|
];
|
||
|
|
||
|
$body = !empty($body_part['body']) ? $twig->processString($body_part['body'], $vars) : null;
|
||
|
|
||
|
if ($params['process_markdown'] && $body_part['content_type'] === 'text/html') {
|
||
|
$parsedown = new Parsedown();
|
||
|
$body = $parsedown->text($body);
|
||
|
}
|
||
|
|
||
|
if (isset($body_part['template'])) {
|
||
|
$body = $twig->processTemplate($body_part['template'], ['content' => $body] + $vars);
|
||
|
}
|
||
|
|
||
|
$content_type = !empty($body_part['content_type']) ? $twig->processString($body_part['content_type'], $vars) : null;
|
||
|
$charset = !empty($body_part['charset']) ? $twig->processString($body_part['charset'], $vars) : null;
|
||
|
|
||
|
if (!$message->getBody()) {
|
||
|
$message->setBody($body, $content_type, $charset);
|
||
|
}
|
||
|
else {
|
||
|
$message->addPart($body, $content_type, $charset);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'subject':
|
||
|
if ($value) {
|
||
|
$message->setSubject($twig->processString($language->translate($value), $vars));
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'to':
|
||
|
if (is_string($value) && !empty($params['to_name'])) {
|
||
|
$value = [
|
||
|
'mail' => $twig->processString($value, $vars),
|
||
|
'name' => $twig->processString($params['to_name'], $vars),
|
||
|
];
|
||
|
}
|
||
|
|
||
|
foreach ($this->parseAddressValue($value, $vars) as $address) {
|
||
|
$message->addTo($address->mail, $address->name);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'cc':
|
||
|
if (is_string($value) && !empty($params['cc_name'])) {
|
||
|
$value = [
|
||
|
'mail' => $twig->processString($value, $vars),
|
||
|
'name' => $twig->processString($params['cc_name'], $vars),
|
||
|
];
|
||
|
}
|
||
|
|
||
|
foreach ($this->parseAddressValue($value, $vars) as $address) {
|
||
|
$message->addCc($address->mail, $address->name);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'bcc':
|
||
|
foreach ($this->parseAddressValue($value, $vars) as $address) {
|
||
|
$message->addBcc($address->mail, $address->name);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'from':
|
||
|
if (is_string($value) && !empty($params['from_name'])) {
|
||
|
$value = [
|
||
|
'mail' => $twig->processString($value, $vars),
|
||
|
'name' => $twig->processString($params['from_name'], $vars),
|
||
|
];
|
||
|
}
|
||
|
|
||
|
foreach ($this->parseAddressValue($value, $vars) as $address) {
|
||
|
$message->addFrom($address->mail, $address->name);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 'reply_to':
|
||
|
if (is_string($value) && !empty($params['reply_to_name'])) {
|
||
|
$value = [
|
||
|
'mail' => $twig->processString($value, $vars),
|
||
|
'name' => $twig->processString($params['reply_to_name'], $vars),
|
||
|
];
|
||
|
}
|
||
|
|
||
|
foreach ($this->parseAddressValue($value, $vars) as $address) {
|
||
|
$message->addReplyTo($address->mail, $address->name);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $message;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return parsed e-mail address value.
|
||
|
*
|
||
|
* @param string|string[] $value
|
||
|
* @param array $vars
|
||
|
* @return array
|
||
|
*/
|
||
|
public function parseAddressValue($value, array $vars = [])
|
||
|
{
|
||
|
$parsed = [];
|
||
|
|
||
|
/** @var Twig $twig */
|
||
|
$twig = Grav::instance()['twig'];
|
||
|
|
||
|
// Single e-mail address string
|
||
|
if (is_string($value)) {
|
||
|
$parsed[] = (object) [
|
||
|
'mail' => $twig->processString($value, $vars),
|
||
|
'name' => null,
|
||
|
];
|
||
|
}
|
||
|
|
||
|
else {
|
||
|
// Cast value as array
|
||
|
$value = (array) $value;
|
||
|
|
||
|
// Single e-mail address array
|
||
|
if (!empty($value['mail'])) {
|
||
|
$parsed[] = (object) [
|
||
|
'mail' => $twig->processString($value['mail'], $vars),
|
||
|
'name' => !empty($value['name']) ? $twig->processString($value['name'], $vars) : NULL,
|
||
|
];
|
||
|
}
|
||
|
|
||
|
// Multiple addresses (either as strings or arrays)
|
||
|
elseif (!(empty($value['mail']) && !empty($value['name']))) {
|
||
|
foreach ($value as $y => $itemx) {
|
||
|
$addresses = $this->parseAddressValue($itemx, $vars);
|
||
|
|
||
|
if (($address = reset($addresses))) {
|
||
|
$parsed[] = $address;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $parsed;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Return debugging logs if enabled
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
public function getLogs()
|
||
|
{
|
||
|
if ($this->debug()) {
|
||
|
return $this->logger->dump();
|
||
|
}
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @internal
|
||
|
* @return null|\Swift_Mailer
|
||
|
*/
|
||
|
protected function getMailer()
|
||
|
{
|
||
|
if (!$this->enabled()) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (!$this->mailer) {
|
||
|
/** @var Config $config */
|
||
|
$config = Grav::instance()['config'];
|
||
|
$queue_enabled = $config->get('plugins.email.queue.enabled');
|
||
|
|
||
|
$transport = $queue_enabled === true ? $this->getQueue() : $this->getTransport();
|
||
|
|
||
|
// Create the Mailer using your created Transport
|
||
|
$this->mailer = new \Swift_Mailer($transport);
|
||
|
|
||
|
// Register the logger if we're debugging.
|
||
|
if ($this->debug()) {
|
||
|
$this->logger = new \Swift_Plugins_Loggers_ArrayLogger();
|
||
|
$this->mailer->registerPlugin(new \Swift_Plugins_LoggerPlugin($this->logger));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $this->mailer;
|
||
|
}
|
||
|
|
||
|
protected static function getQueuePath()
|
||
|
{
|
||
|
$queue_path = Grav::instance()['locator']->findResource('user://data', true) . '/email-queue';
|
||
|
|
||
|
if (!file_exists($queue_path)) {
|
||
|
mkdir($queue_path);
|
||
|
}
|
||
|
|
||
|
return $queue_path;
|
||
|
}
|
||
|
|
||
|
protected static function getQueue()
|
||
|
{
|
||
|
$queue_path = static::getQueuePath();
|
||
|
|
||
|
$spool = new \Swift_FileSpool($queue_path);
|
||
|
$transport = new \Swift_SpoolTransport($spool);
|
||
|
|
||
|
return $transport;
|
||
|
}
|
||
|
|
||
|
public static function flushQueue()
|
||
|
{
|
||
|
$grav = Grav::instance();
|
||
|
|
||
|
$grav['debugger']->enabled(false);
|
||
|
|
||
|
$config = $grav['config']->get('plugins.email.queue');
|
||
|
|
||
|
try {
|
||
|
$queue = static::getQueue();
|
||
|
$spool = $queue->getSpool();
|
||
|
$spool->setMessageLimit($config['flush_msg_limit']);
|
||
|
$spool->setTimeLimit($config['flush_time_limit']);
|
||
|
$failures = [];
|
||
|
$result = $spool->flushQueue(static::getTransport(), $failures);
|
||
|
return $result . ' messages flushed from queue...';
|
||
|
} catch (\Exception $e) {
|
||
|
$grav['log']->error($e->getMessage());
|
||
|
return $e->getMessage();
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
public static function clearQueueFailures()
|
||
|
{
|
||
|
$grav = Grav::instance();
|
||
|
$grav['debugger']->enabled(false);
|
||
|
|
||
|
$preferences = \Swift_Preferences::getInstance();
|
||
|
$preferences->setTempDir(sys_get_temp_dir());
|
||
|
|
||
|
/** @var \Swift_Transport $transport */
|
||
|
$transport = static::getTransport();
|
||
|
if (!$transport->isStarted()) {
|
||
|
$transport->start();
|
||
|
}
|
||
|
|
||
|
$queue_path = static::getQueuePath();
|
||
|
|
||
|
foreach (new \GlobIterator($queue_path . '/*.sending') as $file) {
|
||
|
$final_message = $file->getPathname();
|
||
|
|
||
|
/** @var \Swift_Message $message */
|
||
|
$message = unserialize(file_get_contents($final_message));
|
||
|
|
||
|
echo(sprintf(
|
||
|
'Retrying "%s" to "%s"',
|
||
|
$message->getSubject(),
|
||
|
implode(', ', array_keys($message->getTo()))
|
||
|
) . "\n");
|
||
|
|
||
|
try {
|
||
|
$clean = static::cloneMessage($message);
|
||
|
$transport->send($clean);
|
||
|
echo("sent!\n");
|
||
|
|
||
|
// DOn't want to trip up any errors from sending too fast
|
||
|
sleep(1);
|
||
|
} catch (\Swift_TransportException $e) {
|
||
|
echo("ERROR: Send failed - deleting spooled message\n");
|
||
|
}
|
||
|
|
||
|
// Remove the file
|
||
|
unlink($final_message);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clean copy a message
|
||
|
*
|
||
|
* @param \Swift_Message $message
|
||
|
*/
|
||
|
public static function cloneMessage($message)
|
||
|
{
|
||
|
$clean = new \Swift_Message();
|
||
|
|
||
|
$clean->setBoundary($message->getBoundary());
|
||
|
$clean->setBcc($message->getBcc());
|
||
|
$clean->setBody($message->getBody());
|
||
|
$clean->setCharset($message->getCharset());
|
||
|
$clean->setChildren($message->getChildren());
|
||
|
$clean->setContentType($message->getContentType());
|
||
|
$clean->setCc($message->getCc());
|
||
|
$clean->setDate($message->getDate());
|
||
|
$clean->setDescription($message->getDescription());
|
||
|
$clean->setEncoder($message->getEncoder());
|
||
|
$clean->setFormat($message->getFormat());
|
||
|
$clean->setFrom($message->getFrom());
|
||
|
$clean->setId($message->getId());
|
||
|
$clean->setMaxLineLength($message->getMaxLineLength());
|
||
|
$clean->setPriority($message->getPriority());
|
||
|
$clean->setReplyTo($message->getReplyTo());
|
||
|
$clean->setReturnPath($message->getReturnPath());
|
||
|
$clean->setSender($message->getSender());
|
||
|
$clean->setSubject($message->getSubject());
|
||
|
$clean->setTo($message->getTo());
|
||
|
$clean->setAuthMode($message->getAuthMode());
|
||
|
|
||
|
return $clean;
|
||
|
|
||
|
}
|
||
|
|
||
|
protected static function getTransport()
|
||
|
{
|
||
|
/** @var Config $config */
|
||
|
$config = Grav::instance()['config'];
|
||
|
|
||
|
$engine = $config->get('plugins.email.mailer.engine');
|
||
|
|
||
|
// Create the Transport and initialize it.
|
||
|
switch ($engine) {
|
||
|
case 'smtp':
|
||
|
$transport = new \Swift_SmtpTransport();
|
||
|
|
||
|
$options = $config->get('plugins.email.mailer.smtp');
|
||
|
if (!empty($options['server'])) {
|
||
|
$transport->setHost($options['server']);
|
||
|
}
|
||
|
if (!empty($options['port'])) {
|
||
|
$transport->setPort($options['port']);
|
||
|
}
|
||
|
if (!empty($options['encryption']) && $options['encryption'] !== 'none') {
|
||
|
$transport->setEncryption($options['encryption']);
|
||
|
}
|
||
|
if (!empty($options['user'])) {
|
||
|
$transport->setUsername($options['user']);
|
||
|
}
|
||
|
if (!empty($options['password'])) {
|
||
|
$transport->setPassword($options['password']);
|
||
|
}
|
||
|
if (!empty($options['auth_mode'])) {
|
||
|
$transport->setAuthMode($options['auth_mode']);
|
||
|
}
|
||
|
break;
|
||
|
case 'sendmail':
|
||
|
default:
|
||
|
$options = $config->get('plugins.email.mailer.sendmail');
|
||
|
$bin = !empty($options['bin']) ? $options['bin'] : '/usr/sbin/sendmail';
|
||
|
$transport = new \Swift_SendmailTransport($bin);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return $transport;
|
||
|
}
|
||
|
}
|