Improve email sending system and add Postmark as mail transport

This commit is contained in:
Frederic Guillot
2015-06-06 14:10:31 -04:00
parent c87e1fbc33
commit 9d9e3afba2
15 changed files with 284 additions and 137 deletions

View File

@@ -112,7 +112,7 @@ class Webhook extends Base
$this->response->text('Not Authorized', 401);
}
echo $this->postmarkWebhook->parsePayload($this->request->getJson() ?: array()) ? 'PARSED' : 'IGNORED';
echo $this->postmark->receiveEmail($this->request->getJson() ?: array()) ? 'PARSED' : 'IGNORED';
}
/**

View File

@@ -11,6 +11,7 @@ use Pimple\Container;
* @author Frederic Guillot
*
* @property \Core\Helper $helper
* @property \Core\EmailClient $emailClient
* @property \Core\HttpClient $httpClient
* @property \Core\Paginator $paginator
* @property \Core\Request $request
@@ -22,9 +23,10 @@ use Pimple\Container;
* @property \Integration\HipchatWebhook $hipchatWebhook
* @property \Integration\Jabber $jabber
* @property \Integration\MailgunWebhook $mailgunWebhook
* @property \Integration\PostmarkWebhook $postmarkWebhook
* @property \Integration\Postmark $postmark
* @property \Integration\SendgridWebhook $sendgridWebhook
* @property \Integration\SlackWebhook $slackWebhook
* @property \Integration\Smtp $smtp
* @property \Model\Acl $acl
* @property \Model\Action $action
* @property \Model\Authentication $authentication

43
app/Core/EmailClient.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace Core;
/**
* Mail client
*
* @package core
* @author Frederic Guillot
*/
class EmailClient extends Base
{
/**
* Send a HTML email
*
* @access public
* @param string $email
* @param string $name
* @param string $subject
* @param string $html
*/
public function send($email, $name, $subject, $html)
{
$this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
$start_time = microtime(true);
$author = 'Kanboard';
if (Session::isOpen() && $this->userSession->isLogged()) {
$author = e('%s via Kanboard', $this->user->getFullname($this->session['user']));
}
switch (MAIL_TRANSPORT) {
case 'postmark':
$this->postmark->sendEmail($email, $name, $subject, $html, $author);
break;
default:
$this->smtp->sendEmail($email, $name, $subject, $html, $author);
}
$this->container['logger']->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
}
}

View File

