Laravel 中的入站邮件

发布于 作者

Inbound Email in Laravel image

我最近需要能够接收电子邮件并在这些电子邮件上处理附件。我喜欢用 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=mailgun
MAILGUN_DOMAIN=mg.example.com
MAILGUN_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 online
Update update available (version 2.2.8, Ctrl-U to update)
Version 2.1.18
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://d1fc8c85.ngrok.io -> mgexample.app:80
Forwarding 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 ValidateMailgunWebhook
Middleware 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/widgets
HTTP/1.1 403 Forbidden
Server: 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 的快速浏览。我只触及了你所能做到的皮毛!

Paul Redmond photo

Laravel News 的特约撰稿人。全栈 Web 开发人员和作家。

归档于
Cube

Laravel 新闻

加入 40,000 多名其他开发人员,永不错过新技巧、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动完成和本地和生产环境的即时反馈进行调试。

Tinkerwell
No Compromises logo

无妥协

Joel 和 Aaron,来自 No Compromises 播客的两位经验丰富的开发人员,现在可以为您的 Laravel 项目聘用。 ⬧ 每月 7,500 美元的固定费用。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 退款保证。

无妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保您的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?即时、自动化的 Laravel 升级和代码现代化,以保持您的应用程序新鲜。

Shift
Bacancy logo

Bacancy

使用经验丰富的 Laravel 开发人员为您的项目加油,他们拥有 4-6 年的经验,每月只需 2,500 美元。获得 160 小时的专业知识和 15 天的无风险试用。立即预约通话!

Bacancy
Lucky Media logo

Lucky Media

现在就获得 Lucky - Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar:Laravel 电子商务

Laravel 的电子商务。一个开源软件包,将现代无头电子商务功能的力量带到 Laravel。

Lunar:Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 工作板

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit:Laravel SaaS 启动工具包

SaaSykit 是一个 Laravel SaaS 启动工具包,它包含运行现代 SaaS 所需的所有功能。支付、漂亮的结账、管理面板、用户仪表盘、身份验证、准备好的组件、统计数据、博客、文档等等。

SaaSykit:Laravel SaaS 启动工具包
Rector logo

Rector

您无缝升级 Laravel、降低成本和加速创新的合作伙伴,助力企业取得成功

Rector
MongoDB logo

MongoDB

使用 MongoDB 和 Laravel 的强大集成来增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务性、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。了解 MongoDB 的灵活、现代数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的无依赖性输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel 提示构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 软件包向您的 Laravel 应用程序添加评论

阅读文章