From f190be9e2d4d285fb71d84e5d3884206067cf7af Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 19 Apr 2015 19:23:42 -0400 Subject: Add Sendgrid integration (incoming email handling) --- README.markdown | 2 + app/Controller/Webhook.php | 14 +++++ app/Core/Tool.php | 20 ++++++ app/Integration/MailgunWebhook.php | 29 +++------ app/Integration/PostmarkWebhook.php | 11 ++-- app/Integration/SendgridWebhook.php | 74 +++++++++++++++++++++++ app/Locale/da_DK/translations.php | 2 + app/Locale/de_DE/translations.php | 2 + app/Locale/es_ES/translations.php | 2 + app/Locale/fi_FI/translations.php | 2 + app/Locale/fr_FR/translations.php | 2 + app/Locale/hu_HU/translations.php | 2 + app/Locale/it_IT/translations.php | 2 + app/Locale/ja_JP/translations.php | 2 + app/Locale/nl_NL/translations.php | 2 + app/Locale/pl_PL/translations.php | 2 + app/Locale/pt_BR/translations.php | 2 + app/Locale/ru_RU/translations.php | 2 + app/Locale/sr_Latn_RS/translations.php | 2 + app/Locale/sv_SE/translations.php | 2 + app/Locale/th_TH/translations.php | 2 + app/Locale/tr_TR/translations.php | 2 + app/Locale/zh_CN/translations.php | 2 + app/Model/User.php | 4 ++ app/ServiceProvider/ClassProvider.php | 1 + app/Template/config/integrations.php | 6 ++ assets/img/chosen-sprite.png | Bin assets/img/chosen-sprite@2x.png | Bin assets/img/sendgrid-icon.png | Bin 0 -> 600 bytes docs/create-tasks-by-email.markdown | 44 ++++++++++++++ docs/mailgun.markdown | 35 +---------- docs/postmark.markdown | 36 ++--------- docs/sendgrid.markdown | 24 ++++++++ tests/units/ProjectTest.php | 3 + tests/units/SendgridWebhookTest.php | 107 +++++++++++++++++++++++++++++++++ tests/units/ToolTest.php | 15 +++++ tests/units/UserTest.php | 10 +++ 37 files changed, 380 insertions(+), 89 deletions(-) create mode 100644 app/Integration/SendgridWebhook.php mode change 100755 => 100644 assets/img/chosen-sprite.png mode change 100755 => 100644 assets/img/chosen-sprite@2x.png create mode 100644 assets/img/sendgrid-icon.png create mode 100644 docs/create-tasks-by-email.markdown create mode 100644 docs/sendgrid.markdown create mode 100644 tests/units/SendgridWebhookTest.php create mode 100644 tests/units/ToolTest.php diff --git a/README.markdown b/README.markdown index e2f137ca..6a3136d0 100644 --- a/README.markdown +++ b/README.markdown @@ -84,6 +84,7 @@ Documentation - [Task links](docs/task-links.markdown) - [Transitions](docs/transitions.markdown) - [Time tracking](docs/time-tracking.markdown) +- [Create tasks by email](docs/create-tasks-by-email.markdown) #### Working with users @@ -108,6 +109,7 @@ Documentation - [Gitlab webhooks](docs/gitlab-webhooks.markdown) - [Hipchat](docs/hipchat.markdown) - [Mailgun](docs/mailgun.markdown) +- [Sendgrid](docs/sendgrid.markdown) - [Slack](docs/slack.markdown) - [Postmark](docs/postmark.markdown) diff --git a/app/Controller/Webhook.php b/app/Controller/Webhook.php index 06bfcd4e..667c5087 100644 --- a/app/Controller/Webhook.php +++ b/app/Controller/Webhook.php @@ -128,4 +128,18 @@ class Webhook extends Base echo $this->mailgunWebhook->parsePayload($_POST) ? 'PARSED' : 'IGNORED'; } + + /** + * Handle Sendgrid webhooks + * + * @access public + */ + public function sendgrid() + { + if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) { + $this->response->text('Not Authorized', 401); + } + + echo $this->sendgridWebhook->parsePayload($_POST) ? 'PARSED' : 'IGNORED'; + } } diff --git a/app/Core/Tool.php b/app/Core/Tool.php index ade99cad..84e42ba8 100644 --- a/app/Core/Tool.php +++ b/app/Core/Tool.php @@ -31,4 +31,24 @@ class Tool fclose($fp); } } + + /** + * Get the mailbox hash from an email address + * + * @static + * @access public + * @param string $email + * @return string + */ + public static function getMailboxHash($email) + { + if (! strpos($email, '@') || ! strpos($email, '+')) { + return ''; + } + + list($local_part,) = explode('@', $email); + list(,$identifier) = explode('+', $local_part); + + return $identifier; + } } diff --git a/app/Integration/MailgunWebhook.php b/app/Integration/MailgunWebhook.php index 17338faa..8e542513 100644 --- a/app/Integration/MailgunWebhook.php +++ b/app/Integration/MailgunWebhook.php @@ -3,6 +3,7 @@ namespace Integration; use HTML_To_Markdown; +use Core\Tool; /** * Mailgun Webhook @@ -21,7 +22,7 @@ class MailgunWebhook extends Base */ public function parsePayload(array $payload) { - if (empty($payload['sender']) || empty($payload['subject']) || empty($payload['recipient']) || empty($payload['stripped-text'])) { + if (empty($payload['sender']) || empty($payload['subject']) || empty($payload['recipient'])) { return false; } @@ -34,7 +35,7 @@ class MailgunWebhook extends Base } // The project must have a short name - $project = $this->project->getByIdentifier($this->getMailboxHash($payload['recipient'])); + $project = $this->project->getByIdentifier(Tool::getMailboxHash($payload['recipient'])); if (empty($project)) { $this->container['logger']->debug('MailgunWebhook: ignored => project not found'); @@ -48,12 +49,15 @@ class MailgunWebhook extends Base } // Get the Markdown contents - if (empty($payload['stripped-html'])) { + if (! empty($payload['stripped-html'])) { + $markdown = new HTML_To_Markdown($payload['stripped-html'], array('strip_tags' => true)); + $description = $markdown->output(); + } + else if (! empty($payload['stripped-text'])) { $description = $payload['stripped-text']; } else { - $markdown = new HTML_To_Markdown($payload['stripped-html'], array('strip_tags' => true)); - $description = $markdown->output(); + $description = ''; } // Finally, we create the task @@ -64,19 +68,4 @@ class MailgunWebhook extends Base 'creator_id' => $user['id'], )); } - - /** - * Get the project identifier - * - * @access public - * @param string $email - * @return string - */ - public function getMailboxHash($email) - { - list($local_part,) = explode('@', $email); - list(,$identifier) = explode('+', $local_part); - - return $identifier; - } } diff --git a/app/Integration/PostmarkWebhook.php b/app/Integration/PostmarkWebhook.php index 6387ba20..642955df 100644 --- a/app/Integration/PostmarkWebhook.php +++ b/app/Integration/PostmarkWebhook.php @@ -21,7 +21,7 @@ class PostmarkWebhook extends Base */ public function parsePayload(array $payload) { - if (empty($payload['From']) || empty($payload['Subject']) || empty($payload['MailboxHash']) || empty($payload['TextBody'])) { + if (empty($payload['From']) || empty($payload['Subject']) || empty($payload['MailboxHash'])) { return false; } @@ -48,12 +48,15 @@ class PostmarkWebhook extends Base } // Get the Markdown contents - if (empty($payload['HtmlBody'])) { + if (! empty($payload['HtmlBody'])) { + $markdown = new HTML_To_Markdown($payload['HtmlBody'], array('strip_tags' => true)); + $description = $markdown->output(); + } + else if (! empty($payload['TextBody'])) { $description = $payload['TextBody']; } else { - $markdown = new HTML_To_Markdown($payload['HtmlBody'], array('strip_tags' => true)); - $description = $markdown->output(); + $description = ''; } // Finally, we create the task diff --git a/app/Integration/SendgridWebhook.php b/app/Integration/SendgridWebhook.php new file mode 100644 index 00000000..142ed49f --- /dev/null +++ b/app/Integration/SendgridWebhook.php @@ -0,0 +1,74 @@ +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'])) { + $markdown = new HTML_To_Markdown($payload['html'], array('strip_tags' => true)); + $description = $markdown->output(); + } + 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'], + )); + } +} diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index cc380929..f1352b8d 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 20d45977..2fc01f3e 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index f1d344be..c150bfe5 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 728a5746..99ead93c 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index 54370f3a..709131a6 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -866,4 +866,6 @@ return array( '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', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index feca5d32..d2d0d6e9 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 8601af4f..c2882ffc 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 3a08da17..a35ff2e9 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 4a53bf48..06a7026a 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 519fb32b..c91d4ba1 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index 953f3a59..1e5dae59 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index dee35d70..53616d9a 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index c4bab8cb..fd5c4f5d 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 22286c36..7f765815 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index c8e253b6..f282beee 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index 5fb403e8..11cba833 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index 7470bae1..09df6d45 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -864,4 +864,6 @@ return array( // 'Help on Postmark integration' => '', // 'Mailgun (incoming emails)' => '', // 'Help on Mailgun integration' => '', + // 'Sendgrid (incoming emails)' => '', + // 'Help on Sendgrid integration' => '', ); diff --git a/app/Model/User.php b/app/Model/User.php index d9f174bd..be2b034b 100644 --- a/app/Model/User.php +++ b/app/Model/User.php @@ -150,6 +150,10 @@ class User extends Base */ public function getByEmail($email) { + if (empty($email)) { + return false; + } + return $this->db->table(self::TABLE)->eq('email', $email)->findOne(); } diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index de9de546..a64ac061 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -78,6 +78,7 @@ class ClassProvider implements ServiceProviderInterface 'BitbucketWebhook', 'Hipchat', 'MailgunWebhook', + 'SendgridWebhook', 'SlackWebhook', 'PostmarkWebhook', ) diff --git a/app/Template/config/integrations.php b/app/Template/config/integrations.php index a012b566..bf051cfc 100644 --- a/app/Template/config/integrations.php +++ b/app/Template/config/integrations.php @@ -12,6 +12,12 @@

