需要电子邮件-管道-nano
整合所有外出电子邮件基础设施的铁路引擎 应用程序:电子邮件前缀、截取、速率限制、发送记录、, SparkPost webhook 处理和电子邮件打开跟踪.
目录
结构概览
每一封外出电子邮件都经过以下管道,无论通过
deliver_later (锡德基克语)或 deliver_now (内行). 发动机探测到
自动发送路径并捕获来自 Sidekiq 的组织上下文
中间软件或自定义邮件头.
flowchart TD
A[Mailer.deliver_later] --> B[Sidekiq Queue]
B --> C["EmailLogMiddleware
Generates UUID, sets Thread.current context"]
C --> D["MailThrottleMiddleware
Checks hourly rate limit per org"]
D -->|Rate limited| E["Reschedule job with backoff
Preserves email_log_uuid in job hash"]
E --> R["EmailLogMiddleware handle_post_yield
Creates rate_limited placeholder
(email: nil, rate_limited_at set)"]
R -->|"Job re-enters queue
with same UUID"| B
D -->|Allowed| F[Build Mail Object]
DN[Mailer.deliver_now] --> DN1["after_action callback
Sets X-Neeto-Organization-Id,
X-Mailer-Class, X-Mailer-Action headers"]
DN1 --> F
F --> T{"EmailOpenTrackable
included & trackable present?"}
T -->|Yes| U["after_action injects 1×1 pixel
EmailOpenReceipt created with HMAC token"]
U --> G
T -->|No| G
G["EmailPrefixer
Adds [STAGING]/[DEV] prefix to subject"]
G --> H["Interceptor
Drops/forwards based on environment"]
H -->|Dropped| I["LoggingInterceptor
Mark as dropped with reason"]
H -->|Allowed| J["LoggingInterceptor
Deletes rate_limited placeholder (if any),
creates per-recipient log records
with rate_limited_at preserved"]
J --> K[Mail Delivery]
K --> L["LoggingObserver
Mark queued records as handed_over"]
L --> M{Delivery Method?}
M -->|SparkPost| N[SparkPost Delivers Email]
N --> O[SparkPost Webhook]
O --> P["SparkpostWebhookJob
Match via email_log_uuid in rcpt_meta"]
P --> Q2[Update Status: Delivered / Bounced / Spam]
M -->|Gmail / Outlook / SMTP| S["Final status: handed_over
No further tracking available"]
C -->|Exception| Q[Mark as Failed]
subgraph "Asynchronous — triggered when recipient opens email"
W[Recipient Opens Email] --> X["GET /neeto_email_pipeline/
email_open_receipts/:token.png"]
X --> Y["EmailOpenReceiptsController
Updates first_opened_at, open_count"]
Y --> Z[Returns 1×1 transparent PNG]
end
style DN fill:#60a5fa,color:#000
style DN1 fill:#60a5fa,color:#000
style E fill:#fbbf24,color:#000
style R fill:#fbbf24,color:#000
style I fill:#f87171,color:#000
style Q2 fill:#34d399,color:#000
style S fill:#94a3b8,color:#000
style Q fill:#f87171,color:#000
style U fill:#a78bfa,color:#000
style Y fill:#a78bfa,color:#000
style Z fill:#a78bfa,color:#000
发送 现在 vs 发送 后期 : 这个
deliver_later路径(黄色节点) 通过 Sidekiq 中间软件来限制费率和设置上下文。 这个deliver_now路径( 蓝色节点) 完全跳过 Sidekiq —— 组织上下文 通过自定义邮件信头提供(X-Neeto-Organization-Id, (中文(简体) ),X-Mailer-Class, (中文(简体) ),X-Mailer-Action) 由一个after_action调用。 这些 在电子邮件离开系统前,头被剥去.
电子邮件 生命周期状态
| 状态 | 含义 |
|---|---|
queued | 电子邮件输入系统 |
rate_limited | 由于小时发送限制而暂时持有 |
dropped | 发送前被屏蔽( 收件人太多, 内容被屏蔽, 地址被弹出) |
handed_over | 成功传递到 SparkPost 以交付 |
delivered | SparkPost 确认向收件人的邮件服务器发送 |
bounced | 发送失败( 无效地址、 完整邮箱、 政策拒绝) |
spam_complaint | 收件人将电子邮件标为垃圾邮件 |
failed | 移交前系统出错( SMTP 失败, 邮件构造出错) |
用主机应用程序开发
发动机
安装
-
在您的应用程序的 Gemfile 中添加宝石 :
source "NEETO_GEM_SERVER_URL" do # ..existing gems gem 'neeto-email-pipeline-engine' end -
安装宝石 :
bundle install -
将引擎挂入
config/routes.rb数字 :mount NeetoEmailPipelineEngine::Engine, at: "/neeto_email_pipeline" -
复制并运行迁移 :
bundle exec rails neeto_email_pipeline_engine:install:migrations bundle exec rails db:migrate
配置
常数.yml
在您的 config/constants.yml数字 :
defaults: &defaults
email_pipeline:
email_logging_enabled: true
retention_days: 30
development:
<<: *defaults
test:
<<: *defaults
staging:
<<: *defaults
production:
<<: *defaults
秘密. yml(通过Vault)
添加 SparkPost Webhook 证书到 config/secrets.yml数字 :
defaults: &defaults
email_pipeline:
sparkpost_webhook_username: <%= ENV['...'] %>
sparkpost_webhook_password: <%= ENV['...'] %>
排定的工作
添加清理任务到 config/scheduled_jobs.yml数字 :
purge_old_email_logs:
cron: "0 3 * * *"
class: NeetoEmailPipelineEngine::PurgeOldEmailLogsJob
description: "Purge email logs older than retention period"
前端软件包
安装
-
添加软件包 :
yarn add @bigbinary/neeto-email-pipeline-frontend -
安装对等依赖( neetoui、 neto- molecules、 neto- commons- prefend, 需要过滤- 前端等),如果还没有出现.
构成部分
软件包导出 EmailLogs 组件 :
import { EmailLogs } from "@bigbinary/neeto-email-pipeline-frontend";
<EmailLogs
canViewEmailLogs={true}
breadcrumbs={[
{ text: "Admin panel", link: "/admin" },
{ text: "Email Logs" },
]}
helpUrl="https://neetocalhelp.neetokb.com/p/a-8ed45325"
pageTitle="Email Logs"
/>;
道具 :
| Prop | 类型 | 需求 | 说明 |
|---|---|---|---|
canViewEmailLogs | boolean | 对 | 许可门。 若为 false什么都没有. |
breadcrumbs | array | 对 | Header 组件的 Breadcrumb 线索 . |
helpUrl | string | 没有 | 帮助文章的 URL 。 在标题旁边设置一个帮助图标. |
pageTitle | string | 没有 | 浏览器/文档标题 。 如果不通过, 则默认本地化的“ 电子邮件日志” 标题 . |
特征
电子邮件前缀
在非生产环境下(例如,
[STAGING], (中文(简体) ), [DEVELOPMENT]) (中文(简体) ). 同时支持每个组织的自定义前缀
通过 email_prefix 字段.
要求: 每个邮递员都必须跟 organization_id 参数:
UserMailer.with(organization_id: org.id).welcome_email.deliver_later
电子邮件截取
这个 Interceptor 控制实际发送的电子邮件
环境:
结构/发展:
- 将电子邮件转发到已配置的地址( E)
mail_interceptor.forward_emails_to保密) - 白名单 具体地址 (
mail_interceptor.whitelisted_emails内 机密记录) - 截取匹配的电子邮件
^cpt.*bigbinary\.com$
生产:
- 过滤电子邮件到
@example.com财务报告和审定财务报表@example.net
所有环境——当:
- 整个To、CC和BCC超过10个独特的受援者
- 主题或机构包含被封锁的内容模式(
depop, (中文(简体) ),dep0p(中文(简体) ) - 所有收件人都反弹了2+次(通过NeetoTower API检查)
下降原因记录在电子邮件日志中 status_detail 字段.
说明: 如果 NeetoTower API 不可用( 超时, 连接拒绝 ,), 5xx 错误,引擎将弹出计数视为 0 并允许电子邮件为 发送。 错误向Honeybadger报告,以获取知名度.
限制费率
这个 MailThrottleMiddleware 每个组织执行小时电子邮件发送限制 :
- 通过配置
organization.max_hourly_emails。 。 。 . - 可能在全球被压倒
GLOBAL_HOURLY_EMAIL_LIMIT_FOR_ALL_ORGS环境 var. (中文(简体) ) - 在生产过程中默认允许限制费率,由
RATE_LIMIT_ENGINE_ENABLED环境 var - 当费率受限时,工作会以指数反转(60s to 页:1
- NeetoTower被告知被击中的情况(每10分钟窗口复制一次):
限制, 后面等待的邮件, 垃圾邮件筛选被删除
今天还有寄来的邮件 当NeetoTower不能接受报告时
通知改为速率限制 Slack 频道。 所需产品
NEETO_TOWER_AUTH_TOKEN设置为 NeetoTower 的服务器令牌
电子邮件日志
何时 email_logging_enabled 实值 true,每一封外出电子邮件都使用:
- 每个接收者的记录:每个收件者一行(到/cc/bcc),有电子邮件, 主体、来源、交付方法
- 电子邮件机体捕获: HTML 体被存储在管理员 UI 中预览
- 整个生命周期跟踪: 已排队 —— 速率 有限 —— 移交 —— 发送/发出/垃圾/投诉
- 费率限制意识:记录携带
rate_limited_at时间戳 电子邮件被节制 - 自动清理数字 :
PurgeOldEmailLogsJob删除旧于retention_days
正在发送支持( n): 通过电子邮件发送
deliver_now还记录下来。 自兹 这些电子邮件绕过 Sidekiq 中间软件, 引擎读取组织上下文 从自定义邮件信头( e)X-Neeto-Organization-Id, (中文(简体) ),X-Mailer-Class, (中文(简体) ),X-Mailer-Action由一个after_action在邮递员中召回. 货头在交货前被剥去,所以永远无法到达接收者.
SparkPoost 网络呼号
引擎为 SparkPost 发送事件提供网点:
终点 : POST /neeto_email_pipeline/webhooks/sparkpost/events
认证 : HTTP Basic Auth 使用金库的证书.
处理的活动:
delivery− 状态:deliveredbounce, (中文(简体) ),out_of_band, (中文(简体) ),policy_rejection− 状态:bouncedspam_complaint− 状态:spam_complaint
关联性 : 这个 LoggingInterceptor 注射数 email_log_uuid 输入
X-MSYS-API 页眉元数据. SparkPost 在网路上回响了这一点
rcpt_meta.email_log_uuid,该 SparkpostWebhookJob 用于查找
准确的接收记录.
SparkPots 设置
- 转到 SparkPost Webhooks 创建 Webhook
- 目标 URL:
https://app.yourproduct.com/neeto_email_pipeline/webhooks/sparkpost/events - 认证 : HTTP Basic 有用户名/ 密码字来自您的金库配置
- 要订阅的事件 :
- 交付 :
delivery - 弹出:
bounce, (中文(简体) ),out_of_band, (中文(简体) ),policy_rejection - 垃圾邮件 :
spam_complaint
- 交付 :
电子邮件打开跟踪
通过插入 1×1 透明跟踪方式打开电子邮件时的跟踪 将像素输入正在发送的邮件 。 当收件人的邮件客户端装入像素时, 引擎记录打开的事件.
设置
-
包含
EmailOpenTrackable在您的邮件中关注:class CommentMailer < ApplicationMailer include NeetoEmailPipelineEngine::EmailOpenTrackable end -
过
trackable在您的邮件参数中:CommentMailer.with( organization_id: org.id, trackable: { id: comment.id, klass: "Comment" } ).created_email.deliver_later -
覆盖
email_open_tracking_enabled?在您的组织模式中:def email_open_tracking_enabled? setting&.read_receipts_enabled? end默认( 在引擎中定义)
Helpers(关切)返回false。 。 。 .
如何运作
- 这个
EmailOpenTrackable登记after_action运行后 邮件动作建立消息. - 若为
params[:trackable]已存在organization.email_open_tracking_enabled?返回时true,关注:- 创建一个
EmailOpenReceipt带有HMAC-SHA256活塞符号 - 确定
attribution_scope(单位:千美元)single_recipient或者说shared_envelope根据CC/BCC的存在) - 附加一个 1×1 透明的 PNG
<img>关闭前标记</body>标签
- 创建一个
- 当收件人打开电子邮件时,他们的邮件客户端会加载像素图像,
击打
GET /neeto_email_pipeline/email_open_receipts/:token.png。 。 。 . - 控制器解剖更新
first_opened_at(仅安排在第一个 打开),last_opened_at,以及open_count。 。 。 . - 端点总是返回有效的 1x1 透明 PNG, 不管符号 有效性——没有泄露任何关于是否存在一个符号的信息.
查询打开数据
主机应用直接查询引擎模型:
receipt = NeetoEmailPipelineEngine::EmailOpenReceipt.find_by(
trackable_type: "Comment", trackable_id: comment.id
)
receipt&.first_opened_at # => 2026-06-22 14:30:00 UTC
receipt&.open_count # => 3
receipt&.attribution_scope # => "single_recipient"
警卫
- 托肯为 256- 位 (
SecureRandom.urlsafe_base64(32))并存储为 HMAC-SHA256文摘——数据库出漏不会出出有效的像素URL. - 像素端点跳过所有认证(CSRF,会话,org加载).
- 回复绝不透露符号有效性(总是返回有效的PNG).
- 如果在发送电子邮件后无法跟踪, 后续像素点击是 被忽略 .
已知的限制
- 封锁远程图像的电子邮件客户端(Apple Mail 隐私保护, Outlook defaults)不会触发像素——开放计数可能会被低估.
- 电子邮件代理(Gmail 映像代理,Apple MPP)可以触发假阳性.
电子邮件链接点击跟踪
当收件人通过每封重写在发送的电子邮件中单击链接时跟踪
跟踪 <a> 标签 href 输入已签名的重定向 URL。 当收件人
单击链接,引擎记录点击事件并将其转发到
原目的地.
设置
-
包含
EmailLinkClickTrackable在您的邮件中关注:class CommentMailer < ApplicationMailer include NeetoEmailPipelineEngine::EmailLinkClickTrackable end -
过
trackable在您的邮件参数中:CommentMailer.with( organization_id: org.id, trackable: { id: comment.id, klass: "Comment" } ).created_email.deliver_later -
在电子邮件正文中以一个标记每个可追踪链接
data-link-index属性 :<a href="https://example.com/pricing" data-link-index="0">See pricing</a> -
覆盖
email_link_click_tracking_enabled?在您的组织模式中:def email_link_click_tracking_enabled? setting&.link_click_tracking_enabled? end默认( 在引擎中定义)
Helpers(关切)返回false。 。 。 .
如何运作
- 这个
EmailLinkClickTrackable登记after_action运行 在邮件动作建立消息后 . - 若为
params[:trackable]已存在organization.email_link_click_tracking_enabled?返回时true,该关切扫描 HTML 正文<a href>包含一个标签的标签data-link-index属性和一个 HTTP 或 HTTPS URL。 无链接data-link-index属性或不支持的 URL 方案,例如mailto:, (中文(简体) ),tel:,或#,为 一直没动过. - 对于每个可追踪链接,关切是:
- 创建一个
EmailLinkClick记录在前面, 存储目的地 URL、链接名称/索引、接收者、组织attribution_scope(单位:千美元)single_recipient或者说shared_envelope根据CC/BCC的存在) - 生成一个签名的符号( 通过
ActiveSupport::MessageVerifier编码 记录id目标url - 重写链接
href指向方向端点 符号
- 创建一个
- 当收件人点击链接时,他们的邮件客户端请求
GET /neeto_email_pipeline/email_link_clicks/:token。 。 。 . - 控制器验证符号, 原子递增
click_count,以及 将浏览器重定向到原目标 URL.
点击数据查询
主机应用直接查询引擎模型:
clicks = NeetoEmailPipelineEngine::EmailLinkClick.where(
trackable_type: "Comment", trackable_id: comment.id
)
clicks.sum(:click_count) # => 5
clicks.pluck(:link_name, :click_count)
# => [["See pricing", 3], ["Watch demo", 2]]
警卫
- 托肯斯是加密签名的(通过Active支持:MessageVerifier),而不是加密的. 任何人都可以解码一个令牌 查看目的地URL和记录ID, 任何企图变更或制造假象者, 无法在不知悉应用程序密钥的情况下验证签名 .
- 无效或被篡改的令牌返回一个404 而不是在任何地方方向, 防止终点被用作开放方向.
- 如果在发送电子邮件后无法跟踪, 链接已经重写到 电子邮件仍然会重定向并记录点击,因为符号是 自成一体并独立于当前跟踪设置.
本地测试
电子邮件截取
要测试开发中的电子邮件转发, 请在
config/secrets.yml数字 :
development:
mail_interceptor:
forward_emails_to: "[email protected]"
whitelisted_emails: ""
电子邮件将转发到配置的地址, 而不是原地址 接收者.
限制费率
限制利率是默认情况下在发展中被禁用。 使:
-
设定
RATE_LIMIT_ENGINE_ENABLED=true在你的环境中 -
设定 Org 的小时限制 :
Organization.first.update!(max_hourly_emails: 5) -
发送超过限制的邮件以触发节奏
电子邮件日志
-
启用登录
config/constants.yml数字 :development: email_pipeline: email_logging_enabled: true retention_days: 30 -
重新启动Sidekiq(发动机登记拦截器/观察器)
-
从应用程序发送电子邮件并检查管理面板中的电子邮件日志页面
SparkPost Webhooks( 本地语)
要测试本地的网点, 请使用隧道服务 :
-
启动通向本地服务器的隧道( 例如 ),
tunn.dev, (中文(简体) ),ngrok(中文(简体) ) -
使用隧道 URL 作为 SparkPost 中的 Webhook 目标
-
添加 Webhook 证书到
config/secrets.yml数字 :development: email_pipeline: sparkpost_webhook_username: "your-username" sparkpost_webhook_password: "your-password" -
使用可映射到有效组织的子域(例如,
spinkart.tunn.dev(中文(简体) )
电子邮件打开跟踪
-
启用组织跟踪 :
# In the host app, override email_open_tracking_enabled? to return true # e.g., in neeto-desk: toggle Read Receipts in Admin Panel > Tickets -
从邮件发送邮件, 包括
EmailOpenTrackable和通过trackable被禁锢. -
检查被截取的邮件
/mailbox—— HTML 源码应当包含 1×1 (中文(简体) )<img>标记指向/neeto_email_pipeline/email_open_receipts/<token>.png。 。 。 . -
在浏览器中打开图像 URL 以模拟电子邮件打开 。 验证收据 记录如下:
NeetoEmailPipelineEngine::EmailOpenReceipt.last # => #<EmailOpenReceipt open_count: 1, first_opened_at: "...">
要发送本地电子邮件, 请在您的开发中添加以下行 。 rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
您还需要添加以下嵌入变量:
SPARKPOST_USERNAME=xxx
SPARKPOST_PASSWORD=xxx
SPARKPOST_DOMAIN=xxx # a verified Sparkpost domain. Unverified domains will raise errors.
出版指令
咨询 构建和释放软件包 关于如何出版的详细指南.