@@ -2,22 +2,20 @@
namespace Core;
use Pimple\Container;
/**
* HTTP client
*
* @package core
* @author Frederic Guillot
*/
class HttpClient
class HttpClient extends Base
{
/**
* HTTP connection timeout in seconds
*
* @var integer
*/
const HTTP_TIMEOUT = 2;
const HTTP_TIMEOUT = 5;
/**
* Number of maximum redirections for the HTTP client
@@ -31,26 +29,7 @@ class HttpClient
*
* @var string
*/
const HTTP_USER_AGENT = 'Kanboard Webhook';
/**
* Container instance
*
* @access protected
* @var \Pimple\Container
*/
protected $container;
/**
* Constructor
*
* @access public
* @param \Pimple\Container $container
*/
public function __construct(Container $container)
{
$this->container = $container;
}
const HTTP_USER_AGENT = 'Kanboard';
/**
* Send a POST HTTP request
@@ -58,19 +37,20 @@ class HttpClient
* @access public
* @param string $url
* @param array $data
* @param array $headers
* @return string
*/
public function post($url, array $data)
public function post($url, array $data, array $headers = array())
{
if (empty($url)) {
return '';
}
$headers = array(
$headers = array_merge(array(
'User-Agent: '.self::HTTP_USER_AGENT,
'Content-Type: application/json',
'Connection: close',
);
), $headers);
$context = stream_context_create(array(
'http' => array(
@@ -83,12 +63,21 @@ class HttpClient
)
));
$response = @file_get_contents(trim($url), false, $context);
$stream = @fopen(trim($url), 'r', false, $context);
$response = '';
if (is_resource($stream)) {
$response = stream_get_contents($stream);
}
else {
$this->container['logger']->error('HttpClient: request failed');
}
if (DEBUG) {
$this->container['logger']->debug($url);
$this->container['logger']->debug(var_export($data, true));
$this->container['logger']->debug($response);
$this->container['logger']->debug('HttpClient: url='.$url);
$this->container['logger']->debug('HttpClient: payload='.var_export($data, true));
$this->container['logger']->debug('HttpClient: metadata='.var_export(@stream_get_meta_data($stream), true));
$this->container['logger']->debug('HttpClient: response='.$response);
}
return $response;

View File

@@ -5,13 +5,40 @@ namespace Integration;
use HTML_To_Markdown;
/**
* Postmark Webhook
* Postmark integration
*
* @package integration
* @author Frederic Guillot
*/
class PostmarkWebhook extends \Core\Base
class Postmark extends \Core\Base
{
/**
* Send a HTML email
*
* @access public
* @param string $email
* @param string $name
* @param string $subject
* @param string $html
* @param string $author
*/
public function sendEmail($email, $name, $subject, $html, $author)
{
$headers = array(
'Accept: application/json',
'X-Postmark-Server-Token: '.POSTMARK_API_TOKEN,
);
$payload = array(
'From' => sprintf('%s <%s>', $author, MAIL_FROM),
'To' => sprintf('%s <%s>', $name, $email),
'Subject' => $subject,
'HtmlBody' => $html,
);
$this->httpClient->post('https://api.postmarkapp.com/email', $payload, $headers);
}
/**
* Parse incoming email
*
@@ -19,7 +46,7 @@ class PostmarkWebhook extends \Core\Base
* @param array $payload Incoming email
* @return boolean
*/
public function parsePayload(array $payload)
public function receiveEmail(array $payload)
{
if (empty($payload['From']) || empty($payload['Subject']) || empty($payload['MailboxHash'])) {
return false;
@@ -29,7 +56,7 @@ class PostmarkWebhook extends \Core\Base
$user = $this->user->getByEmail($payload['From']);
if (empty($user)) {
$this->container['logger']->debug('PostmarkWebhook: ignored => user not found');
$this->container['logger']->debug('Postmark: ignored => user not found');
return false;
}
@@ -37,13 +64,13 @@ class PostmarkWebhook extends \Core\Base
$project = $this->project->getByIdentifier($payload['MailboxHash']);
if (empty($project)) {
$this->container['logger']->debug('PostmarkWebhook: ignored => project not found');
$this->container['logger']->debug('Postmark: ignored => project not found');
return false;
}
// The user must be member of the project
if (! $this->projectPermission->isMember($project['id'], $user['id'])) {
$this->container['logger']->debug('PostmarkWebhook: ignored => user is not member of the project');
$this->container['logger']->debug('Postmark: ignored => user is not member of the project');
return false;
}

71
app/Integration/Smtp.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
namespace Integration;
use Swift_Message;
use Swift_Mailer;
use Swift_MailTransport;
use Swift_SendmailTransport;
use Swift_SmtpTransport;
use Swift_TransportException;
/**
* Smtp
*
* @package integration
* @author Frederic Guillot
*/
class Smtp extends \Core\Base
{
/**
* Send a HTML email
*
* @access public
* @param string $email
* @param string $name
* @param string $subject
* @param string $html
* @param string $author
*/
public function sendEmail($email, $name, $subject, $html, $author)
{
try {
$message = Swift_Message::newInstance()
->setSubject($subject)
->setFrom(array(MAIL_FROM => $author))
->setBody($html, 'text/html')
->setTo(array($email => $name));
Swift_Mailer::newInstance($this->getTransport())->send($message);
}
catch (Swift_TransportException $e) {
$this->container['logger']->error($e->getMessage());
}
}
/**
* Get SwiftMailer transport
*
* @access private
* @return \Swift_Transport
*/
private function getTransport()
{
switch (MAIL_TRANSPORT) {
case 'smtp':
$transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT);
$transport->setUsername(MAIL_SMTP_USERNAME);
$transport->setPassword(MAIL_SMTP_PASSWORD);
$transport->setEncryption(MAIL_SMTP_ENCRYPTION);
break;
case 'sendmail':
$transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND);
break;
default:
$transport = Swift_MailTransport::newInstance();
}
return $transport;
}
}

View File

@@ -4,9 +4,6 @@ namespace Model;
use Core\Session;
use Core\Translator;
use Swift_Message;
use Swift_Mailer;
use Swift_TransportException;
/**
* Notification model
@@ -101,43 +98,22 @@ class Notification extends Base
*/
public function sendEmails($template, array $users, array $data)
{
try {
foreach ($users as $user) {
$author = '';
if (Session::isOpen() && $this->userSession->isLogged()) {
$author = e('%s via Kanboard', $this->user->getFullname($this->session['user']));
// Use the user language otherwise use the application language (do not use the session language)
if (! empty($user['language'])) {
Translator::load($user['language']);
}
else {
Translator::load($this->config->get('application_language', 'en_US'));
}
$mailer = Swift_Mailer::newInstance($this->container['mailer']);
foreach ($users as $user) {
$this->container['logger']->debug('Send email notification to '.$user['username'].' lang='.$user['language']);
$start_time = microtime(true);
// Use the user language otherwise use the application language (do not use the session language)
if (! empty($user['language'])) {
Translator::load($user['language']);
}
else {
Translator::load($this->config->get('application_language', 'en_US'));
}
// Send the message
$message = Swift_Message::newInstance()
->setSubject($this->getMailSubject($template, $data))
->setFrom(array(MAIL_FROM => $author ?: 'Kanboard'))
->setBody($this->getMailContent($template, $data), 'text/html')
->setTo(array($user['email'] => $user['name'] ?: $user['username']));
$mailer->send($message);
$this->container['logger']->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
}
}
catch (Swift_TransportException $e) {
$this->container['logger']->error($e->getMessage());
$this->emailClient->send(
$user['email'],
$user['name'] ?: $user['username'],
$this->getMailSubject($template, $data),
$this->getMailContent($template, $data)
);
}
// Restore locales
@@ -167,40 +143,40 @@ class Notification extends Base
{
switch ($template) {
case 'file_creation':
$subject = $this->getStandardMailSubject(t('New attachment'), $data);
$subject = $this->getStandardMailSubject(e('New attachment'), $data);
break;
case 'comment_creation':
$subject = $this->getStandardMailSubject(t('New comment'), $data);
$subject = $this->getStandardMailSubject(e('New comment'), $data);
break;
case 'comment_update':
$subject = $this->getStandardMailSubject(t('Comment updated'), $data);
$subject = $this->getStandardMailSubject(e('Comment updated'), $data);
break;
case 'subtask_creation':
$subject = $this->getStandardMailSubject(t('New subtask'), $data);
$subject = $this->getStandardMailSubject(e('New subtask'), $data);
break;
case 'subtask_update':
$subject = $this->getStandardMailSubject(t('Subtask updated'), $data);
$subject = $this->getStandardMailSubject(e('Subtask updated'), $data);
break;
case 'task_creation':
$subject = $this->getStandardMailSubject(t('New task'), $data);
$subject = $this->getStandardMailSubject(e('New task'), $data);
break;
case 'task_update':
$subject = $this->getStandardMailSubject(t('Task updated'), $data);
$subject = $this->getStandardMailSubject(e('Task updated'), $data);
break;
case 'task_close':
$subject = $this->getStandardMailSubject(t('Task closed'), $data);
$subject = $this->getStandardMailSubject(e('Task closed'), $data);
break;
case 'task_open':
$subject = $this->getStandardMailSubject(t('Task opened'), $data);
$subject = $this->getStandardMailSubject(e('Task opened'), $data);
break;
case 'task_move_column':
$subject = $this->getStandardMailSubject(t('Column Change'), $data);
$subject = $this->getStandardMailSubject(e('Column Change'), $data);
break;
case 'task_move_position':
$subject = $this->getStandardMailSubject(t('Position Change'), $data);
$subject = $this->getStandardMailSubject(e('Position Change'), $data);
break;
case 'task_assignee_change':
$subject = $this->getStandardMailSubject(t('Assignee Change'), $data);
$subject = $this->getStandardMailSubject(e('Assignee Change'), $data);
break;
case 'task_due':
$subject = e('[%s][Due tasks]', $data['project']);

View File

@@ -64,6 +64,7 @@ class ClassProvider implements ServiceProviderInterface
'Webhook',
),
'Core' => array(
'EmailClient',
'Helper',
'HttpClient',
'MemoryCache',
@@ -78,9 +79,10 @@ class ClassProvider implements ServiceProviderInterface
'HipchatWebhook',
'Jabber',
'MailgunWebhook',
'PostmarkWebhook',
'Postmark',
'SendgridWebhook',
'SlackWebhook',
'Smtp',
)
);

View File

@@ -1,33 +0,0 @@
<?php
namespace ServiceProvider;
use Pimple\Container;
use Pimple\ServiceProviderInterface;
use Swift_SmtpTransport;
use Swift_SendmailTransport;
use Swift_MailTransport;
class MailerProvider implements ServiceProviderInterface
{
public function register(Container $container)
{
$container['mailer'] = function () {
switch (MAIL_TRANSPORT) {
case 'smtp':
$transport = Swift_SmtpTransport::newInstance(MAIL_SMTP_HOSTNAME, MAIL_SMTP_PORT);
$transport->setUsername(MAIL_SMTP_USERNAME);
$transport->setPassword(MAIL_SMTP_PASSWORD);
$transport->setEncryption(MAIL_SMTP_ENCRYPTION);
break;
case 'sendmail':
$transport = Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND);
break;
default:
$transport = Swift_MailTransport::newInstance();
}
return $transport;
};
}
}

View File

@@ -27,4 +27,3 @@ $container->register(new ServiceProvider\LoggingProvider);
$container->register(new ServiceProvider\DatabaseProvider);
$container->register(new ServiceProvider\ClassProvider);
$container->register(new ServiceProvider\EventDispatcherProvider);
$container->register(new ServiceProvider\MailerProvider);

View File

@@ -64,6 +64,7 @@ defined('MAIL_SMTP_USERNAME') or define('MAIL_SMTP_USERNAME', '');
defined('MAIL_SMTP_PASSWORD') or define('MAIL_SMTP_PASSWORD', '');
defined('MAIL_SMTP_ENCRYPTION') or define('MAIL_SMTP_ENCRYPTION', null);
defined('MAIL_SENDMAIL_COMMAND') or define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs');
defined('POSTMARK_API_TOKEN') or define('POSTMARK_API_TOKEN', '');
// Enable or disable "Strict-Transport-Security" HTTP header
defined('ENABLE_HSTS') or define('ENABLE_HSTS', true);