+

 

+
+
+

+
+

 


diff --git a/assets/img/chosen-sprite.png b/assets/img/chosen-sprite.png old mode 100755 new mode 100644 diff --git a/assets/img/chosen-sprite@2x.png b/assets/img/chosen-sprite@2x.png old mode 100755 new mode 100644 diff --git a/assets/img/sendgrid-icon.png b/assets/img/sendgrid-icon.png new file mode 100644 index 00000000..4247e27b Binary files /dev/null and b/assets/img/sendgrid-icon.png differ diff --git a/docs/create-tasks-by-email.markdown b/docs/create-tasks-by-email.markdown new file mode 100644 index 00000000..46dae480 --- /dev/null +++ b/docs/create-tasks-by-email.markdown @@ -0,0 +1,44 @@ +Create tasks by email +===================== + +You can create tasks directly by sending an email. + +At the moment, Kanboard is integrated with 3 external services: + +- [Mailgun](http://kanboard.net/documentation/mailgun) +- [Sendgrid](http://kanboard.net/documentation/sendgrid) +- [Postmark](http://kanboard.net/documentation/postmark) + +These services handle incoming emails without having to configure any SMTP server. + +When an email is received, Kanboard receive the message on a specific end-point. +All complicated works are already handled by those services. + +Incoming emails workflow +------------------------ + +1. You send an email to a specific address, by example **something+myproject@inbound.mydomain.tld** +2. Your email is forwarded to the third-party SMTP servers +3. The SMTP provider call the Kanboard webhook with the email in JSON or multipart/form-data formats +4. Kanboard parse the received email and create the task to the right project + +Note: New tasks are automatically created in the first column. + +Email format +------------ + +- The local part of the email address must use the plus separator, by example **kanboard+project123** +- The string defined after the plus sign must match a project identifier, by example **project123** is the identifier of the project **Project 123** +- The email subject becomes the task title +- The email body becomes the task description (Markdown format) + +Incoming emails can be written in text or HTML formats. +**Kanboard is able to convert simple HTML emails to Markdown**. + +Security and requirements +------------------------- + +- The Kanboard webhook is protected by a random token +- The sender email address must match a Kanboard user +- The Kanboard project must have a unique identifier, by example **MYPROJECT** +- The Kanboard user must be member of the project diff --git a/docs/mailgun.markdown b/docs/mailgun.markdown index a1dbe41b..03b70cf9 100644 --- a/docs/mailgun.markdown +++ b/docs/mailgun.markdown @@ -6,34 +6,7 @@ You can use the service [Mailgun](http://www.mailgun.com/) to create tasks direc This integration works with the inbound email service of Mailgun (routes). Kanboard use a webhook to handle incoming emails. -Incoming emails workflow ------------------------- - -1. You send an email to a specific address, by example **something+myproject@inbound.mydomain.tld** -2. Your email is forwarded to Mailgun SMTP servers -3. Mailgun call the Kanboard webhook with the email in JSON format -4. Kanboard parse the received email and create the task to the right project - -Note: New tasks are automatically created in the first column. - -Email format ------------- - -- The local part of the email address must use the plus separator, by example **kanboard+project123** -- The string defined after the plus sign must match a project identifier, by example **project123** is the identifier of the project **Project 123** -- The email subject becomes the task title -- The email body becomes the task description (Markdown format) - -Incoming emails can be written in text or HTML formats. -**Kanboard is able to convert simple HTML emails to Markdown**. - -Security and requirements -------------------------- - -- The Kanboard webhook is protected by a random token -- The sender email address must match a Kanboard user -- The Kanboard project must have a unique identifier, by example **MYPROJECT** -- The Kanboard user must be member of the project +The [incoming email workflow is described here](http://kanboard.net/documentation/email-tasks). Mailgun configuration --------------------- @@ -53,9 +26,3 @@ 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 if your route match in the console -- Double-check requirements mentioned above diff --git a/docs/postmark.markdown b/docs/postmark.markdown index 681ec5a2..a859961c 100644 --- a/docs/postmark.markdown +++ b/docs/postmark.markdown @@ -6,40 +6,15 @@ You can use the service [Postmark](https://postmarkapp.com/) to create tasks dir This integration works with the inbound email service of Postmark. Kanboard use a webhook to handle incoming emails. -Incoming emails workflow ------------------------- - -1. You send an email to a specific address, by example **something+myproject@inbound.mydomain.tld** -2. Your email is forwarded to Postmark SMTP servers -3. Postmark call the Kanboard webhook with the email in JSON format -4. Kanboard parse the received email and create the task to the right project - -Note: New tasks are automatically created in the first column. - -Email format ------------- - -- The local part of the email address must use the plus separator, by example **kanboard+project123** -- The string defined after the plus sign must match a project identifier, by example **project123** is the identifier of the project **Project 123** -- The email subject becomes the task title -- The email body becomes the task description (Markdown format) - -Incoming emails can be written in text or HTML formats. -**Kanboard is able to convert simple HTML emails to Markdown**. - -Security and requirements -------------------------- - -- The Kanboard webhook is protected by a random token -- The sender email address (From header) must match a Kanboard user -- The Kanboard project must have a unique identifier, by example **MYPROJECT** -- The Kanboard user must be member of the project +The [incoming email workflow is described here](http://kanboard.net/documentation/email-tasks). Postmark configuration ---------------------- -- Follow the [official documentation about inbound email processing](http://developer.postmarkapp.com/developer-process-configure.html) -- The Kanboard webhook url is displayed in **Settings > Integrations > Postmark** +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 ---------------------- @@ -52,4 +27,3 @@ Troubleshootings ---------------- - Test the webhook url from the Postmark console, you should have a status code `200 OK` -- Double-check requirements mentioned above diff --git a/docs/sendgrid.markdown b/docs/sendgrid.markdown new file mode 100644 index 00000000..5c7bb835 --- /dev/null +++ b/docs/sendgrid.markdown @@ -0,0 +1,24 @@ +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](http://kanboard.net/documentation/email-tasks). + +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 diff --git a/tests/units/ProjectTest.php b/tests/units/ProjectTest.php index 231d403f..9f49af8a 100644 --- a/tests/units/ProjectTest.php +++ b/tests/units/ProjectTest.php @@ -231,6 +231,9 @@ class ProjectTest extends Base $this->assertNotEmpty($project); $this->assertEquals('TEST1', $project['identifier']); + $project = $p->getByIdentifier(''); + $this->assertFalse($project); + // Validation rules $r = $p->validateCreation(array('name' => 'test', 'identifier' => 'TEST1')); $this->assertFalse($r[0]); diff --git a/tests/units/SendgridWebhookTest.php b/tests/units/SendgridWebhookTest.php new file mode 100644 index 00000000..3b30d212 --- /dev/null +++ b/tests/units/SendgridWebhookTest.php @@ -0,0 +1,107 @@ +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('name' => '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->parsePayload(array())); + + // Unknown user + $this->assertFalse($w->parsePayload(array( + 'envelope' => '{"to":["a@b.c"],"from":"a.b.c"}', + 'subject' => 'Email task' + ))); + + // Project not found + $this->assertFalse($w->parsePayload(array( + 'envelope' => '{"to":["a@b.c"],"from":"me@localhost"}', + 'subject' => 'Email task' + ))); + + // User is not member + $this->assertFalse($w->parsePayload(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->parsePayload(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->parsePayload(array( + 'envelope' => '{"to":["something+test1@localhost"],"from":"me@localhost"}', + 'subject' => 'Email task', + 'html' => 'bold 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->parsePayload(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->parsePayload(array( + 'envelope' => '{"to":["something+test1@localhost"],"from":"me@localhost"}', + 'subject' => 'Email task', + 'html' => 'bold 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']); + } +} diff --git a/tests/units/ToolTest.php b/tests/units/ToolTest.php new file mode 100644 index 00000000..4a62fe3b --- /dev/null +++ b/tests/units/ToolTest.php @@ -0,0 +1,15 @@ +assertEquals('test1', Tool::getMailboxHash('a+test1@localhost')); + $this->assertEquals('', Tool::getMailboxHash('test1@localhost')); + $this->assertEquals('', Tool::getMailboxHash('test1')); + } +} diff --git a/tests/units/UserTest.php b/tests/units/UserTest.php index c4094ea2..a946621c 100644 --- a/tests/units/UserTest.php +++ b/tests/units/UserTest.php @@ -10,6 +10,16 @@ use Model\Project; class UserTest extends Base { + public function testGetByEmail() + { + $u = new User($this->container); + $this->assertNotFalse($u->create(array('username' => 'user1', 'password' => '123456', 'email' => 'user1@localhost'))); + $this->assertNotFalse($u->create(array('username' => 'user2', 'password' => '123456', 'email' => ''))); + + $this->assertNotEmpty($u->getByEmail('user1@localhost')); + $this->assertEmpty($u->getByEmail('')); + } + public function testPassword() { $password = 'test123'; -- cgit v1.2.3