#33 MJML 邮件格式

2026-07-27

MJML(Mailjet Markup Language)是一种专门用于编写响应式 HTML 邮件的标记语言。相当于是 HTML 在电子邮件场景下的更高层的封装,由编译器生成兼容各种邮箱客户端的 HTML。主要解决邮件 HTML 的痛点:

  • Gmail / Outlook / Apple Mail 支持差异巨大
  • 邮件需要大量 table 布局
  • CSS inline 化复杂
  • 移动端适配困难

注意:MJML 只负责 UI 不含模板功能。如果需要使用变量或者其他模板语法,可以另外选择任何一种语言进行渲染,比如 Node Handlebars,Golang 模板,Python Jinja2 等。

MJML 最初由法国邮件服务公司 Mailjet 在 2015 年开源。目前项目已经独立维护,属于开源生态:

  • 语法:MJML
  • 编译器:Node.js 实现
  • 输出:标准 HTML Email

GitHub Repository: mjmlio/mjmlio

MJML 文件格式

扩展名:.mjml

<mjml>
  <mj-head>
    <mj-title>Welcome</mj-title>
  </mj-head>

  <mj-body>

    <mj-section>
      <mj-column>

        <mj-text font-size="20px">
          Hello Woody
        </mj-text>

        <mj-button href="https://example.com">
          Click
        </mj-button>

      </mj-column>
    </mj-section>

  </mj-body>
</mjml>

通过一下方法可以直接预览效果:

  1. VSCode 拓展 [MJML Official]
  2. 官方 MJML Live Editor
  3. 自己实现 Preview 服务,AI 很容易生成一个

渲染 HTML

安装:

npm install -g mjml

转换:

mjml template.mjml -o template.html

生成的 HTML 会包含:

  • table 布局
  • inline style
  • Outlook VML兼容代码
  • media query

#32 使用 curl 命令发送邮件

2026-04-05

用 perl 开发的命令行工具 swaks 用来发送邮件挺方便的,但是有些环境不便安装,我问 AI,结果推荐了 curl 命令。

-> % curl --help smtp
smtp: SMTP protocol
     --crlf                       Convert LF to CRLF in upload
 -F, --form <name=content>        Specify multipart MIME data
     --form-string <name=string>  Specify multipart MIME data
 -H, --header <header/@file>      Pass custom header(s) to server
     --login-options <options>    Server login options
     --mail-auth <address>        Originator address of the original email
     --mail-from <address>        Mail from this address
     --mail-rcpt <address>        Mail to this address
     --mail-rcpt-allowfails       Allow RCPT TO command to fail
     --oauth2-bearer <token>      OAuth 2 Bearer Token
 -X, --request <method>           Specify request method to use
     --ssl                        Try enabling TLS
     --ssl-reqd                   Require SSL/TLS

我用 curl 发送了几封邮件,还真的挺好的,满足我的要求。

  • 生成邮件内容:

    export FROM=noreply@example.com
    export RCPT=ninedoors@126.com
    export USER=shanda
    export PASS=6404290f99f75bf182673bcbd012c121
    
    # AI 生成验证码邮件模板
    cat > /tmp/mail.html <<EOF
    From: $FROM
    To: $RCPT
    Subject: Your Verification Code: CODE
    Content-Type: text/html; charset=UTF-8
    
    <!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport"content="width=device-width, initial-scale=1.0"><title>Verification Code</title><style>@media only screen and(max-width:600px){.container{width:100%!important}.mobile-padding{padding:10px!important}}</style></head><body style="margin: 0; padding: 10px; background-color: #f4f4f4; font-family: Arial, sans-serif;"><table role="presentation"align="center"border="0"cellpadding="0"cellspacing="0"width="100%"style="max-width: 600px; width: 100%; margin: 0 auto; background-color: #ffffff;"><tr><td class="mobile-padding"style="padding: 40px 20px;"><table role="presentation"width="100%"border="0"cellpadding="0"cellspacing="0"><tr><td style="padding-bottom: 30px;"><h1 style="color: #333333; margin: 0;">Verification Code</h1></td></tr></table><table role="presentation"width="100%"border="0"cellpadding="0"cellspacing="0"><tr><td style="padding: 20px 0;"><p style="color: #666666; line-height: 1.6; font-size: 16px;">Your verification code is:<h2 style="letter-spacing: 2px;">CODE</h2></p></td></tr></table></td></tr></table></body></html>
    EOF
    
    # 替换验证码内容,我不想每次发送相同内容
    # sed "s/CODE/$(printf "%06d" $((RANDOM % 1000000)))/g" /tmp/mail.html > /tmp/mail2.html
    sed s/CODE/$(python -c "import secrets; print('%06d' % secrets.randbelow(10**6))")/g /tmp/mail.html > /tmp/mail2.html
    
  • 邮件发送:

    curl smtp://smtp.engagelab.cc:2525 --mail-from nobody@whatever.com --mail-rcpt $RCPT --user "$USER:$PASS" --upload-file /tmp/mail2.html -vv -s 2>&1 | grep -Fv '] * '
    

