Laravel 中的入站邮件
发布于 作者 Paul Redmond
我最近需要能够接收电子邮件并在这些电子邮件上处理附件。我喜欢用 Mailgun 发送事务性邮件,所以当我需要处理入站邮件时,我开始更深入地研究 Mailgun,并意识到他们强大的 入站路由 邮件功能!
一起学习如何在你的 Laravel 应用程序中设置一个 Webhook 来处理入站邮件并保护它。我们甚至会使用 Laravel Valet (或直接使用 ngrok)在本地测试它!
什么是入站路由?
如果你曾经不得不处理将电子邮件导向 Web 应用程序,你就会知道,自己动手构建的解决方案会多么痛苦、容易出错以及维护成本高。
Mailgun 消除了处理入站邮件、解析邮件和调度邮件的痛苦。使用它帮助我找到了自动化入站邮件的巧妙方法。
想象一下,如果你正在构建一个问题系统,你可以轻松地解析电子邮件并将其与系统中的工单关联起来。你的用户可以直接回复支持邮件,而不必登录应用程序才能沟通问题。Mailgun 会解析电子邮件正文和附件,因此你可以跳过保存电子邮件签名并保存附件到工单。GitHub 问题邮件是一个很好的入站邮件处理示例,它允许你从电子邮件中发送消息到 GitHub 问题。
Mailgun 入站路由在解析电子邮件消息并以 JSON 格式将其传递给你的 Webhook 方面做得非常出色。JSON 负载包括附件、签名、电子邮件正文(作为剥离的纯文本值)以及许多其他内容。入站引擎允许你将入站电子邮件通过各种规则进行管道传输。
设置 Mailgun
Mailgun 提供了慷慨的免费层,让你有空间尝试入站邮件。如果你想跟随我一起操作,你需要有一个 Mailgun 帐户,并且为入站邮件配置了 DNS。你还需要 设置 MX DNS 记录 才能接收入站邮件。
虽然我不会引导你为你的域名设置 Mailgun,但我将引导你设置一个示例 Webhook,然后在本地运行它。
当我集成 Mailgun 入站路由时,我关注的一个领域是能够在本地进行实验和试用。不得不部署到生产环境才能进行测试会很麻烦而且很烦人。幸运的是,Laravel Valet 使在本地测试 Webhook 变得轻而易举;我对 Valet(实际上是 ngrok)的使用之容易感到震惊,它使我能够在我的本地机器上端到端地完成这个过程。
如果你没有使用 Valet,你也可以直接使用 ngrok,我将引导你完成此过程。
创建项目
想象一下,你的客户正在通过电子邮件发送小部件订单,你的工作是设置一个 Webhook 来接收这些订单并进行处理。他们可以发送附件,你也要处理这些附件。
让我们创建一个新的 Laravel 项目,并逐步介绍如何为小部件设置一个安全的 Webhook。你可以使用 Laravel 安装程序 或 Composer 创建项目。
# Valet$ laravel new mgexample # Composer$ composer create-project laravel/laravel:5.4 mgexample # Link Valet$ cd mgexample$ valet link
要使用 Mailgun 作为电子邮件提供商,你需要更新 MAIL_DRIVER
并添加来自你帐户的 Mailgun 域名和密钥值。
MAIL_DRIVER=mailgunMAILGUN_DOMAIN=mg.example.comMAILGUN_SECRET=horizon
设置 MAIL_DRIVER
并不是处理入站邮件的必要条件。Mailgun 配置位于 config/services.php
中。
这就是设置的全部内容。现在该编写一个 Webhook 路由并快速尝试一下了!
Webhook 路由
你可以配置入站规则以临时存储电子邮件消息(最多 3 天)并通知 Webhook。我们将在 routes/api.php
文件中定义一个,以定义一个无状态的 API 路由。该路由最终将类似于 POST http://example.com/api/mailgun/widgets
。你可以随意命名路由和控制器。
首先,让我们为此路由创建一个控制器。
php artisan make:controller MailgunWidgetsController
接下来,让我们在 routes/api.php
中定义路由。
Route::group([ 'prefix' => 'mailgun',],function () { Route::post('widgets', 'MailgunWidgetsController@store');});
我正在使用路由组,因为我喜欢将我的 Mailgun 路由集中在一个地方。每个入站路由最终都会使用一个 Webhook,因此将它们分组可以轻松地应用安全的 Webhook。
现在,我们只记录请求,这样我们就可以查看 Mailgun 在 Webhook 负载中发送的内容。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class MailgunWidgetsController extends Controller{ public function store() { app('log')->debug(request()->all()); return response()->json(['status' => 'ok']); }}
请注意,如果你没有返回 200 状态代码,该服务将在稍后重试,直到收到 200 或达到失败尝试次数。
这就是我们在应用程序中开始接收邮件所需的一切,尽管我们还需要在 Mailgun 控制面板中进行更多设置。
Mailgun 路由设置
在我们尝试我们的端点之前,我们需要在 Mailgun 中设置一个新路由。有很多选项和方法来处理消息,但我们将选择一个相对简单的设置。
首先,让我们启动 Valet 共享,这样我们就可以使用 URL 来配置 Webhook。
$ valet share ngrok by @inconshreveable (Ctrl+C to quit) Session Status onlineUpdate update available (version 2.2.8, Ctrl-U to update)Version 2.1.18Region United States (us)Web Interface http://127.0.0.1:4040Forwarding http://d1fc8c85.ngrok.io -> mgexample.app:80Forwarding https://d1fc8c85.ngrok.io -> mgexample.app:80
如果你没有使用 Valet,请安装 ngrok 并运行它。
ngrok http example.dev:80
接下来,从 ngrok 中复制转发 URL,并在 Mailgun 中配置一个 新路由
我们将匹配 [email protected]
收件人。用你自己的域名替换示例电子邮件地址。
匹配收件人意味着发送到该电子邮件的任何订单都将触发 Webhook。
接下来,store and notify 规则将存储消息并将 JSON 负载发送到定义的 Webhook。你可以使用逗号指定多个检索 URL!
我们选中“stop”以防止其他规则触发。我们只有一个路由,但如果你不想让其他路由处理相同的消息,你需要停止。把它想象成 JavaScript 的 Event.stopPropagation()
方法。
保留其余选项的默认值,如果你想添加描述,也可以添加。
单击“创建路由”,然后导航到日志部分,这样你就可以在 Mailgun 控制台中查看我们即将发送的入站消息。
使用 Laravel Valet 运行 Webhook
在 Valet 运行(或 ngrok)的情况下,我们就可以开始尝试了。你只需创建一个电子邮件并将其发送到你配置的电子邮件地址即可。
发送电子邮件后,转到运行 ngrok 的终端,你应该会看到一个 HTTP 请求。
HTTP Requests------------- POST /api/mailgun/widgets 200 OK
此外,检查你的 storage/logs/laravel.log
文件,你应该会看到 JSON 负载。我发送了一个示例 CSV 文件,它在 Webhook 负载中看起来像这样。
"attachments": [ { "url": "https://se.api.mailgun.net/v3/domains/mg.example.com/messages/eyJwIjpmYWxzZSwiayI6IjBhYjM5MWE5LTU5YzUtNGJkMS1hMzE5LTBhNjU0ODAwOTY4ZCIsInMiOiIyYWMyN2YxYzc2IiwiYyI6InRhbmtiIn0=/attachments/0", "content-type": "text/csv", "name": "widget-order.csv", "size": 554 }]
因此 Mailgun 在他们的端存储了这个附件,我们可以通过 Mailgun API 获取它。在我向你展示之前,让我们保护 Webhook,这样我们就可以确信请求是真实的。
保护 Webhook
为了保护 Webhook,Mailgun 会在 JSON POST 正文中发送一个 timestamp
和一个 token
。使用我们配置的密钥,我们可以对时间戳和令牌进行编码,然后将该值与 JSON 负载中的 signature
密钥进行比较。
要验证 Webhook,你需要使用 HMAC 算法,使用你的 API 密钥作为密钥,并使用 SHA256。
我们将创建一个中间件来检查这些值,以确保请求是合法的。你也可以通过将令牌存储在像Redis这样的地方,并拒绝任何使用相同令牌的后续请求来添加另一层安全性。我们不会介绍这一点,但它很容易做到。
让我们创建一个中间件并将其注册为一个API中间件。
$ php artisan make:middleware ValidateMailgunWebhookMiddleware created successfully.
接下来,在 app/Http/Kernel.php
中注册中间件。
protected $routeMiddleware = [ // ... 'mailgun.webhook' => \App\Http\Middleware\ValidateMailgunWebhook::class,];
在我们忘记之前,让我们更新路由组,在 routes/api.php
中使用这个中间件。
Route::group([ 'prefix' => 'mailgun', 'middleware' => ['mailgun.webhook'],],function () { Route::post('widgets', 'MailgunWidgetsController@store');});
最后,这是我对中间件的实现。
<?php namespace App\Http\Middleware; use Closure;use Illuminate\Http\Response; class ValidateMailgunWebhook{ public function handle($request, Closure $next) { if (!$request->isMethod('post')) { abort(Response::HTTP_FORBIDDEN, 'Only POST requests are allowed.'); } if ($this->verify($request)) { return $next($request); } abort(Response::HTTP_FORBIDDEN); } protected function buildSignature($request) { return hash_hmac( 'sha256', sprintf('%s%s', $request->input('timestamp'), $request->input('token')), config('services.mailgun.secret') ); } protected function verify($request) { if (abs(time() - $request->input('timestamp')) > 15) { return false; } return $this->buildSignature($request) === $request->input('signature'); }}
让我们稍微分解一下,解释一下发生了什么。
首先,如果请求方法不是 POST,则会返回一个 403
响应。接下来,我们验证请求,如果验证成功,我们允许请求继续。最后,我们默认情况下中止并返回一个 403 - 我们防御性地保护路由,除非它有效。
verify()
方法检查请求时间戳,并确保请求不超过十五秒。验证然后将请求的签名与我们从时间戳和令牌构建的签名进行比较。
buildSignature()
方法使用 Mailgun 密钥对组合的时间戳和令牌进行编码。
测试安全路由
如果你发送另一封电子邮件,你的中间件应该仍然允许有效的请求通过。但是,如果你从终端发送请求,你现在将收到一个 403 错误。
$ curl -I -X POST http://mgexample.dev/api/mailgun/widgetsHTTP/1.1 403 ForbiddenServer: nginx/1.10.3
控制器示例
为了结束,让我们讨论一些关于控制器的技巧。我发现调度一个作业是处理 Mailgun Webhook 的最佳方式,前提是我对有效负载感到满意。通常,我想在我这边进行一些异步处理,所以将有效负载调度到一个作业是有意义的。
如果你的用例很简单,你不必使事情复杂化。也许你只需要在数据库中存储一个值,并进行一些最小的工作 - 使用你最好的判断。
假设我想处理一个特定类型的文件集合。你可能会在你的控制器中看到类似的东西。
public function store(Request $request){ app('log')->debug(request()->all()); $files = collect(json_decode($request->input('attachments'), true)) ->filter(function ($file) { return $file['content-type'] == 'text/csv'; }); if ($files->count() === 0) { return response()->json([ 'status' => 'error', 'message' => 'Missing expected CSV attachment' ], 406); } dispatch(new ProcessWidgetFiles($files)); return response()->json(['status' => 'ok'], 200);}
请注意,如果你返回一个 406
(不可接受) 响应代码,Mailgun 将假设 POST 被拒绝,并且不会重试。如果你返回一个 200,Webhook 将成功。如果控制器返回任何其他状态代码,Mailgun 将按照计划重试,直到成功或最终在最大重试次数后失败。
在分派的作业中,你可以使用 Guzzle 下载文件并在你的作业类中处理它们。
use GuzzleHttp\Client; $response = (new Client())->get($file['url'], [ 'auth' => ['api', config('services.mailgun.secret')],]); // do something with $response->getBody();
文件请求使用 Guzzle 的 auth
键进行基本的 HTTP 身份验证。请注意,这些文件只会在几天内临时可用。
你收到了邮件
所以,这就是使用 Mailgun 的入站电子邮件路由与 Laravel 中的 Webhook 的快速浏览。我只触及了你所能做到的皮毛!