Make mail transports pluggable and move integrations to plugins

- Postmark: https://github.com/kanboard/plugin-postmark
- Mailgun: https://github.com/kanboard/plugin-mailgun
- Sendgrid: https://github.com/kanboard/plugin-sendgrid
This commit is contained in:
Frederic Guillot 2015-10-16 20:50:12 -04:00
parent 9c9ed02cd7
commit f99a3c501f
55 changed files with 652 additions and 1457 deletions

View File

@ -3,7 +3,8 @@ Version 1.0.20 (unreleased)
Breaking changes:
- Add namespace Kanboard
- Add namespace Kanboard (update your plugins)
- Move Mailgun, Sendgrid and Postmark to plugins
New features:
@ -13,6 +14,7 @@ Improvements:
* Allow to change comments sorting
* Add the possibility to append or not custom filters
* Make mail transports pluggable
Version 1.0.19
--------------

View File

@ -92,37 +92,4 @@ class Webhook extends Base
echo $result ? 'PARSED' : 'IGNORED';
}
/**
* Handle Postmark webhooks
*
* @access public
*/
public function postmark()
{
$this->checkWebhookToken();
echo $this->postmark->receiveEmail($this->request->getJson()) ? 'PARSED' : 'IGNORED';
}
/**
* Handle Mailgun webhooks
*
* @access public
*/
public function mailgun()
{
$this->checkWebhookToken();
echo $this->mailgun->receiveEmail($_POST) ? 'PARSED' : 'IGNORED';
}
/**
* Handle Sendgrid webhooks
*
* @access public
*/
public function sendgrid()
{
$this->checkWebhookToken();
echo $this->sendgrid->receiveEmail($_POST) ? 'PARSED' : 'IGNORED';
}
}

View File

@ -11,7 +11,7 @@ use Pimple\Container;
* @author Frederic Guillot
*
* @property \Kanboard\Core\Helper $helper
* @property \Kanboard\Core\EmailClient $emailClient
* @property \Kanboard\Core\Mail\Client $emailClient
* @property \Kanboard\Core\HttpClient $httpClient
* @property \Kanboard\Core\Paginator $paginator
* @property \Kanboard\Core\Request $request
@ -29,11 +29,7 @@ use Pimple\Container;
* @property \Kanboard\Integration\GitlabWebhook $gitlabWebhook
* @property \Kanboard\Integration\HipchatWebhook $hipchatWebhook
* @property \Kanboard\Integration\Jabber $jabber
* @property \Kanboard\Integration\Mailgun $mailgun
* @property \Kanboard\Integration\Postmark $postmark
* @property \Kanboard\Integration\Sendgrid $sendgrid
* @property \Kanboard\Integration\SlackWebhook $slackWebhook
* @property \Kanboard\Integration\Smtp $smtp
* @property \Kanboard\Formatter\ProjectGanttFormatter $projectGanttFormatter
* @property \Kanboard\Formatter\TaskFilterGanttFormatter $taskFilterGanttFormatter
* @property \Kanboard\Formatter\TaskFilterAutoCompleteFormatter $taskFilterAutoCompleteFormatter

View File

@ -1,49 +0,0 @@
<?php
namespace Kanboard\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 'sendgrid':
$this->sendgrid->sendEmail($email, $name, $subject, $html, $author);
break;
case 'mailgun':
$this->mailgun->sendEmail($email, $name, $subject, $html, $author);
break;
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');
}
}

96
app/Core/Mail/Client.php Normal file
View File

