diff options
37 files changed, 380 insertions, 89 deletions
| 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 @@ +<?php + +namespace Integration; + +use HTML_To_Markdown; +use Core\Tool; + +/** + * Sendgrid Webhook + * + * @package  integration + * @author   Frederic Guillot + */ +class SendgridWebhook extends Base +{ +    /** +     * Parse incoming email +     * +     * @access public +     * @param  array   $payload   Incoming email +     * @return boolean +     */ +    public function parsePayload(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'])) { +            $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 @@      <p class="form-help"><a href="http://kanboard.net/documentation/mailgun" target="_blank"><?= t('Help on Mailgun integration') ?></a></p>      </div> +    <h3><img src="assets/img/sendgrid-icon.png"/> <?= t('Sendgrid (incoming emails)') ?></h3> +    <div class="listing"> +    <input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'sendgrid', array('token' => $values['webhook_token'])) ?>"/><br/> +    <p class="form-help"><a href="http://kanboard.net/documentation/sendgrid" target="_blank"><?= t('Help on Sendgrid integration') ?></a></p> +    </div> +      <h3><img src="assets/img/postmark-icon.png"/> <?= t('Postmark (incoming emails)') ?></h3>      <div class="listing">      <input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'postmark', array('token' => $values['webhook_token'])) ?>"/><br/> diff --git a/assets/img/chosen-sprite.png b/assets/img/chosen-sprite.pngBinary files differ index 3611ae4a..3611ae4a 100755..100644 --- a/assets/img/chosen-sprite.png +++ b/assets/img/chosen-sprite.png diff --git a/assets/img/chosen-sprite@2x.png b/assets/img/chosen-sprite@2x.pngBinary files differ index ffe4d7d1..ffe4d7d1 100755..100644 --- a/assets/img/chosen-sprite@2x.png +++ b/assets/img/chosen-sprite@2x.png diff --git a/assets/img/sendgrid-icon.png b/assets/img/sendgrid-icon.pngBinary files differ new file mode 100644 index 00000000..4247e27b --- /dev/null +++ b/assets/img/sendgrid-icon.png 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 @@ +<?php + +require_once __DIR__.'/Base.php'; + +use Integration\SendgridWebhook; +use Model\TaskCreation; +use Model\TaskFinder; +use Model\Project; +use Model\ProjectPermission; +use Model\User; + +class SendgridWebhookTest extends Base +{ +    public function testHandlePayload() +    { +        $w = new SendgridWebhook($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('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' => '<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->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' => '<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']); +    } +} 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 @@ +<?php + +require_once __DIR__.'/Base.php'; + +use Core\Tool; + +class ToolTest extends Base +{ +    public function testMailboxHash() +    { +        $this->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'; | 
