handesk icon indicating copy to clipboard operation
handesk copied to clipboard

How to use webhooks from Mailgun for incoming emails?

Open marktopper opened this issue 7 years ago • 6 comments
trafficstars

Is it possible to use Mailgun incoming webhooks to trigger new tickets, comments and so on?

marktopper avatar Jun 25 '18 18:06 marktopper

It is not implemented but sounds like a really nice feature,

do you have any documentation on the payload mailchilmp would send to the webhook se we can implement it?

BadChoice avatar Jun 26 '18 15:06 BadChoice

I basically already did something, not the cleanest code. Just copied the other webhook and made it match the payload from Mailgun.

<?php

namespace App\Http\Controllers;

use App\Idea;
use App\User;
use App\Ticket;
use App\Attachment;
use Illuminate\Http\Request;
use App\Services\Pop3\IncomingMailCommentParser;

class MailWebhookController extends Controller
{
    protected $handlers = [
      'newIdeaEmailParser',
      'newCommentEmailParser',
      'newTicketEmailParser',
    ];

    public function store(Request $request)
    {
    	$self = $this;
        collect($this->handlers)->first(function ($handler) use ($request, $self) {
        	return $self->$handler($request);
        });
    }

    public function newIdeaEmailParser(Request $request)
    {
        if (! starts_with(strtolower($request->get('subject')), 'idea:')) {
            return false;
        }

        $from = $this->parseEmailNameAddresses($request->get('from'));
        $fromAddress = collect($from)->keys()->first();
        $fromName = collect($from)->first();

        $subject = ucfirst(trim(str_after(strtolower($request->get('subject')), 'idea:')));
        Idea::createAndNotify(['name' => $fromName, 'email' => fromAddress], $rquest->get('subject'), $request->get('body-plain'), null, ['email']);

        return true;
    }

    public function newCommentEmailParser(Request $request)
    {
    	if (! str_contains($request->get('body-html'), config('mail.fetch.replyAboveLine'))) {
    		return null;
    	}

        preg_match('~ticket-id:(\d+)(\.)~', $request->get('body-html'), $results);
        $ticket = Ticket::find((count($results) > 1) ? $results[1] : null);

        if (is_null($ticket)) {
        	return;
        }

        $fromEmail = collect($this->parseEmailNameAddresses($request->get('from')))->keys()->first();
        if ($fromEmail == $ticket->requester->email) {
            return null;
        }

        $user = User::where('email', $fromEmail)->first();

        $comment = $ticket->addComment($user, strstr($request->get('body-html'), config('mail.fetch.replyAboveLine'), true));
        //Attachment::storeAttachmentsFromEmail($message, $comment);

        return true;
    }

    public function newTicketEmailParser(Request $request)
    {
        $from = $this->parseEmailNameAddresses($request->get('from'));
        $fromAddress = collect($from)->keys()->first();
        $fromName = collect($from)->first();

        $ticket = Ticket::createAndNotify(['name' => $fromName, 'email' => $fromAddress], $request->get('subject'), $request->get('body-html'), ['email']);
        //Attachment::storeAttachmentsFromEmail($message, $ticket);

        return true;
    }

    protected function parseEmailNameAddresses($string)
    {
    	$emails = array();

		if(preg_match_all('/\s*"?([^><,"]+)"?\s*((?:<[^><,]+>)?)\s*/', $string, $matches, PREG_SET_ORDER) > 0)
		{
		    foreach($matches as $m)
		    {
		        if(! empty($m[2]))
		        {
		            $emails[trim($m[2], '<>')] = $m[1];
		        }
		        else
		        {
		            $emails[$m[1]] = '';
		        }
		    }
		}

		return $emails;
    }
}

However, they documentation is here: https://documentation.mailgun.com/en/latest/api-routes.html#examples

Payload:

