Add Mailgun integration (incoming emails)

This commit is contained in:
Frederic Guillot 2015-04-19 16:01:41 -04:00
parent 392133d9ba
commit ac86c3100a
27 changed files with 255 additions and 3 deletions

View File

@ -107,6 +107,7 @@ Documentation
- [Github webhooks](docs/github-webhooks.markdown)
- [Gitlab webhooks](docs/gitlab-webhooks.markdown)
- [Hipchat](docs/hipchat.markdown)
- [Mailgun](docs/mailgun.markdown)
- [Slack](docs/slack.markdown)
- [Postmark](docs/postmark.markdown)

View File

@ -112,8 +112,20 @@ class Webhook extends Base
$this->response->text('Not Authorized', 401);
}
$result = $this->postmarkWebhook->parsePayload($this->request->getJson() ?: array());
echo $this->postmarkWebhook->parsePayload($this->request->getJson() ?: array()) ? 'PARSED' : 'IGNORED';
}
echo $result ? 'PARSED' : 'IGNORED';
/**
* Handle Mailgun webhooks
*
* @access public
*/
public function mailgun()
{
if ($this->config->get('webhook_token') !== $this->request->getStringParam('token')) {
$this->response->text('Not Authorized', 401);
}
echo $this->mailgunWebhook->parsePayload($_POST) ? 'PARSED' : 'IGNORED';
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Integration;
use HTML_To_Markdown;
/**
* Mailgun Webhook
*
* @package integration
* @author Frederic Guillot
*/
class MailgunWebhook extends Base
{
/**
* Parse incoming email
*
* @access public
* @param array $payload Incoming email
* @return boolean
*/
public function parsePayload(array $payload)
{
if (empty($payload['sender']) || empty($payload['subject']) || empty($payload['recipient']) || empty($payload['stripped-text'])) {
return false;
}
// The user must exists in Kanboard
$user = $this->user->getByEmail($payload['sender']);
if (empty($user)) {
$this->container['logger']->debug('MailgunWebhook: ignored => user not found');
return false;
}
// The project must have a short name
$project = $this->project->getByIdentifier($this->getMailboxHash($payload['recipient']));
if (empty($project)) {
$this->container['logger']->debug('MailgunWebhook: 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('MailgunWebhook: ignored => user is not member of the project');
return false;
}
// Get the Markdown contents
if (empty($payload['stripped-html'])) {
$description = $payload['stripped-text'];
}
else {
$markdown = new HTML_To_Markdown($payload['stripped-html'], array('strip_tags' => true));
$description = $markdown->output();
}
// Finally, we create the task
return (bool) $this->taskCreation->create(array(
'project_id' => $project['id'],
'title' => $payload['subject'],
'description' => $description,
'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;
}
}

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -864,4 +864,6 @@ return array(
'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',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -862,4 +862,6 @@ return array(
// 'Identifier' => '',
// 'Postmark (incoming emails)' => '',
// 'Help on Postmark integration' => '',
// 'Mailgun (incoming emails)' => '',
// 'Help on Mailgun integration' => '',
);

View File

@ -68,6 +68,10 @@ class Project extends Base
*/
public function getByIdentifier($identifier)
{
if (empty($identifier)) {
return false;
}
return $this->db->table(self::TABLE)->eq('identifier', strtoupper($identifier))->findOne();
}

View File

@ -77,6 +77,7 @@ class ClassProvider implements ServiceProviderInterface
'GithubWebhook',
'BitbucketWebhook',
'Hipchat',
'MailgunWebhook',
'SlackWebhook',
'PostmarkWebhook',
)

View File

@ -6,6 +6,12 @@
<?= $this->formCsrf() ?>
<h3><img src="assets/img/mailgun-icon.png"/>&nbsp;<?= t('Mailgun (incoming emails)') ?></h3>
<div class="listing">
<input type="text" class="auto-select" readonly="readonly" value="<?= $this->getCurrentBaseUrl().$this->u('webhook', 'mailgun', array('token' => $values['webhook_token'])) ?>"/><br/>
<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/postmark-icon.png"/>&nbsp;<?= 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/>

BIN
assets/img/mailgun-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

61
docs/mailgun.markdown Normal file
View File

@ -0,0 +1,61 @@
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.
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
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
Troubleshootings
----------------
- Test if your route match in the console
- Double-check requirements mentioned above

View File

@ -21,7 +21,7 @@ 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 subject
- 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.

View File

@ -0,0 +1,51 @@
<?php
require_once __DIR__.'/Base.php';
use Integration\MailgunWebhook;
use Model\TaskCreation;
use Model\TaskFinder;
use Model\Project;
use Model\ProjectPermission;
use Model\User;
class MailgunWebhookTest extends Base
{
public function testHandlePayload()
{
$w = new MailgunWebhook($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('sender' => 'a@b.c', 'subject' => 'Email task', 'recipient' => 'foobar', 'stripped-text' => 'boo')));
// Project not found
$this->assertFalse($w->parsePayload(array('sender' => 'me@localhost', 'subject' => 'Email task', 'recipient' => 'foo+test@localhost', 'stripped-text' => 'boo')));
// User is not member
$this->assertFalse($w->parsePayload(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->parsePayload(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']);
}
}