@ -0,0 +1,96 @@
<?php
namespace Kanboard\Core\Mail;
use Pimple\Container;
use Kanboard\Core\Base;
/**
* Mail Client
*
* @package mail
* @author Frederic Guillot
*/
class Client extends Base
{
/**
* Mail transport instances
*
* @access private
* @var \Pimple\Container
*/
private $transports;
/**
* Constructor
*
* @access public
* @param \Pimple\Container $container
*/
public function __construct(Container $container)
{
parent::__construct($container);
$this->transports = new Container;
}
/**
* Send a HTML email
*
* @access public
* @param string $email
* @param string $name
* @param string $subject
* @param string $html
* @return EmailClient
*/
public function send($email, $name, $subject, $html)
{
$this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')');
$start_time = microtime(true);
$author = 'Kanboard';
if ($this->userSession->isLogged()) {
$author = e('%s via Kanboard', $this->user->getFullname($this->session['user']));
}
$this->getTransport(MAIL_TRANSPORT)->sendEmail($email, $name, $subject, $html, $author);
if (DEBUG) {
$this->logger->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds');
}
return $this;
}
/**
* Get mail transport instance
*
* @access public
* @param string $transport
* @return EmailClientInterface
*/
public function getTransport($transport)
{
return $this->transports[$transport];
}
/**
* Add a new mail transport
*
* @access public
* @param string $transport
* @param string $class
* @return EmailClient
*/
public function setTransport($transport, $class)
{
$container = $this->container;
$this->transports[$transport] = function() use ($class, $container) {
return new $class($container);
};
return $this;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Kanboard\Core\Mail;
/**
* Mail Client Interface
*
* @package mail
* @author Frederic Guillot
*/
interface ClientInterface
{
/**
* 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);
}

View File

@ -1,21 +1,21 @@
<?php
namespace Kanboard\Integration;
namespace Kanboard\Core\Mail\Transport;
use Swift_Message;
use Swift_Mailer;
use Swift_MailTransport;
use Swift_SendmailTransport;
use Swift_SmtpTransport;
use Swift_TransportException;
use Kanboard\Core\Base;
use Kanboard\Core\Mail\ClientInterface;
/**
* Smtp
* PHP Mail Handler
*
* @package integration
* @package transport
* @author Frederic Guillot
*/
class Smtp extends \Kanboard\Core\Base
class Mail extends Base implements ClientInterface
{
/**
* Send a HTML email
@ -40,32 +40,18 @@ class Smtp extends \Kanboard\Core\Base
Swift_Mailer::newInstance($this->getTransport())->send($message);
}
catch (Swift_TransportException $e) {
$this->container['logger']->error($e->getMessage());
$this->logger->error($e->getMessage());
}
}
/**
* Get SwiftMailer transport
*
* @access private
* @access protected
* @return \Swift_Transport
*/
private function getTransport()
protected 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;
return Swift_MailTransport::newInstance();
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Kanboard\Core\Mail\Transport;
use Swift_SendmailTransport;
/**
* PHP Mail Handler
*
* @package transport
* @author Frederic Guillot
*/
class Sendmail extends Mail
{
/**
* Get SwiftMailer transport
*
* @access protected
* @return \Swift_Transport
*/
protected function getTransport()
{
return Swift_SendmailTransport::newInstance(MAIL_SENDMAIL_COMMAND);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Kanboard\Core\Mail\Transport;
use Swift_SmtpTransport;
/**
* PHP Mail Handler
*
* @package transport
* @author Frederic Guillot
*/
class Smtp extends Mail
{
/**
* Get SwiftMailer transport
*
* @access protected
* @return \Swift_Transport
*/
protected function getTransport()
{
$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);
return $transport;
}
}

View File

@ -66,7 +66,7 @@ abstract class Base extends \Kanboard\Core\Base
*/
public function getPluginName()
{
return ucfirst(substr(get_called_class(), 7, -7));
return ucfirst(substr(get_called_class(), 16, -7));
}
/**

View File

@ -1,95 +0,0 @@
<?php
namespace Kanboard\Integration;
use Kanboard\Core\Tool;
/**
* Mailgun Integration
*
* @package integration
* @author Frederic Guillot
*/
class Mailgun extends \Kanboard\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(
'Authorization: Basic '.base64_encode('api:'.MAILGUN_API_TOKEN)
);
$payload = array(
'from' => sprintf('%s <%s>', $author, MAIL_FROM),
'to' => sprintf('%s <%s>', $name, $email),
'subject' => $subject,
'html' => $html,
);
$this->httpClient->postForm('https://api.mailgun.net/v3/'.MAILGUN_DOMAIN.'/messages', $payload, $headers);
}
/**
* Parse incoming email
*
* @access public
* @param array $payload Incoming email
* @return boolean
*/
public function receiveEmail(array $payload)
{
if (empty($payload['sender']) || empty($payload['subject']) || empty($payload['recipient'])) {
return false;
}
// The user must exists in Kanboard
$user = $this->user->getByEmail($payload['sender']);
if (empty($user)) {
$this->container['logger']->debug('Mailgun: ignored => user not found');
return false;
}
// The project must have a short name
$project = $this->project->getByIdentifier(Tool::getMailboxHash($payload['recipient']));
if (empty($project)) {
$this->container['logger']->debug('Mailgun: 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('Mailgun: ignored => user is not member of the project');
return false;
}
// Get the Markdown contents
if (! empty($payload['stripped-html'])) {
$description = $this->htmlConverter->convert($payload['stripped-html']);
}
else if (! empty($payload['stripped-text'])) {
$description = $payload['stripped-text'];
}
else {
$description = '';
}
// Finally, we create the task
return (bool) $this->taskCreation->create(array(
'project_id' => $project['id'],
'title' => $payload['subject'],
'description' => $description,
'creator_id' => $user['id'],
));
}
}

View File

@ -1,94 +0,0 @@
<?php
namespace Kanboard\Integration;
/**
* Postmark integration
*
* @package integration
* @author Frederic Guillot
*/
class Postmark extends \Kanboard\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->postJson('https://api.postmarkapp.com/email', $payload, $headers);
}
/**
* Parse incoming email
*
* @access public
* @param array $payload Incoming email
* @return boolean
*/
public function receiveEmail(array $payload)
{
if (empty($payload['From']) || empty($payload['Subject']) || empty($payload['MailboxHash'])) {
return false;
}
// The user must exists in Kanboard
$user = $this->user->getByEmail($payload['From']);
if (empty($user)) {
$this->container['logger']->debug('Postmark: ignored => user not found');
return false;
}
// The project must have a short name
$project = $this->project->getByIdentifier($payload['MailboxHash']);
if (empty($project)) {
$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('Postmark: ignored => user is not member of the project');
return false;
}
// Get the Markdown contents
if (! empty($payload['HtmlBody'])) {
$description = $this->htmlConverter->convert($payload['HtmlBody']);
}
else if (! empty($payload['TextBody'])) {
$description = $payload['TextBody'];
}
else {
$description = '';
}
// Finally, we create the task
return (bool) $this->taskCreation->create(array(
'project_id' => $project['id'],
'title' => $payload['Subject'],
'description' => $description,
'creator_id' => $user['id'],
));
}
}

View File

@ -1,98 +0,0 @@
<?php
namespace Kanboard\Integration;
use Kanboard\Core\Tool;
/**
* Sendgrid Integration
*
* @package integration
* @author Frederic Guillot
*/
class Sendgrid extends \Kanboard\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)
{
$payload = array(
'api_user' => SENDGRID_API_USER,
'api_key' => SENDGRID_API_KEY,
'to' => $email,
'toname' => $name,
'from' => MAIL_FROM,
'fromname' => $author,
'html' => $html,
'subject' => $subject,
);
$this->httpClient->postForm('https://api.sendgrid.com/api/mail.send.json', $payload);
}
/**
* Parse incoming email
*
* @access public
* @param array $payload Incoming email
* @return boolean
*/
public function receiveEmail(array $payload)
{
if (empty($payload['envelope']) || empty($payload['subject'])) {
return false;
}
$envelope = json_decode($payload['envelope'], true);
$sender = isset($envelope['to'][0]) ? $envelope['to'][0] : '';
// The user must exists in Kanboard
$user = $this->user->getByEmail($envelope['from']);
if (empty($user)) {
$this->container['logger']->debug('SendgridWebhook: ignored => user not found');
return false;
}
// The project must have a short name
$project = $this->project->getByIdentifier(Tool::getMailboxHash($sender));
if (empty($project)) {
$this->container['logger']->debug('SendgridWebhook: 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('SendgridWebhook: ignored => user is not member of the project');
return false;
}
// Get the Markdown contents
if (! empty($payload['html'])) {
$description = $this->htmlConverter->convert($payload['html']);
}
else if (! empty($payload['text'])) {
$description = $payload['text'];
}
else {
$description = '';
}
// Finally, we create the task
return (bool) $this->taskCreation->create(array(
'project_id' => $project['id'],
'title' => $payload['subject'],
'description' => $description,
'creator_id' => $user['id'],
));
}
}

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Kopie',
'Project cloned successfully.' => 'Kopie projektu byla úspěšně vytvořena.',
'Unable to clone this project.' => 'Kopii projektu nelze vytvořit.',
'Email notifications' => 'Upozornění E-Mailem ',
'Enable email notifications' => 'Povolit upozornění pomocí e-mailů',
'Task position:' => 'Pořadí úkolu:',
'The task #%d have been opened.' => 'Úkol #%d byl znovu otevřen.',
@ -735,12 +734,6 @@ return array(
'SEK - Swedish Krona' => 'SEK - Schwedische Kronen',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'Identifikátor projektu je volitelný alfanumerický kód používaný k identifikaci vašeho projektu.',
'Identifier' => 'Identifikator',
'Postmark (incoming emails)' => 'Postmark (Eingehende E-Mails)',
'Help on Postmark integration' => 'Hilfe bei Postmark-Integration',
'Mailgun (incoming emails)' => 'Mailgun (Eingehende E-Mails)',
'Help on Mailgun integration' => 'Hilfe bei Mailgun-Integration',
'Sendgrid (incoming emails)' => 'Sendgrid (Eingehende E-Mails)',
'Help on Sendgrid integration' => 'Hilfe bei Sendgrid-Integration',
'Disable two factor authentication' => 'Zrušit dvou stupňovou autorizaci',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Willst du wirklich für folgenden Nutzer die Zwei-Faktor-Authentifizierung deaktivieren: "%s"?',
// 'Edit link' => '',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Kopier',
'Project cloned successfully.' => 'Projektet er kopieret.',
'Unable to clone this project.' => 'Projektet kunne ikke kopieres',
'Email notifications' => 'Email notifikationer',
'Enable email notifications' => 'Aktivér email notifikationer',
'Task position:' => 'Opgave position:',
'The task #%d have been opened.' => 'Opgaven #%d er blevet åbnet.',
@ -735,12 +734,6 @@ return array(
// 'SEK - Swedish Krona' => '',
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
// 'Edit link' => '',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'duplizieren',
'Project cloned successfully.' => 'Projekt wurde dupliziert.',
'Unable to clone this project.' => 'Duplizieren dieses Projekts schlug fehl.',
'Email notifications' => 'E-Mail-Benachrichtigungen',
'Enable email notifications' => 'E-Mail-Benachrichtigungen einschalten',
'Task position:' => 'Position der Aufgabe',
'The task #%d have been opened.' => 'Die Aufgabe #%d wurde geöffnet.',
@ -735,12 +734,6 @@ return array(
'SEK - Swedish Krona' => 'SEK - Schwedische Kronen',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'Der Projektidentifikator ist ein optionaler alphanumerischer Code, der das Projekt identifiziert.',
'Identifier' => 'Identifikator',
'Postmark (incoming emails)' => 'Postmark (Eingehende E-Mails)',
'Help on Postmark integration' => 'Hilfe bei Postmark-Integration',
'Mailgun (incoming emails)' => 'Mailgun (Eingehende E-Mails)',
'Help on Mailgun integration' => 'Hilfe bei Mailgun-Integration',
'Sendgrid (incoming emails)' => 'Sendgrid (Eingehende E-Mails)',
'Help on Sendgrid integration' => 'Hilfe bei Sendgrid-Integration',
'Disable two factor authentication' => 'Deaktiviere Zwei-Faktor-Authentifizierung',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Willst du wirklich für folgenden Nutzer die Zwei-Faktor-Authentifizierung deaktivieren: "%s"?',
'Edit link' => 'Verbindung bearbeiten',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Clonar',
'Project cloned successfully.' => 'Proyecto clonado correctamente',
'Unable to clone this project.' => 'Impsible clonar proyecto',
'Email notifications' => 'Notificaciones correo electrónico',
'Enable email notifications' => 'Habilitar notificaciones por correo electrónico',
'Task position:' => 'Posición de la tarea',
'The task #%d have been opened.' => 'La tarea #%d ha sido abierta',
@ -735,12 +734,6 @@ return array(
'SEK - Swedish Krona' => 'SEK - Corona sueca',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'El identificador del proyecto us un código opcional alfanumérico que se usa para identificar su proyecto.',
'Identifier' => 'Identificador',
'Postmark (incoming emails)' => 'Matasellos (emails entrantes)',
'Help on Postmark integration' => 'Ayuda sobre la integración de Matasellos',
'Mailgun (incoming emails)' => 'Mailgun (emails entrantes)',
'Help on Mailgun integration' => 'Ayuda sobre la integración con Mailgun',
'Sendgrid (incoming emails)' => 'Sendgrid (emails entrantes)',
'Help on Sendgrid integration' => 'Ayuda sobre la integración con Sendgrid',
'Disable two factor authentication' => 'Desactivar la autenticación de dos factores',
'Do you really want to disable the two factor authentication for this user: "%s"?' => '¿Realmentes quiere desactuvar la autenticación de dos factores para este usuario: "%s?"',
'Edit link' => 'Editar enlace',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Kahdenna',
'Project cloned successfully.' => 'Projekti kahdennettu onnistuneesti',
'Unable to clone this project.' => 'Projektin kahdennus epäonnistui',
'Email notifications' => 'Sähköposti-ilmoitukset',
'Enable email notifications' => 'Ota käyttöön sähköposti-ilmoitukset',
'Task position:' => 'Tehtävän sijainti',
'The task #%d have been opened.' => 'Tehtävä #%d on avattu',
@ -735,12 +734,6 @@ return array(
// 'SEK - Swedish Krona' => '',
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
// 'Edit link' => '',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Clone',
'Project cloned successfully.' => 'Projet cloné avec succès.',
'Unable to clone this project.' => 'Impossible de cloner ce projet.',
'Email notifications' => 'Notifications par email',
'Enable email notifications' => 'Activer les notifications par emails',
'Task position:' => 'Position de la tâche :',
'The task #%d have been opened.' => 'La tâche #%d a été ouverte.',
@ -737,12 +736,6 @@ return array(
'SEK - Swedish Krona' => 'SEK - Couronne suédoise',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'L\'identificateur du projet est un code alpha-numérique optionnel pour identifier votre projet.',
'Identifier' => 'Identificateur',
'Postmark (incoming emails)' => 'Postmark (emails entrants)',
'Help on Postmark integration' => 'Aide sur l\'intégration avec Postmark',
'Mailgun (incoming emails)' => 'Mailgun (emails entrants)',
'Help on Mailgun integration' => 'Aide sur l\'intégration avec Mailgun',
'Sendgrid (incoming emails)' => 'Sendgrid (emails entrants)',
'Help on Sendgrid integration' => 'Aide sur l\'intégration avec Sendgrid',
'Disable two factor authentication' => 'Désactiver l\'authentification à deux facteurs',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Voulez-vous vraiment désactiver l\'authentification à deux facteurs pour cet utilisateur : « %s » ?',
'Edit link' => 'Modifier un lien',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Másolat',
'Project cloned successfully.' => 'A projekt sikeresen másolva.',
'Unable to clone this project.' => 'A projekt másolása sikertelen.',
'Email notifications' => 'E-mail értesítések',
'Enable email notifications' => 'E-mail értesítések engedélyezése',
'Task position:' => 'Feladat helye:',
'The task #%d have been opened.' => 'Feladat #%d megnyitva.',
@ -735,12 +734,6 @@ return array(
// 'SEK - Swedish Krona' => '',
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
// 'Edit link' => '',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Klon',
'Project cloned successfully.' => 'Kloning proyek berhasil.',
'Unable to clone this project.' => 'Tidak dapat mengkloning proyek.',
'Email notifications' => 'Pemberitahuan email',
'Enable email notifications' => 'Aktifkan pemberitahuan dari email',
'Task position:' => 'Posisi tugas :',
'The task #%d have been opened.' => 'Tugas #%d telah dibuka.',
@ -735,12 +734,6 @@ return array(
'SEK - Swedish Krona' => 'SEK - Krona Swedia',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'Identifier proyek adalah kode alfanumerik opsional digunakan untuk mengidentifikasi proyek Anda.',
'Identifier' => 'Identifier',
'Postmark (incoming emails)' => 'Postmark (email masuk)',
'Help on Postmark integration' => 'Bantuan pada integrasi Postmark',
'Mailgun (incoming emails)' => 'Mailgun (email masuk)',
'Help on Mailgun integration' => 'Bantuan pada integrasi Mailgun',
'Sendgrid (incoming emails)' => 'Sendgrid (email masuk)',
'Help on Sendgrid integration' => 'Bantuan pada integrasi Sendgrid',
'Disable two factor authentication' => 'Matikan dua faktor otentifikasi',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Apakah anda yakin akan mematikan dua faktor otentifikasi untuk pengguna ini : « %s » ?',
'Edit link' => 'Modifikasi tautan',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Clona',
'Project cloned successfully.' => 'Progetto clonato con successo.',
'Unable to clone this project.' => 'Impossibile clonare questo progetto',
'Email notifications' => 'Notifiche email',
'Enable email notifications' => 'Abilita le notifiche via email',
'Task position:' => 'Posizione del compito:',
'The task #%d have been opened.' => 'Il compito #%d è stato aperto.',
@ -735,12 +734,6 @@ return array(
// 'SEK - Swedish Krona' => '',
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
// 'Edit link' => '',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => '複製',
'Project cloned successfully.' => 'プロジェクトを複製しました。',
'Unable to clone this project.' => 'プロジェクトの複製に失敗しました。',
'Email notifications' => 'メール通知',
'Enable email notifications' => 'メール通知を設定',
'Task position:' => 'タスクの位置:',
'The task #%d have been opened.' => 'タスク #%d をオープンしました。',
@ -735,12 +734,6 @@ return array(
// 'SEK - Swedish Krona' => '',
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
// 'Edit link' => '',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Kopier',
'Project cloned successfully.' => 'Prosjektet er kopiert.',
'Unable to clone this project.' => 'Prosjektet kunne ikke kopieres',
'Email notifications' => 'Epostvarslinger',
'Enable email notifications' => 'Aktiver epostvarslinger',
'Task position:' => 'Oppgaveposisjon:',
'The task #%d have been opened.' => 'Oppgaven #%d er åpnet.',
@ -730,17 +729,11 @@ return array(
// 'This chart show the task complexity over the time (Work Remaining).' => '',
// 'Screenshot taken %s' => '',
'Add a screenshot' => 'Legg til et skjermbilde',
'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => 'Ta et skjermbilde og trykk CTRL+V for å lime det inn her.',
// 'Take a screenshot and press CTRL+V or ⌘+V to paste here.' => '',
'Screenshot uploaded successfully.' => 'Skjermbilde opplastet',
// 'SEK - Swedish Krona' => '',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'Prosjektkoden er en alfanumerisk kode som kan brukes for å identifisere prosjektet',
'Identifier' => 'Prosjektkode',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
// 'Edit link' => '',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Kloon',
'Project cloned successfully.' => 'Project succesvol gekloond.',
'Unable to clone this project.' => 'Klonen van project niet gelukt.',
'Email notifications' => 'Email notificatie',
'Enable email notifications' => 'Email notificatie aanzetten',
'Task position:' => 'Taak positie :',
'The task #%d have been opened.' => 'Taak #%d is geopend.',
@ -735,12 +734,6 @@ return array(
// 'SEK - Swedish Krona' => '',
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
// 'Edit link' => '',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Sklonuj',
'Project cloned successfully.' => 'Projekt sklonowany pomyślnie.',
'Unable to clone this project.' => 'Nie można sklonować projektu.',
'Email notifications' => 'Powiadomienia email',
'Enable email notifications' => 'Włącz powiadomienia email',
'Task position:' => 'Pozycja zadania:',
'The task #%d have been opened.' => 'Zadania #%d zostały otwarte.',
@ -735,12 +734,6 @@ return array(
'SEK - Swedish Krona' => 'SEK - Korona szwedzka',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'Identyfikator projektu to opcjonalny kod alfanumeryczny do identyfikacji projektu.',
'Identifier' => 'Identyfikator',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
'Disable two factor authentication' => 'Wyłącz uwierzytelnianie dwuetapowe',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Czy na pewno chcesz wyłączyć uwierzytelnianie dwuetapowe dla tego użytkownika: "%s"?',
'Edit link' => 'Edytuj link',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Clonar',
'Project cloned successfully.' => 'Projeto clonado com sucesso.',
'Unable to clone this project.' => 'Não foi possível clonar este projeto.',
'Email notifications' => 'Notificações por email',
'Enable email notifications' => 'Habilitar notificações por email',
'Task position:' => 'Posição da tarefa:',
'The task #%d have been opened.' => 'A tarefa #%d foi aberta.',
@ -735,12 +734,6 @@ return array(
'SEK - Swedish Krona' => 'SEK - Coroa sueca',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'O identificador de projeto é um código alfanumérico opcional utilizado para identificar o seu projeto.',
'Identifier' => 'Identificador',
'Postmark (incoming emails)' => 'Postmark (e-mails recebidos)',
'Help on Postmark integration' => 'Ajuda na integração do Postmark',
'Mailgun (incoming emails)' => 'Mailgun (e-mails recebidos)',
'Help on Mailgun integration' => 'Ajuda na integração do Mailgun',
'Sendgrid (incoming emails)' => 'Sendgrid (e-mails recebidos)',
'Help on Sendgrid integration' => 'Ajuda na integração do Sendgrid',
'Disable two factor authentication' => 'Desativar autenticação à dois fatores',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Você deseja realmente desativar a autenticação à dois fatores para esse usuário: "%s"?',
'Edit link' => 'Editar um link',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Clonar',
'Project cloned successfully.' => 'Projecto clonado com sucesso.',
'Unable to clone this project.' => 'Não foi possível clonar este projecto.',
'Email notifications' => 'Notificações por email',
'Enable email notifications' => 'Activar notificações por email',
'Task position:' => 'Posição da tarefa:',
'The task #%d have been opened.' => 'A tarefa #%d foi aberta.',
@ -735,12 +734,6 @@ return array(
'SEK - Swedish Krona' => 'SEK - Coroa sueca',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'O identificador de projecto é um código alfanumérico opcional utilizado para identificar o seu projecto.',
'Identifier' => 'Identificador',
'Postmark (incoming emails)' => 'Postmark (e-mails recebidos)',
'Help on Postmark integration' => 'Ajuda na integração do Postmark',
'Mailgun (incoming emails)' => 'Mailgun (e-mails recebidos)',
'Help on Mailgun integration' => 'Ajuda na integração do Mailgun',
'Sendgrid (incoming emails)' => 'Sendgrid (e-mails recebidos)',
'Help on Sendgrid integration' => 'Ajuda na integração do Sendgrid',
'Disable two factor authentication' => 'Desactivar autenticação com dois factores',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Tem a certeza que quer desactivar a autenticação com dois factores para esse utilizador: "%s"?',
'Edit link' => 'Editar um link',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Клонировать',
'Project cloned successfully.' => 'Проект клонирован.',
'Unable to clone this project.' => 'Не удалось клонировать проект.',
'Email notifications' => 'Уведомления по e-mail',
'Enable email notifications' => 'Включить уведомления по e-mail',
'Task position:' => 'Позиция задачи:',
'The task #%d have been opened.' => 'Задача #%d была открыта.',
@ -735,12 +734,6 @@ return array(
'SEK - Swedish Krona' => 'SEK - Шведская крона',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'Идентификатор проекта - это опциональный буквенно-цифровой код использующийся для идентификации проекта',
'Identifier' => 'Идентификатор',
'Postmark (incoming emails)' => 'Postmark (входящие сообщения)',
'Help on Postmark integration' => 'Справка о Postmark интеграции',
'Mailgun (incoming emails)' => 'Mailgun (входящие сообщения)',
'Help on Mailgun integration' => 'Справка о Mailgun интеграции',
'Sendgrid (incoming emails)' => 'Sendgrid (входящие сообщения)',
'Help on Sendgrid integration' => 'Справка о Sendgrid интеграции',
'Disable two factor authentication' => 'Выключить двухфакторную авторизацию',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Вы действительно хотите выключить двухфакторную авторизацию для пользователя "%s"?',
'Edit link' => 'Редактировать ссылку',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Iskopiraj',
'Project cloned successfully.' => 'Projekat uspešno iskopiran.',
'Unable to clone this project.' => 'Nije moguće iskopirati projekat.',
'Email notifications' => 'Obaveštenje e-mailom',
'Enable email notifications' => 'Omogući obaveštenja e-mailom',
'Task position:' => 'Pozicija zadatka:',
'The task #%d have been opened.' => 'Zadatak #%d je otvoren.',
@ -735,12 +734,6 @@ return array(
// 'SEK - Swedish Krona' => '',
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
// 'Edit link' => '',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Klona',
'Project cloned successfully.' => 'Projektet har klonats.',
'Unable to clone this project.' => 'Kunde inte klona projektet.',
'Email notifications' => 'Epostnotiser',
'Enable email notifications' => 'Aktivera epostnotiser',
'Task position:' => 'Uppgiftsposition:',
'The task #%d have been opened.' => 'Uppgiften #%d har öppnats.',
@ -735,12 +734,6 @@ return array(
'SEK - Swedish Krona' => 'SEK - Svensk Krona',
'The project identifier is an optional alphanumeric code used to identify your project.' => 'Projektidentifieraren är en valbar alfanumerisk kod som används för att identifiera ditt projekt.',
'Identifier' => 'Identifierare',
'Postmark (incoming emails)' => 'Postmark (inkommande e-post)',
'Help on Postmark integration' => 'Hjälp för Postmark integration',
'Mailgun (incoming emails)' => 'Mailgrun (inkommande e-post)',
'Help on Mailgun integration' => 'Hjälp för Mailgrun integration',
'Sendgrid (incoming emails)' => 'Sendgrid (inkommande e-post)',
'Help on Sendgrid integration' => 'Hjälp för Sendgrid integration',
'Disable two factor authentication' => 'Inaktivera två-faktors autentisering',
'Do you really want to disable the two factor authentication for this user: "%s"?' => 'Vill du verkligen inaktivera två-faktors autentisering för denna användare: "%s"?',
'Edit link' => 'Ändra länk',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'เลียนแบบ',
'Project cloned successfully.' => 'เลียนแบบโปรเจคเรียบร้อยแล้ว',
'Unable to clone this project.' => 'ไม่สามารถเลียบแบบโปรเจคได้',
'Email notifications' => 'อีเมลแจ้งเตือน',
'Enable email notifications' => 'เปิดอีเมลแจ้งเตือน',
'Task position:' => 'ตำแหน่งงาน',
'The task #%d have been opened.' => 'งานที่ #%d ถุกเปิด',
@ -735,12 +734,6 @@ return array(
// 'SEK - Swedish Krona' => '',
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
'Edit link' => 'แก้ไขลิงค์',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => 'Kopya oluştur',
'Project cloned successfully.' => 'Proje kopyası başarıyla oluşturuldu.',
'Unable to clone this project.' => 'Proje kopyası oluşturulamadı.',
'Email notifications' => 'E-Posta bilgilendirmesi',
'Enable email notifications' => 'E-Posta bilgilendirmesini aç',
'Task position:' => 'Görev pozisyonu',
'The task #%d have been opened.' => '#%d numaralı görev açıldı.',
@ -735,12 +734,6 @@ return array(
// 'SEK - Swedish Krona' => '',
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
// 'Edit link' => '',

View File

@ -353,7 +353,6 @@ return array(
'Clone' => '克隆',
'Project cloned successfully.' => '成功复制项目。',
'Unable to clone this project.' => '无法复制此项目',
'Email notifications' => '邮件通知',
'Enable email notifications' => '启用邮件通知',
'Task position:' => '任务位置:',
'The task #%d have been opened.' => '任务#%d已经被开启.',
@ -735,12 +734,6 @@ return array(
// 'SEK - Swedish Krona' => '',
// 'The project identifier is an optional alphanumeric code used to identify your project.' => '',
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
// 'Sendgrid (incoming emails)' => '',
// 'Help on Sendgrid integration' => '',
// 'Disable two factor authentication' => '',
// 'Do you really want to disable the two factor authentication for this user: "%s"?' => '',
'Edit link' => '编辑链接',

View File

@ -3,6 +3,7 @@
namespace Kanboard\ServiceProvider;
use Kanboard\Core\Plugin\Loader;
use Kanboard\Core\Mail\Client as EmailClient;
use Kanboard\Core\ObjectStorage\FileStorage;
use Kanboard\Core\Paginator;
use Kanboard\Core\OAuth2;
@ -78,7 +79,6 @@ class ClassProvider implements ServiceProviderInterface
'ProjectGanttFormatter',
),
'Core' => array(
'EmailClient',
'Helper',
'HttpClient',
'Lexer',
@ -99,11 +99,7 @@ class ClassProvider implements ServiceProviderInterface
'GitlabWebhook',
'HipchatWebhook',
'Jabber',
'Mailgun',
'Postmark',
'Sendgrid',
'SlackWebhook',
'Smtp',
)
);
@ -127,6 +123,14 @@ class ClassProvider implements ServiceProviderInterface
return new FileStorage(FILES_DIR);
};
$container['emailClient'] = function($container) {
$mailer = new EmailClient($container);
$mailer->setTransport('smtp', '\Kanboard\Core\Mail\Transport\Smtp');
$mailer->setTransport('sendmail', '\Kanboard\Core\Mail\Transport\Sendmail');
$mailer->setTransport('mail', '\Kanboard\Core\Mail\Transport\Mail');
return $mailer;
};
$container['pluginLoader'] = new Loader($container);
$container['cspRules'] = array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '* data:');

View File

@ -6,6 +6,8 @@
<?= $this->form->csrf() ?>
<?= $this->hook->render('template:config:integrations', array('values' => $values)) ?>
<h3><i class="fa fa-google"></i> <?= t('Google Authentication') ?></h3>
<div class="listing">
<input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('oauth', 'google', array(), false, '', true) ?>"/><br/>
@ -24,24 +26,6 @@
<p class="form-help"><?= $this->url->doc(t('Help on Gitlab authentication'), 'gitlab-authentication') ?></p>
</div>
<h3><img src="<?= $this->url->dir() ?>assets/img/mailgun-icon.png"/>&nbsp;<?= t('Mailgun (incoming emails)') ?></h3>
<div class="listing">
<input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('webhook', 'mailgun', array('token' => $values['webhook_token']), false, '', true) ?>"/><br/>
<p class="form-help"><?= $this->url->doc(t('Help on Mailgun integration'), 'mailgun') ?></p>
</div>
<h3><img src="<?= $this->url->dir() ?>assets/img/sendgrid-icon.png"/>&nbsp;<?= t('Sendgrid (incoming emails)') ?></h3>
<div class="listing">
<input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('webhook', 'sendgrid', array('token' => $values['webhook_token']), false, '', true) ?>"/><br/>
<p class="form-help"><?= $this->url->doc(t('Help on Sendgrid integration'), 'sendgrid') ?></p>
</div>
<h3><img src="<?= $this->url->dir() ?>assets/img/postmark-icon.png"/>&nbsp;<?= t('Postmark (incoming emails)') ?></h3>
<div class="listing">
<input type="text" class="auto-select" readonly="readonly" value="<?= $this->url->href('webhook', 'postmark', array('token' => $values['webhook_token']), false, '', true) ?>"/><br/>
<p class="form-help"><?= $this->url->doc(t('Help on Postmark integration'), 'postmark') ?></p>
</div>
<h3><img src="<?= $this->url->dir() ?>assets/img/gravatar-icon.png"/>&nbsp;<?= t('Gravatar') ?></h3>
<div class="listing">
<?= $this->form->checkbox('integration_gravatar', t('Enable Gravatar images'), 1, $values['integration_gravatar'] == 1) ?>

View File

@ -82,11 +82,6 @@ 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', '');
defined('MAILGUN_API_TOKEN') or define('MAILGUN_API_TOKEN', '');
defined('MAILGUN_DOMAIN') or define('MAILGUN_DOMAIN', '');
defined('SENDGRID_API_USER') or define('SENDGRID_API_USER', '');
defined('SENDGRID_API_KEY') or define('SENDGRID_API_KEY', '');
// Enable or disable "Strict-Transport-Security" HTTP header
defined('ENABLE_HSTS') or define('ENABLE_HSTS', true);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 B

View File

@ -20,9 +20,7 @@ There are several email transports available:
- SMTP
- Sendmail
- PHP native mail function
- Mailgun
- Postmark
- Sendgrid
- Other methods can be provided by external plugins: Postmark, Sendgrid and Mailgun
Server settings
---------------
@ -77,63 +75,6 @@ This is the default configuration:
define('MAIL_TRANSPORT', 'mail');
```
### Mailgun HTTP API
You can use the HTTP API of Mailgun to send emails.
Configuration:
```php
// We choose "mailgun" as mail transport
define('MAIL_TRANSPORT', 'mailgun');
// Mailgun API key
define('MAILGUN_API_TOKEN', 'YOUR_API_KEY');
// Mailgun domain name
define('MAILGUN_DOMAIN', 'YOUR_DOMAIN_CONFIGURED_IN_MAILGUN');
// Be sure to use the sender email address configured in Mailgun
define('MAIL_FROM', 'sender-address-configured-in-mailgun@example.org');
```
### Postmark HTTP API
Postmark is a third-party email service.
If you already use the Postmark integration to receive emails in Kanboard you can use the same provider to send email too.
This system use their HTTP API instead of the SMTP protocol.
Here are the required settings for this configuration:
```php
// We choose "postmark" as mail transport
define('MAIL_TRANSPORT', 'postmark');
// Copy and paste your Postmark API token
define('POSTMARK_API_TOKEN', 'COPY HERE YOUR POSTMARK API TOKEN');
// Be sure to use the Postmark configured sender email address
define('MAIL_FROM', 'sender-address-configured-in-postmark@example.org');
```
### Sendgrid HTTP API
You can use the HTTP API of Sendgrid to send emails.
Configuration:
```php
// We choose "sendgrid" as mail transport
define('MAIL_TRANSPORT', 'sendgrid');
// Sendgrid username
define('SENDGRID_API_USER', 'YOUR_SENDGRID_USERNAME');
// Sendgrid password
define('SENDGRID_API_KEY', 'YOUR_SENDGRID_PASSWORD');
```
### The sender email address
By default, emails will use the sender address `notifications@kanboard.local`.

View File

@ -68,10 +68,7 @@ Using Kanboard
- [Gitlab webhooks](gitlab-webhooks.markdown)
- [Hipchat](hipchat.markdown)
- [Jabber](jabber.markdown)
- [Mailgun](mailgun.markdown)
- [Sendgrid](sendgrid.markdown)
- [Slack](slack.markdown)
- [Postmark](postmark.markdown)
- [iCalendar subscriptions](ical.markdown)
- [RSS/Atom subscriptions](rss.markdown)
- [Json-RPC API](api-json-rpc.markdown)

View File

@ -1,28 +0,0 @@
Mailgun
=======
You can use the service [Mailgun](http://www.mailgun.com/) to create tasks directly by email.
This integration works with the inbound email service of Mailgun (routes).
Kanboard use a webhook to handle incoming emails.
The [incoming email workflow is described here](create-tasks-by-email.markdown).
Mailgun configuration
---------------------
Create a new route in the web interface or via the API ([official documentation](https://documentation.mailgun.com/user_manual.html#routes)), here an example:
```
match_recipient("^kanboard\+(.*)@mydomain.tld$")
forward("https://mykanboard/?controller=webhook&action=mailgun&token=mytoken")
```
The Kanboard webhook url is displayed in **Settings > Integrations > Mailgun**
Kanboard configuration
----------------------
1. Be sure that your users have an email address in their profiles
2. Assign a project identifier to the desired projects: **Project settings > Edit**
3. Try to send an email to your project

148
doc/plugin-hooks.markdown Normal file
View File

@ -0,0 +1,148 @@
Plugin Hooks
============
Application Hooks
-----------------
Hooks can extend, replace, filter data or change the default behavior. Each hook is identified with a unique name, example: `controller:calendar:user:events`
### Listen on hook events
In your `initialize()` method you need to call the method `on()` of the class `Kanboard\Core\Plugin\Hook`:
```php
$this->hook->on('hook_name', $callable);
```
The first argument is the name of the hook and the second is a PHP callable.
### Hooks executed only one time
Some hooks can have only one listener:
#### model:subtask-time-tracking:calculate:time-spent
- Override time spent calculation when subtask timer is stopped
- Arguments:
- `$user_id` (integer)
- `$start` (DateTime)
- `$end` (DateTime)
#### model:subtask-time-tracking:calendar:events
- Override subtask time tracking events to display the calendar
- Arguments:
- `$user_id` (integer)
- `$events` (array)
- `$start` (string, ISO-8601 format)
- `$end` (string, ISO-8601 format)
### Merge hooks
"Merge hooks" act in the same way as the function `array_merge`. The hook callback must return an array. This array will be merged with the default one.
Example to add events in the user calendar:
```php
class Plugin extends Base
{
public function initialize()
{
$container = $this->container;
$this->hook->on('controller:calendar:user:events', function($user_id, $start, $end) use ($container) {
$model = new SubtaskForecast($container);
return $model->getCalendarEvents($user_id, $end); // Return new events
});
}
}
```
List of merge hooks:
#### controller:calendar:project:events
- Add more events to the project calendar
- Arguments:
- `$project_id` (integer)
- `$start` Calendar start date (string, ISO-8601 format)
- `$end` Calendar` end date (string, ISO-8601 format)
#### controller:calendar:user:events
- Add more events to the user calendar
- Arguments:
- `$user_id` (integer)
- `$start` Calendar start date (string, ISO-8601 format)
- `$end` Calendar end date (string, ISO-8601 format)
Asset Hooks
-----------
Asset hooks can be used to add easily a new stylesheet or a new javascript file in the layout. You can use this feature to create a theme and override all Kanboard default styles.
Example to add a new stylesheet:
```php
<?php
namespace Kanboard\Plugin\Css;
use Kanboard\Core\Plugin\Base;
class Plugin extends Base
{
public function initialize()
{
$this->hook->on('template:layout:css', 'plugins/Css/skin.css');
}
}
```
List of asset Hooks:
- `template:layout:css`
- `template:layout:js`
Template Hooks
--------------
Template hooks allow to add new content in existing templates.
Example to add new content in the dashboard sidebar:
```php
$this->template->hook->attach('template:dashboard:sidebar', 'myplugin:dashboard/sidebar');
```
This call is usually defined in the `initialize()` method.
The first argument is name of the hook and the second argument is the template name.
Template names prefixed with the plugin name and colon indicate the location of the template.
Example with `myplugin:dashboard/sidebar`:
- `myplugin` is the name of your plugin (lowercase)
- `dashboard/sidebar` is the template name
- On the filesystem, the plugin will be located here: `plugins\Myplugin\Template\dashboard\sidebar.php`
- Templates are written in pure PHP (don't forget to escape data)
Template name without prefix are core templates.
List of template hooks:
- `template:dashboard:sidebar`
- `template:config:sidebar`
- `template:config:integrations`
- `template:export:sidebar`
- `template:layout:head`
- `template:layout:top`
- `template:layout:bottom`
- `template:project:dropdown`
- `template:project-user:sidebar`
- `template:task:sidebar:information`
- `template:task:sidebar:actions`
- `template:user:sidebar:information`
- `template:user:sidebar:actions`
Other template hooks can be added if necessary, just ask on the issue tracker.

View File

@ -0,0 +1,36 @@
Plugin Overrides
================
Override HTTP Content Security Policy
-------------------------------------
If you would like to replace the default HTTP Content Security Policy header, you can use the method `setContentSecurityPolicy()`:
```php
<?php
namespace Kanboard\Plugin\Csp;
use Kanboard\Core\Plugin\Base;
class Plugin extends Base
{
public function initialize()
{
$this->setContentSecurityPolicy(array('script-src' => 'something'));
}
}
```
Template Overrides
------------------
Any templates defined in the core can be overrided. By example, you can redefine the default layout or change email notifications.
Example of template override:
```php
$this->template->setTemplateOverride('header', 'theme:layout/header');
```
The first argument is the original template name and the second argument the template to use as replacement.

View File

@ -0,0 +1,200 @@
Plugin Registration
===================
Directory structure
-------------------
Plugins are stored in the `plugins` subdirectory. An example of a plugin directory structure:
```bash
plugins
└── Budget <= Plugin name
├── Asset <= Javascript/CSS files
├── Controller
├── LICENSE <= Plugin license
├── Locale
│ ├── fr_FR
│   ├── it_IT
│   ├── ja_JP
│   └── zh_CN
├── Model
├── Plugin.php <= Plugin registration file
├── README.md
├── Schema <= Database migrations
├── Template
└── Test <= Unit tests
```
Only the registration file `Plugin.php` is required. Other folders are optionals.
The first letter of the plugin name must be capitalized.
Plugin Registration File
------------------------
Kanboard will scan the directory `plugins` and load automatically everything under this directory. The file `Plugin.php` is used to load and register the plugin.
Example of `Plugin.php` file (`plugins/Foobar/Plugin.php`):
```php
<?php
namespace Kanboard\Plugin\Foobar;
use Kanboard\Core\Plugin\Base;
class Plugin extends Plugin\Base
{
public function initialize()
{
$this->template->hook->attach('template:layout:head', 'theme:layout/head');
}
}
```
This file should contains a class `Plugin` defined under the namespace `Kanboard\Plugin\Yourplugin` and extends `Kanboard\Core\Plugin\Base`.
The only required method is `initialize()`. This method is called for each request when the plugin is loaded.
Plugin Methods
--------------
Available methods from `Kanboard\Core\Plugin\Base`:
- `initialize()`: Executed when the plugin is loaded
- `getClasses()`: Return all classes that should be stored in the dependency injection container
- `on($event, $callback)`: Listen on internal events
- `getPluginName()`: Should return plugin name
- `getPluginAuthor()`: Should return plugin author
- `getPluginVersion()`: Should return plugin version
- `getPluginDescription()`: Should return plugin description
- `getPluginHomepage()`: Should return plugin Homepage (link)
- `setContentSecurityPolicy(array $rules)`: Override default HTTP CSP rules
Your plugin registration class also inherit from `Kanboard\Core\Base`, that means you can access to all classes and methods of Kanboard easily.
This example will fetch the user #123:
```php
$this->user->getById(123);
```
Plugin Translations
-------------------
Plugin can be translated in the same way the rest of the application. You must load the translations yourself when the session is created:
```php
$this->on('session.bootstrap', function($container) {
Translator::load($container['config']->getCurrentLanguage(), __DIR__.'/Locale');
});
```
The translations must be stored in `plugins/Myplugin/Locale/xx_XX/translations.php`.
Dependency Injection Container
------------------------------
Kanboard use Pimple, a simple PHP Dependency Injection Container. However, Kanboard can register any class in the container easily.
Those classes are available everywhere in the application and only one instance is created.
Here an example to register your own models in the container:
```php
public function getClasses()
{
return array(
'Plugin\Budget\Model' => array(
'HourlyRate',
'Budget',
)
);
}
```
Now, if you use a class that extends from `Core\Base`, you can access directly to those class instance:
```php
$this->hourlyRate->remove(123);
$this->budget->getDailyBudgetBreakdown(456);
// It's the same thing as using the container:
$this->container['hourlyRate']->getAll();
```
Keys of the containers are unique across the application. If you override an existing class you will change the default behavior.
Event Listening
----------------
Kanboard use internal events and your plugin can listen and perform actions on these events.
```php
$this->on('session.bootstrap', function($container) {
// Do something
});
```
- The first argument is the event name
- The second argument is a PHP callable function (closure or class method)
Extend Automatic Actions
------------------------
To define a new automatic action with a plugin, you just need to call the method `extendActions()` from the class `Kanboard\Model\Action`, here an example:
```php
<?php
namespace Kanboard\Plugin\AutomaticAction;
use Kanboard\Core\Plugin\Base;
class Plugin extends Base
{
public function initialize()
{
$this->action->extendActions(
'\Kanboard\Plugin\AutomaticAction\Action\SendSlackMessage', // Use absolute namespace
t('Send a message to Slack when the task color change')
);
}
}
```
- The first argument of the method `extendActions()` is the action class with the complete namespace path. **The namespace path must starts with a backslash** otherwise Kanboard will not be able to load your class.
- The second argument is the description of your automatic action.
The automatic action class must inherits from the class `Kanboard\Action\Base` and implements all abstract methods:
- `getCompatibleEvents()`
- `getActionRequiredParameters()`
- `getEventRequiredParameters()`
- `doAction(array $data)`
- `hasRequiredCondition(array $data)`
For more details you should take a look to existing automatic actions or this [plugin example](https://github.com/kanboard/plugin-example-automatic-action).
Extend ACL
----------
Kanboard use an access list for privilege separations. Your extension can add new rules:
```php
$this->acl->extend('project_manager_acl', array('mycontroller' => '*'));
```
- The first argument is the ACL name
- The second argument are the new rules
+ Syntax to include only some actions: `array('controller' => array('action1', 'action2'))`
+ Syntax to include all actions of a controller: `array('controller' => '*')`
+ Everything is lowercase
List of ACL:
- `public_acl`: Public access without authentication
- `project_member_acl`: Project member access
- `project_manager_acl`: Project manager access
- `project_admin_acl`: Project Admins
- `admin_acl`: Administrators

View File

@ -0,0 +1,38 @@
Plugin Schema Migrations
========================
Kanboard execute database migrations automatically for you. Migrations must be stored in a folder **Schema** and the filename must be the same as the database driver:
```bash
Schema
├── Mysql.php
├── Postgres.php
└── Sqlite.php
```
Each file contains all migrations, here an example for Sqlite:
```php
<?php
namespace Kanboard\Plugin\Something\Schema;
const VERSION = 1;
function version_1($pdo)
{
$pdo->exec('CREATE TABLE IF NOT EXISTS something (
"id" INTEGER PRIMARY KEY,
"project_id" INTEGER NOT NULL,
"something" TEXT,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
)');
}
```
- The constant `VERSION` is the last version of your schema
- Each function is a migration `version_1()`, `version_2()`, etc...
- A `PDO` instance is passed as first argument
- Everything is executed inside a transaction, if something doesn't work a rollback is performed and the error is displayed to the user
Kanboard will compare the version defined in your schema and the version stored in the database. If the versions are different, Kanboard will execute one by one each migration until to reach the last version.

View File

@ -1,430 +1,24 @@
Plugin Development
==================
Note: The plugin API is considered alpha at the moment.
Note: The plugin API is **considered alpha** at the moment.
Plugins are useful to extend the core functionalities of Kanboard, adding features, creating themes or changing the default behavior.
Plugin creators should specify explicitly the compatible versions of Kanboard. Internal code of Kanboard may change over the time and your plugin must be tested with new versions.
Directory structure
-------------------
Plugins are stored in the `plugins` subdirectory. An example of a plugin directory structure:
```bash
plugins
└── Budget <= Plugin name
├── Asset <= Javascript/CSS files
├── Controller
├── LICENSE <= Plugin license
├── Locale
│ ├── fr_FR
│   ├── it_IT
│   ├── ja_JP
│   └── zh_CN
├── Model
├── Plugin.php <= Plugin registration file
├── README.md
├── Schema <= Database migrations
├── Template
└── Test <= Unit tests
```
Only the registration file `Plugin.php` is required. Other folders are optionals.
The first letter of the plugin name must be capitalized.
Plugin registration file
------------------------
Kanboard will scan the directory `plugins` and load automatically everything under this directory. The file `Plugin.php` is used to load and register the plugin.
Example of `Plugin.php` file (`plugins/Foobar/Plugin.php`):
```php
<?php
namespace Kanboard\Plugin\Foobar;
use Kanboard\Core\Plugin\Base;
class Plugin extends Plugin\Base
{
public function initialize()
{
$this->template->hook->attach('template:layout:head', 'theme:layout/head');
}
}
```
This file should contains a class `Plugin` defined under the namespace `Kanboard\Plugin\Yourplugin` and extends `Kanboard\Core\Plugin\Base`.
The only required method is `initialize()`. This method is called for each request when the plugin is loaded.
Plugin methods
--------------
Available methods from `Kanboard\Core\Plugin\Base`:
- `initialize()`: Executed when the plugin is loaded
- `getClasses()`: Return all classes that should be stored in the dependency injection container
- `on($event, $callback)`: Listen on internal events
- `getPluginName()`: Should return plugin name
- `getPluginAuthor()`: Should return plugin author
- `getPluginVersion()`: Should return plugin version
- `getPluginDescription()`: Should return plugin description
- `getPluginHomepage()`: Should return plugin Homepage (link)
Your plugin registration class also inherit from `Kanboard\Core\Base`, that means you can access to all classes and methods of Kanboard easily.
This example will fetch the user #123:
```php
$this->user->getById(123);
```
Application Hooks
-----------------
Hooks can extend, replace, filter data or change the default behavior. Each hook is identified with a unique name, example: `controller:calendar:user:events`
### Listen on hook events
In your `initialize()` method you need to call the method `on()` of the class `Kanboard\Core\Plugin\Hook`:
```php
$this->hook->on('hook_name', $callable);
```
The first argument is the name of the hook and the second is a PHP callable.
### Hooks executed only one time
Some hooks can have only one listener:
#### model:subtask-time-tracking:calculate:time-spent
- Override time spent calculation when subtask timer is stopped
- Arguments:
- `$user_id` (integer)
- `$start` (DateTime)
- `$end` (DateTime)
#### model:subtask-time-tracking:calendar:events
- Override subtask time tracking events to display the calendar
- Arguments:
- `$user_id` (integer)
- `$events` (array)
- `$start` (string, ISO-8601 format)
- `$end` (string, ISO-8601 format)
### Merge hooks
"Merge hooks" act in the same way as the function `array_merge`. The hook callback must return an array. This array will be merged with the default one.
Example to add events in the user calendar:
```php
class Plugin extends Base
{
public function initialize()
{
$container = $this->container;
$this->hook->on('controller:calendar:user:events', function($user_id, $start, $end) use ($container) {
$model = new SubtaskForecast($container);
return $model->getCalendarEvents($user_id, $end); // Return new events
});
}
}
```
List of merge hooks:
#### controller:calendar:project:events
- Add more events to the project calendar
- Arguments:
- `$project_id` (integer)
- `$start` Calendar start date (string, ISO-8601 format)
- `$end` Calendar` end date (string, ISO-8601 format)
#### controller:calendar:user:events
- Add more events to the user calendar
- Arguments:
- `$user_id` (integer)
- `$start` Calendar start date (string, ISO-8601 format)
- `$end` Calendar end date (string, ISO-8601 format)
Asset Hooks
-----------
Asset hooks can be used to add easily a new stylesheet or a new javascript file in the layout. You can use this feature to create a theme and override all Kanboard default styles.
Example to add a new stylesheet:
```php
<?php
namespace Kanboard\Plugin\Css;
use Kanboard\Core\Plugin\Base;
class Plugin extends Base
{
public function initialize()
{
$this->hook->on('template:layout:css', 'plugins/Css/skin.css');
}
}
```
List of asset Hooks:
- `template:layout:css`
- `template:layout:js`
Template hooks
--------------
Template hooks allow to add new content in existing templates.
Example to add new content in the dashboard sidebar:
```php
$this->template->hook->attach('template:dashboard:sidebar', 'myplugin:dashboard/sidebar');
```
This call is usually defined in the `initialize()` method.
The first argument is name of the hook and the second argument is the template name.
Template names prefixed with the plugin name and colon indicate the location of the template.
Example with `myplugin:dashboard/sidebar`:
- `myplugin` is the name of your plugin (lowercase)
- `dashboard/sidebar` is the template name
- On the filesystem, the plugin will be located here: `plugins\Myplugin\Template\dashboard\sidebar.php`
- Templates are written in pure PHP (don't forget to escape data)
Template name without prefix are core templates.
List of template hooks:
- `template:dashboard:sidebar`
- `template:config:sidebar`
- `template:export:sidebar`
- `template:layout:head`
- `template:layout:top`
- `template:layout:bottom`
- `template:project:dropdown`
- `template:project-user:sidebar`
- `template:task:sidebar:information`
- `template:task:sidebar:actions`
- `template:user:sidebar:information`
- `template:user:sidebar:actions`
Other template hooks can be added if necessary, just ask on the issue tracker.
Template overrides
------------------
Any templates defined in the core can be overrided. By example, you can redefine the default layout or change email notifications.
Example of template override:
```php
$this->template->setTemplateOverride('header', 'theme:layout/header');
```
The first argument is the original template name and the second argument the template to use as replacement.
Listen on events
----------------
Kanboard use internal events and your plugin can listen and perform actions on these events.
```php
$this->on('session.bootstrap', function($container) {
// Do something
});
```
- The first argument is the event name
- The second argument is a PHP callable function (closure or class method)
Extend Automatic Actions
------------------------
To define a new automatic action with a plugin, you just need to call the method `extendActions()` from the class `Model\Action`, here an example:
```php
<?php
namespace Kanboard\Plugin\AutomaticAction;
use Kanboard\Core\Plugin\Base;
class Plugin extends Base
{
public function initialize()
{
$this->action->extendActions(
'\Kanboard\Plugin\AutomaticAction\Action\SendSlackMessage', // Use absolute namespace
t('Send a message to Slack when the task color change')
);
}
}
```
- The first argument of the method `extendActions()` is the action class with the complete namespace path. **The namespace path must starts with a backslash** otherwise Kanboard will not be able to load your class.
- The second argument is the description of your automatic action.
The automatic action class must inherits from the class `Action\Base` and implements all abstract methods:
- `getCompatibleEvents()`
- `getActionRequiredParameters()`
- `getEventRequiredParameters()`
- `doAction(array $data)`
- `hasRequiredCondition(array $data)`
For more details you should take a look to existing automatic actions or this [plugin example](https://github.com/kanboard/plugin-example-automatic-action).
Extend ACL
----------
Kanboard use an access list for privilege separations. Your extension can add new rules:
```php
$this->acl->extend('project_manager_acl', array('mycontroller' => '*'));
```
- The first argument is the ACL name
- The second argument are the new rules
+ Syntax to include only some actions: `array('controller' => array('action1', 'action2'))`
+ Syntax to include all actions of a controller: `array('controller' => '*')`
+ Everything is lowercase
List of ACL:
- `public_acl`: Public access without authentication
- `project_member_acl`: Project member access
- `project_manager_acl`: Project manager access
- `project_admin_acl`: Project Admins
- `admin_acl`: Administrators
Plugin Translations
-------------------
Plugin can be translated in the same way the rest of the application. You must load the translations yourself when the session is created:
```php
$this->on('session.bootstrap', function($container) {
Translator::load($container['config']->getCurrentLanguage(), __DIR__.'/Locale');
});
```
The translations must be stored in `plugins/Myplugin/Locale/xx_XX/translations.php`.
Override HTTP Content Security Policy
-------------------------------------
If you would like to replace the default HTTP Content Security Policy header, you can use the method `setContentSecurityPolicy()`:
```php
<?php
namespace Kanboard\Plugin\Csp;
use Kanboard\Core\Plugin\Base;
class Plugin extends Base
{
public function initialize()
{
$this->setContentSecurityPolicy(array('script-src' => 'something'));
}
}
```
Dependency Injection Container
------------------------------
Kanboard use Pimple, a simple PHP Dependency Injection Container. However, Kanboard can register any class in the container easily.
Those classes are available everywhere in the application and only one instance is created.
Here an example to register your own models in the container:
```php
public function getClasses()
{
return array(
'Plugin\Budget\Model' => array(
'HourlyRate',
'Budget',
)
);
}
```
Now, if you use a class that extends from `Core\Base`, you can access directly to those class instance:
```php
$this->hourlyRate->remove(123);
$this->budget->getDailyBudgetBreakdown(456);
// It's the same thing as using the container:
$this->container['hourlyRate']->getAll();
```
Keys of the containers are unique across the application. If you override an existing class you will change the default behavior.
Schema migrations
-----------------
Kanboard execute database migrations automatically for you. Migrations must be stored in a folder **Schema** and the filename must be the same as the database driver:
```bash
Schema
├── Mysql.php
├── Postgres.php
└── Sqlite.php
```
Each file contains all migrations, here an example for Sqlite:
```php
<?php
namespace Kanboard\Plugin\Something\Schema;
const VERSION = 1;
function version_1($pdo)
{
$pdo->exec('CREATE TABLE IF NOT EXISTS something (
"id" INTEGER PRIMARY KEY,
"project_id" INTEGER NOT NULL,
"something" TEXT,
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
)');
}
```
- The constant `VERSION` is the last version of your schema
- Each function is a migration `version_1()`, `version_2()`, etc...
- A `PDO` instance is passed as first argument
- Everything is executed inside a transaction, if something doesn't work a rollback is performed and the error is displayed to the user
Kanboard will compare the version defined in your schema and the version stored in the database. If the versions are different, Kanboard will execute one by one each migration until to reach the last version.
- [Plugin Registration](plugin-registration.markdown)
- [Plugin Hooks](plugin-hooks.markdown)
- [Plugin Overrides](plugin-overrides.markdown)
- [Plugin Schema Migrations](plugin-schema-migrations.markdown)
Examples of plugins
-------------------
- [Sendgrid](https://github.com/kanboard/plugin-sendgrid)
- [Mailgun](https://github.com/kanboard/plugin-mailgun)
- [Postmark](https://github.com/kanboard/plugin-postmark)
- [Amazon S3](https://github.com/kanboard/plugin-s3)
- [Budget planning](https://github.com/kanboard/plugin-budget)
- [User timetable](https://github.com/kanboard/plugin-timetable)
- [Subtask Forecast](https://github.com/kanboard/plugin-subtask-forecast)

View File

@ -1,29 +0,0 @@
Postmark
========
You can use the service [Postmark](https://postmarkapp.com/) to create tasks directly by email.
This integration works with the inbound email service of Postmark.
Kanboard use a webhook to handle incoming emails.
The [incoming email workflow is described here](create-tasks-by-email.markdown).
Postmark configuration
----------------------
Just follow the [official documentation about inbound email processing](http://developer.postmarkapp.com/developer-process-configure.html).
Basically, you have to forward your own domain or subdomain to a specific Postmark email address.
The Kanboard webhook url is displayed in **Settings > Integrations > Postmark**
Kanboard configuration
----------------------
1. Be sure that your users have an email address in their profiles
2. Assign a project identifier to the desired projects: **Project settings > Edit**
3. Try to send an email to your project
Troubleshootings
----------------
- Test the webhook url from the Postmark console, you should have a status code `200 OK`

View File

@ -1,24 +0,0 @@
Sendgrid
========
You can use the service [Sendgrid](https://sendgrid.com/) to create tasks directly by email.
This integration works with the [Parse API of Sendgrid](https://sendgrid.com/docs/API_Reference/Webhooks/parse.html).
Kanboard use a webhook to handle incoming emails.
The [incoming email workflow is described here](create-tasks-by-email.markdown).
Sendgrid configuration
----------------------
1. Create a new domain or subdomain (by example **inbound.mydomain.tld**) with a MX record that point to **mx.sendgrid.net**
2. Add your domain and the Kanboard webhook url to [the configuration page in Sendgrid](https://sendgrid.com/developer/reply)
The Kanboard webhook url is displayed in **Settings > Integrations > Sendgrid**
Kanboard configuration
----------------------
1. Be sure that your users have an email address in their profiles
2. Assign a project identifier to the desired projects: **Project settings > Edit**
3. Try to send an email to your project

View File

@ -5,16 +5,24 @@ require __DIR__.'/app/common.php';
use Symfony\Component\Console\Application;
use Symfony\Component\EventDispatcher\Event;
use Kanboard\Console\TaskOverdueNotification;
use Kanboard\Console\SubtaskExport;
use Kanboard\Console\TaskExport;
use Kanboard\Console\ProjectDailyStatsCalculation;
use Kanboard\Console\ProjectDailyColumnStatsExport;
use Kanboard\Console\TransitionExport;
use Kanboard\Console\LocaleSync;
use Kanboard\Console\LocaleComparator;
$container['dispatcher']->dispatch('console.bootstrap', new Event);
$application = new Application('Kanboard', APP_VERSION);
$application->add(new Console\TaskOverdueNotification($container));
$application->add(new Console\SubtaskExport($container));
$application->add(new Console\TaskExport($container));
$application->add(new Console\ProjectDailyStatsCalculation($container));
$application->add(new Console\ProjectDailyColumnStatsExport($container));
$application->add(new Console\TransitionExport($container));
$application->add(new Console\LocaleSync($container));
$application->add(new Console\LocaleComparator($container));
$application->add(new TaskOverdueNotification($container));
$application->add(new SubtaskExport($container));
$application->add(new TaskExport($container));
$application->add(new ProjectDailyStatsCalculation($container));
$application->add(new ProjectDailyColumnStatsExport($container));
$application->add(new TransitionExport($container));
$application->add(new LocaleSync($container));
$application->add(new LocaleComparator($container));
$application->run();

View File

@ -1,71 +0,0 @@
<?php
require_once __DIR__.'/../Base.php';
use Kanboard\Integration\Mailgun;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Project;
use Kanboard\Model\ProjectPermission;
use Kanboard\Model\User;
class MailgunTest extends Base
{
public function testSendEmail()
{
$pm = new Mailgun($this->container);
$pm->sendEmail('test@localhost', 'Me', 'Test', 'Content', 'Bob');
$this->assertStringStartsWith('https://api.mailgun.net/v3/', $this->container['httpClient']->getUrl());
$data = $this->container['httpClient']->getData();
$this->assertArrayHasKey('from', $data);
$this->assertArrayHasKey('to', $data);
$this->assertArrayHasKey('subject', $data);
$this->assertArrayHasKey('html', $data);
$this->assertEquals('Me <test@localhost>', $data['to']);
$this->assertEquals('Bob <notifications@kanboard.local>', $data['from']);
$this->assertEquals('Test', $data['subject']);
$this->assertEquals('Content', $data['html']);
}
public function testHandlePayload()
{
$w = new Mailgun($this->container);
$p = new Project($this->container);
$pp = new ProjectPermission($this->container);
$u = new User($this->container);
$tc = new TaskCreation($this->container);
$tf = new TaskFinder($this->container);
$this->assertEquals(2, $u->create(array('username' => 'me', 'email' => 'me@localhost')));
$this->assertEquals(1, $p->create(array('name' => 'test1')));
$this->assertEquals(2, $p->create(array('name' => 'test2', 'identifier' => 'TEST1')));
// Empty payload
$this->assertFalse($w->receiveEmail(array()));
// Unknown user
$this->assertFalse($w->receiveEmail(array('sender' => 'a@b.c', 'subject' => 'Email task', 'recipient' => 'foobar', 'stripped-text' => 'boo')));
// Project not found
$this->assertFalse($w->receiveEmail(array('sender' => 'me@localhost', 'subject' => 'Email task', 'recipient' => 'foo+test@localhost', 'stripped-text' => 'boo')));
// User is not member
$this->assertFalse($w->receiveEmail(array('sender' => 'me@localhost', 'subject' => 'Email task', 'recipient' => 'foo+test1@localhost', 'stripped-text' => 'boo')));
$this->assertTrue($pp->addMember(2, 2));
// The task must be created
$this->assertTrue($w->receiveEmail(array('sender' => 'me@localhost', 'subject' => 'Email task', 'recipient' => 'foo+test1@localhost', 'stripped-text' => 'boo')));
$task = $tf->getById(1);
$this->assertNotEmpty($task);
$this->assertEquals(2, $task['project_id']);
$this->assertEquals('Email task', $task['title']);
$this->assertEquals('boo', $task['description']);
$this->assertEquals(2, $task['creator_id']);
}
}

View File

@ -1,106 +0,0 @@
<?php
require_once __DIR__.'/../Base.php';
use Kanboard\Integration\Postmark;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Project;
use Kanboard\Model\ProjectPermission;
use Kanboard\Model\User;
class PostmarkTest extends Base
{
public function testSendEmail()
{
$pm = new Postmark($this->container);
$pm->sendEmail('test@localhost', 'Me', 'Test', 'Content', 'Bob');
$this->assertEquals('https://api.postmarkapp.com/email', $this->container['httpClient']->getUrl());
$data = $this->container['httpClient']->getData();
$this->assertArrayHasKey('From', $data);
$this->assertArrayHasKey('To', $data);
$this->assertArrayHasKey('Subject', $data);
$this->assertArrayHasKey('HtmlBody', $data);
$this->assertEquals('Me <test@localhost>', $data['To']);
$this->assertEquals('Bob <notifications@kanboard.local>', $data['From']);
$this->assertEquals('Test', $data['Subject']);
$this->assertEquals('Content', $data['HtmlBody']);
$this->assertContains('Accept: application/json', $this->container['httpClient']->getHeaders());
$this->assertContains('X-Postmark-Server-Token: ', $this->container['httpClient']->getHeaders());
}
public function testHandlePayload()
{
$w = new Postmark($this->container);
$p = new Project($this->container);
$pp = new ProjectPermission($this->container);
$u = new User($this->container);
$tc = new TaskCreation($this->container);
$tf = new TaskFinder($this->container);
$this->assertEquals(2, $u->create(array('username' => 'me', 'email' => 'me@localhost')));
$this->assertEquals(1, $p->create(array('name' => 'test1')));
$this->assertEquals(2, $p->create(array('name' => 'test2', 'identifier' => 'TEST1')));
// Empty payload
$this->assertFalse($w->receiveEmail(array()));
// Unknown user
$this->assertFalse($w->receiveEmail(array('From' => 'a@b.c', 'Subject' => 'Email task', 'MailboxHash' => 'foobar', 'TextBody' => 'boo')));
// Project not found
$this->assertFalse($w->receiveEmail(array('From' => 'me@localhost', 'Subject' => 'Email task', 'MailboxHash' => 'test', 'TextBody' => 'boo')));
// User is not member
$this->assertFalse($w->receiveEmail(array('From' => 'me@localhost', 'Subject' => 'Email task', 'MailboxHash' => 'test1', 'TextBody' => 'boo')));
$this->assertTrue($pp->addMember(2, 2));
// The task must be created
$this->assertTrue($w->receiveEmail(array('From' => 'me@localhost', 'Subject' => 'Email task', 'MailboxHash' => 'test1', 'TextBody' => 'boo')));
$task = $tf->getById(1);
$this->assertNotEmpty($task);
$this->assertEquals(2, $task['project_id']);
$this->assertEquals('Email task', $task['title']);
$this->assertEquals('boo', $task['description']);
$this->assertEquals(2, $task['creator_id']);
}
public function testHtml2Markdown()
{
$w = new Postmark($this->container);
$p = new Project($this->container);
$pp = new ProjectPermission($this->container);
$u = new User($this->container);
$tc = new TaskCreation($this->container);
$tf = new TaskFinder($this->container);
$this->assertEquals(2, $u->create(array('username' => 'me', 'email' => 'me@localhost')));
$this->assertEquals(1, $p->create(array('name' => 'test2', 'identifier' => 'TEST1')));
$this->assertTrue($pp->addMember(1, 2));
$this->assertTrue($w->receiveEmail(array('From' => 'me@localhost', 'Subject' => 'Email task', 'MailboxHash' => 'test1', 'TextBody' => 'boo', 'HtmlBody' => '<p><strong>boo</strong></p>')));
$task = $tf->getById(1);
$this->assertNotEmpty($task);
$this->assertEquals(1, $task['project_id']);
$this->assertEquals('Email task', $task['title']);
$this->assertEquals('**boo**', $task['description']);
$this->assertEquals(2, $task['creator_id']);
$this->assertTrue($w->receiveEmail(array('From' => 'me@localhost', 'Subject' => 'Email task', 'MailboxHash' => 'test1', 'TextBody' => '**boo**', 'HtmlBody' => '')));
$task = $tf->getById(2);
$this->assertNotEmpty($task);
$this->assertEquals(1, $task['project_id']);
$this->assertEquals('Email task', $task['title']);
$this->assertEquals('**boo**', $task['description']);
$this->assertEquals(2, $task['creator_id']);
}
}

View File

@ -1,135 +0,0 @@
<?php
require_once __DIR__.'/../Base.php';
use Kanboard\Integration\Sendgrid;
use Kanboard\Model\TaskCreation;
use Kanboard\Model\TaskFinder;
use Kanboard\Model\Project;
use Kanboard\Model\ProjectPermission;
use Kanboard\Model\User;
class SendgridTest extends Base
{
public function testSendEmail()
{
$pm = new Sendgrid($this->container);
$pm->sendEmail('test@localhost', 'Me', 'Test', 'Content', 'Bob');
$this->assertEquals('https://api.sendgrid.com/api/mail.send.json', $this->container['httpClient']->getUrl());
$data = $this->container['httpClient']->getData();
$this->assertArrayHasKey('api_user', $data);
$this->assertArrayHasKey('api_key', $data);
$this->assertArrayHasKey('from', $data);
$this->assertArrayHasKey('fromname', $data);
$this->assertArrayHasKey('to', $data);
$this->assertArrayHasKey('toname', $data);
$this->assertArrayHasKey('subject', $data);
$this->assertArrayHasKey('html', $data);
$this->assertEquals('test@localhost', $data['to']);
$this->assertEquals('Me', $data['toname']);
$this->assertEquals('notifications@kanboard.local', $data['from']);
$this->assertEquals('Bob', $data['fromname']);
$this->assertEquals('Test', $data['subject']);
$this->assertEquals('Content', $data['html']);
$this->assertEquals('', $data['api_key']);
$this->assertEquals('', $data['api_user']);
}
public function testHandlePayload()
{
$w = new Sendgrid($this->container);
$p = new Project($this->container);
$pp = new ProjectPermission($this->container);
$u = new User($this->container);
$tc = new TaskCreation($this->container);
$tf = new TaskFinder($this->container);
$this->assertEquals(2, $u->create(array('username' => 'me', 'email' => 'me@localhost')));
$this->assertEquals(1, $p->create(array('name' => 'test1')));
$this->assertEquals(2, $p->create(array('name' => 'test2', 'identifier' => 'TEST1')));
// Empty payload
$this->assertFalse($w->receiveEmail(array()));
// Unknown user
$this->assertFalse($w->receiveEmail(array(
'envelope' => '{"to":["a@b.c"],"from":"a.b.c"}',
'subject' => 'Email task'
)));
// Project not found
$this->assertFalse($w->receiveEmail(array(
'envelope' => '{"to":["a@b.c"],"from":"me@localhost"}',
'subject' => 'Email task'
)));
// User is not member
$this->assertFalse($w->receiveEmail(array(
'envelope' => '{"to":["something+test1@localhost"],"from":"me@localhost"}',
'subject' => 'Email task'
)));
$this->assertTrue($pp->addMember(2, 2));
// The task must be created
$this->assertTrue($w->receiveEmail(array(
'envelope' => '{"to":["something+test1@localhost"],"from":"me@localhost"}',
'subject' => 'Email task'
)));
$task = $tf->getById(1);
$this->assertNotEmpty($task);
$this->assertEquals(2, $task['project_id']);
$this->assertEquals('Email task', $task['title']);
$this->assertEquals('', $task['description']);
$this->assertEquals(2, $task['creator_id']);
// Html content
$this->assertTrue($w->receiveEmail(array(
'envelope' => '{"to":["something+test1@localhost"],"from":"me@localhost"}',
'subject' => 'Email task',
'html' => '<strong>bold</strong> text',
)));
$task = $tf->getById(2);
$this->assertNotEmpty($task);
$this->assertEquals(2, $task['project_id']);
$this->assertEquals('Email task', $task['title']);
$this->assertEquals('**bold** text', $task['description']);
$this->assertEquals(2, $task['creator_id']);
// Text content
$this->assertTrue($w->receiveEmail(array(
'envelope' => '{"to":["something+test1@localhost"],"from":"me@localhost"}',
'subject' => 'Email task',
'text' => '**bold** text',
)));
$task = $tf->getById(3);
$this->assertNotEmpty($task);
$this->assertEquals(2, $task['project_id']);
$this->assertEquals('Email task', $task['title']);
$this->assertEquals('**bold** text', $task['description']);
$this->assertEquals(2, $task['creator_id']);
// Text + html content
$this->assertTrue($w->receiveEmail(array(
'envelope' => '{"to":["something+test1@localhost"],"from":"me@localhost"}',
'subject' => 'Email task',
'html' => '<strong>bold</strong> html',
'text' => '**bold** text',
)));
$task = $tf->getById(4);
$this->assertNotEmpty($task);
$this->assertEquals(2, $task['project_id']);
$this->assertEquals('Email task', $task['title']);
$this->assertEquals('**bold** html', $task['description']);
$this->assertEquals(2, $task['creator_id']);
}
}

View File

@ -74,7 +74,7 @@ class EmailNotificationTest extends Base
$this->assertTrue($u->update(array('id' => 1, 'email' => 'test@localhost')));
$this->container['emailClient'] = $this
->getMockBuilder('\Kanboard\Core\EmailClient')
->getMockBuilder('\Kanboard\Core\Mail\Client')
->setConstructorArgs(array($this->container))
->setMethods(array('send'))
->getMock();
@ -104,7 +104,7 @@ class EmailNotificationTest extends Base
$this->assertEquals(1, $tc->create(array('title' => 'test', 'project_id' => 1)));
$this->container['emailClient'] = $this
->getMockBuilder('\Kanboard\Core\EmailClient')
->getMockBuilder('\Kanboard\Core\Mail\Client')
->setConstructorArgs(array($this->container))
->setMethods(array('send'))
->getMock();