Content-Type: multipart/alternative; boundary="001a114490d2c5be3d05433e6d03"
Date: Fri, 9 Dec 2016 13:04:51 -0600
From: Excited User <[email protected]>
Message-Id: <CABPem2N_Ucj3wRRZnLVpVF_fRjkTBXHZReZC3zY-hHsRa=T51g@samples.mailgun.com>
Mime-Version: 1.0
Subject: Message Routes
To: [email protected]
X-Envelope-From: <[email protected]>
X-Mailgun-Incoming: Yes
X-Originating-Ip: [2001:xxx:xxxx:xxx::beef:93]
body-html: <div dir="ltr">Testing Mailgun&#39;s forwarded and stored message routes :)</div>
body-plain: Testing Mailgun's forwarded and stored message routes :)
domain: sandboxdb91ab935a414789809f96c91229a0ee.mailgun.org
from: Excited User <[email protected]>
message-headers: [["X-Mailgun-Incoming", "Yes"], ["X-Envelope-From", "<[email protected]>"], ["Mime-Version", "1.0"], ["X-Originating-Ip", "[2001:xxx:xxxx:xxx::beef:93]"], ["From", "Excited User <[email protected]>"], ["Date", "Fri, 9 Dec 2016 13:04:51 -0600"], ["Message-Id", "<CABPem2N_Ucj3wRRZnLVpVF_fRjkTBXHZReZC3zY-hHsRa=T51g@samples.mailgun.com>"], ["Subject", "Message Routes"], ["To", "[email protected]"], ["Content-Type", "multipart/alternative; boundary=\"001a114490d2c5be3d05433e6d03\""]]
message-url: https://si.api.mailgun.net/v3/domains/sandboxdb91ab935a414789809f96c91229a0ee.mailgun.org/messages/eyJwIjpmYWxzZSwiayI6IjFlOTZmNTkyLTAyOWItNDJkYi1iNjM5LTgzNTgwYzMxYjNhOCIsInMiOiIyMmNkYTRkZWFhIiwiYyI6InNhaWFkIn0=
recipient: [email protected]
sender: [email protected]
signature: 6ed72df4b5f00af436fff03730dc8bda31bf5800fdf431d1da5c0009a639d57e
stripped-html: <div dir="ltr">Testing Mailgun&#39;s forwarded and stored message routes :)</div>
stripped-signature:
stripped-text: Testing Mailgun's forwarded and stored message routes :)
subject: Message Routes
timestamp: 1481310293
token: f2a24f20007696fb23fd66ff0f59f17fac3f885324caaaec50

marktopper avatar Jun 27 '18 07:06 marktopper

It looks pretty well, But I believe we can really simply it by just using the handlers we already have

The thing is that we already have the app/Jobs/EmailParsers and we already have the NewCommentEmailParser, NewIdeaEmailParser and newTicketEmailParser, furthermore we already have the app\Jobs\ParseNewEmails job that it fetches the emails from the mail and parses them all using the above parsers

If you dig there a bit more, you can see a function called processMessage($message) that I believe it is what we should use

So I would suggest that the webhook controller, basically maps the incoming payload, into a $message (I believe its is an IncomingEmail

Maybe we can create a new MailChimpIncomingEmail that from the payload, is able to return the required fields/params, we basically use message->fromName, message->fromAddress, message->subject and message->body()

Then, to make it able to use the ParseNewEmails functionality, I believe we could make the processMessage public and the new webhook controlelr should look something like this

class MailWebhookController extends Controller
{
    public function store()
    {
        $message = new MailChimpEmail(request());
        (new ParseNewEmails)->processMessage($message);
    }
}

It this make sense to you, you could send me a PR

I suggest you create a simple test with a basic payload so it will be a lot easier and faster tot test and verify it all is working,

Let me know if you have any question

BadChoice avatar Jun 27 '18 07:06 BadChoice

BTW checkout the BitbucketWebhookTest for a payload test example

BadChoice avatar Jun 27 '18 07:06 BadChoice

@BadChoice note that @marktopper was talking about Mailgun, but your replies are all talking about MailChimp. They are 2 different services, with different webhook payloads. ;)

(That said, technically your reply is mostly suitable in both cases. But if you're considering baking any of this into the app, I just wanted to be sure you were noting the difference.)

drbyte avatar Jun 29 '18 13:06 drbyte

Thank you for the clarification, you are right, I was confused hehe, but as you say, the idea should be valid for both and we could have a IncomingMailgunEmail and IncomingMailchimpEmail to map the payloads

BadChoice avatar Jun 29 '18 19:06 BadChoice