遇到一个奇怪的现象:zsh 中 RANDOM 这样使用结果不会变更,没有时间研究,暂时忽略。

-> % echo $(printf "%06d" $((RANDOM % 1000000)))
018840
-> % echo $(printf "%06d" $((RANDOM % 1000000)))
018840
-> % echo $(printf "%06d" $((RANDOM % 1000000)))
018840

如果直接使用却会,不知道为什么:

for i in {1..5}; do echo $RANDOM done

#31 电子邮件服务行业(ESP)细分领域

2026-01-15
  • L1: Email Infrastructure (基础设施)
  • L2:Email Deliverability & Trust (送达与信誉)
    • Spam filtering
    • Abuse mitigation
    • Authentication protocols
    • Reputation system
    • Deliverability analytics
    • ISP feedback loops
    • Email hygiene / list quality
  • L3:Email Marketing & Growth(营销与增长)
    • Email marketing automation
    • List building
    • Segmentation
    • Campaign orchestration
    • A/B testing
    • Email design & template system
  • L4:Compliance & Governance(合规治理)
    • GDPR / CAN-SPAM / CASL 法规框架
    • Opt-in / opt-out system 订阅机制
    • Data retention policies 数据保留策略
    • Consent management 同意管理
  • L5:Market & Ecosystem(外部生态)
    • International email strategies
    • Industry relations(ISP / regulators)
    • Domain reputation ecosystem

#29 进箱:电子邮件自动归类

2025-08-17

谷歌(Gmail)和苹果(Apple iCloud)

Gmail: Primary(主要) Social(社交) Updates(动态) “Forums(论坛) Promotions(推广)
Apple: Primary(主要) Transactions(交易) Updates(更新) Promotions(推广)

Gmail 和 Apple 分类比较类似:

  • 主要:个人邮件
  • 更新/动态:资讯、订阅
  • 推广:营销邮件

不同的是,谷歌搞了一个社交与论坛,因为他们有这方面的业务。
苹果则是单独列了一个交易,将订单、物流等邮件单独管理。

微软(Outlook)

  • 重点
  • 其他

这个归类方式就比较简单,可能和微软的在线业务不够丰富、市场占有率不够高(和谷歌、苹果相比)有关。

总结

这个分类就收益于 AI 的出现,体现了 ISP 对电子邮件内容的控制程度的升高。

#28 Go 解析 SPF 记录

2025-07-08

背景知识:什么是 SPF?

SPF(Sender Policy Framework,发送方策略框架)是一种电子邮件验证协议,定义在 https://datatracker.ietf.org/doc/html/rfc7208 中。它的核心作用是让域名所有者声明:哪些 IP 地址或服务器被允许代表该域名发送邮件

接收方邮件服务器收到邮件后,会查询发件人域名的 SPF 记录(一条 DNS TXT 记录),检查连接 IP 是否在授权列表中。这能有效防止邮件伪造和钓鱼攻击。

一条典型的 SPF 记录长这样:

v=spf1 ip4:192.0.2.0/24 include:_spf.google.com ~all

Golang 解析

注意:这个库只有 12 次提交,已经 2 年多没有更新,肯定不完备,但是放在小工具中简单使用也没有什么问题。
UPDATE: 这个库太简陋了,不如自己写一个。

mileusna/spf 是一个轻量级的 Go 语言 SPF 检查库,提供了两个核心接口:

功能 函数 说明
SPF 校验 spf.CheckHost() 给定 IP、域名和发件人,返回完整的 SPF 判定结果
记录查询 spf.LookupSPF() 仅从 DNS 拉取原始 SPF TXT 字符串
package main

import (
    "net"

    "github.com/mileusna/spf"
)

func main() {
    // optional, set DNS server which will be used by resolver.
    // Default is Google's 8.8.8.8:53
    spf.DNSServer = "1.1.1.1:53"

    ip := net.ParseIP("123.123.123.123")
    r := spf.CheckHost(ip, "qq.com", "service@qq.com", "");
    // returns spf check result
    // "PASS" / "FAIL" / "SOFTFAIL" / "NEUTRAL" / "NONE" / "TEMPERROR" / "PERMERROR"

    // if you only need to retrive SPF record as string from DNS
    spfRecord, _ := spf.LookupSPF("qq.com")
    fmt.Println(spfRecord)
    // v=spf1 include:spf.mail.qq.com -all
}

spf.CheckHost() 返回以下 7 种字符串结果之一:

返回值 含义 邮件处理建议
"PASS" IP 被明确授权 正常接收
"FAIL" IP 不在授权列表中(-all-ip4 匹配失败) 建议拒收
"SOFTFAIL" 软失败(~all),IP 可能未授权但无法确定 接受但标记/放入垃圾箱
"NEUTRAL" 策略判定为中立(?all 按常规流程处理
"NONE" 域名无 SPF 记录或记录无法解析 建议拒收
"TEMPERROR" DNS 临时错误(超时、SERVFAIL 等) 可能是 DNS 抖动,稍后重试,暂不拒收
"PERMERROR" SPF 记录存在永久错误(语法错误、多重记录等) 配置异常,建议拒收

#27 邮件域名认证机制

2024-03-03

说明:SMTP AUTH (PLAIN / LOGIN) 属于邮件客户端身份认证机制,与上面说的邮件域名认证机制(也叫做邮件认证机制,Email Authentication)是两码事。

邮件域名认证机制包括 SPF、DKIM、DMARC 三种:

  • SPF 校验(RFC 7208):当前 SMTP 会话的客户端 IP 是否被 MAIL FROM 域名授权发送邮件。
    PS:优先使用 MAIL FROM(Envelope-From)域名,若 MAIL FROM 为空(<>),则使用 HELO/EHLO 域名。

  • DKIM 校验:DKIM 通过 DKIM-Signature 头中的 d=(Signing Domain,署域)和 s=(Selector)从 DNS 获取公钥,对指定的 header 和 body 进行签名校验。

    dig txt selector._domainkey.signing-domain
    
  • DMARC 校验(RFC 7489 Domain-based Message Authentication, Reporting, and Conformance (DMARC)):

    DMARC 在 SPF、DKIM 基础上引入了域名 “对齐” (Alignment) 概念,要求邮件 FROM 头域名必须与 SPF 校验所使用的域名 或 DKIM 署域 中的至少一个保持对齐(一致),实践中通常是两者同时保持对齐。
    PS:严格对齐就是要求域名部分完全一致,宽松对齐就是仅要求主域名 (eTLD + 1) 一致,对应 DMARC 记录的 aspf/adkim TAG 的值 r(relaxed mode)与 s(strict mode),如果没有指定,默认就是宽松对齐,实际上主流 MP 都没有指定。

    此外,DMARC 还提供了如果校验失败的处理策略框架 (none 监控 / quarantine 隔离 / reject 拒绝) 与反馈机制 (rua 汇总报告 / ruf 失败报告)。

#25 邮件地址黑名单

2023-12-25
  1. 退订
  2. FBL 举报
  3. 运营商反馈
  4. 收件人反馈(投诉)
  5. 地址不存在
  6. 邮箱容量已满
  7. 平台或客户手动指定的域名或地址
  8. 第三方提供的黑名单地址

有些是永久禁止,有些是临时禁止(比如一个月、三个月、半年)。

#24 SpamHaus

2023-12-05

关于 SpamHaus

Wikipedia:The SpamHaus Project

Spamhaus Project 是一个位于安道尔公国(法国和西班牙中间,498 平方公里)的国际组织,由 Steve Linford 于 1998 年创立,旨在跟踪垃圾邮件发送者和垃圾邮件相关活动。
SpamHaus 这个名字是一个伪德语表达,由林福德创造,指的是向垃圾邮件发送者发送垃圾邮件或故意向垃圾邮件发送者提供服务的互联网服务提供商或其他公司。

该组织维护了几个反垃圾邮件列表,被很多邮件服务采用。
这个列表通过基于 DNS 的黑名单(DNSBL)和白名单(DNSWL)提供,对个人和中小企业免费,大量查询使用应该是有付费服务(采用 rsync 订阅)。
SpamHaus 提供了文档《Understanding DNSBL Filtering》讲解其 DNSBL 的工作原理。

主要列表

  • SBL:Spamhaus Block List,发送垃圾邮件 IP
    • BCL:Spamhaus Botnet Controller List,僵尸网络
    • CSS:Combined Spam Sources,属于 SBL
    • DROP:Don't Route Or Peer,直接丢弃所有流量 IP,提供给防火墙使用,属于 SBL
  • XBL:Exploits Block List,被劫持机器 IP
  • PBL:Policy Block List,未通过身份验证滥发邮件 IP,可以自助移除
  • ZEN:SBL + XBL + PBL
  • DBL:Domain Block List,域名级别拉黑