Try   HackMD

一篇讀懂 email

目錄

TL;DR

  • 持續保持符合信件規範
  • 同時提供 text/html & text/plaintext 格式的信件
  • 統一控管郵件發送量/頻率,避免短時間內大量的郵件觸發防護機制
    • 對於高頻率/低頻率的不同使用者,應有不同的寄件策略
  • 按照使用者習慣的語言寄出對應信件
    • 非使用者常用語言的信件有高機率被分到垃圾信
    • 我在垃圾桶撈到了南非荷蘭文的 Tiktok 認證信,我很疑惑
  • 透過驗證碼及各類優惠等互動信件引導使用者幫 tiktok train 屬於他們自己信箱的 filter,透過分類/點擊/回覆 等行為,提高 tiktok 信件的優先權
  • 持續追蹤信件送達率,主要分類率,不重複點擊率,重複點擊率,首次閱讀時間差等數據,持續改進送信方案。
    • 送達率:成功寄出信件的機率
    • 主要分類率:送達且分類在主要信件的機率
    • 不重複點擊率:用戶點開同一信件至少一次的機率
    • 重複點擊率:用戶點開同一信件不只一次的機率
    • 首次閱讀時間差:信件抵達及用戶第一次點開的時間間隔

EMail 送出流程,以 SMTP 為例

SMTP 的全名是:Simple Mail Transfer Protocol

SMTP 是 string stream 組成的協議,因此即便用 telnet 也可以送出信件。

以最常見的 SMTP 舉例,假設今天有以下情景

rance_jen@outlook.com 想要寄信給 daniel_Yu@outlook.com,同時副本給 xu_hsin_liu@outlook.com

S 指 Server, C 指 Client
參考這裏

C: HELO trendmicro.com
S: 250 smtp.example.com, I am glad to meet you
C: MAIL FROM:<rance_jen@trendmicro.com>
S: 250 Ok
C: RCPT TO:<daniel_Yu@outlook.com>
S: 250 Ok
C: RCPT TO:<xu_hsin_liu@outlook.com>
S: 250 Ok
C: DATA
S: 354 End data with <CR><LF>.<CR><LF>
C: {EML_DATA}
C: .
S: 250 Ok: queued as 12345
C: QUIT
S: 221 Bye
{The server closes the connection}

這裡的一切資訊都是不可信的,SMTP 沒有驗證機制,因此 Client 可以聲稱自己來自任何 Domain。

以下一一解讀各個 command,以及可以檢驗的機制
參考這裏

Following commands are not case sensitive

  • HELO {HELO_DOMAIN}
    Client 連線後第一個指令,用以告訴對方自己的 Domain。

  • MAIL FROM:<{SMPT_FROM}>
    用來告訴對方這封 mail 是來自什麼 Domain

SMPT_FROM 不是信件上面顯示的 From Address,這個資訊通常只有 Mail Server 知道。

  • RCPT TO:<{SMTP_TO}>
    用來告訴對方這封 Mail 要寄給哪個 address

SMPT_FROM 不是信件上面顯示的 to Address,這個是伺服器真正用來決定要把信寄到哪個 address 的欄位。

SMTP_TO 和 eml 內的收件人(To)是可以不一樣的,所以即便收到一個收件人看起來不是自己的信也是正常的。

  • DATA
    代表接下來開始傳輸信件內容,就是以下的 EML 格式

  • QUIT
    結束傳輸。

Sender IP

由於 SMTP 是基於 TCP 的協定,Mail Server 在收到 TCP 連線時會取得 Client 的 IP,此資訊通常稱 Sender IP,由於是在 SMTP 通訊中少數較難偽造的資訊,因此常用於驗證信件合法性,下面會再提到。

Email 格式解讀

詳細 spec 可以參考這裡

以下是一個 eml

User-Agent: Microsoft-MacOutlook/10.10.2.180910
Date: Fri, 8 Jan 2021 10:48:18 +0800
Subject: Test Subject
From: "Rance Jen (Joker-TW)" <rance_jen@trendmicro.com>
To: "Daniel Yu (RoleModel-TW)" <daniel_Yu@outlook.com>
CC: "Xu Hsin Liu (Monster-TW)" <xu_hsin_liu@outlook.com>
Message-ID: <E84DE573-6B9F-4C1C-95BD-E6CD40633C14@outlook.com>
Thread-Topic: Test Subject
MIME-Version: 1.0
Content-type: multipart/alternative;
	boundary="B_3692947709_2010432344"

--B_3692947709_2010432344
Content-type: text/plain;
	charset="UTF-8"
Content-transfer-encoding: 7bit

Test body


--B_3692947709_2010432344
Content-type: text/html;
	charset="UTF-8"
Content-transfer-encoding: quoted-printable

<html xmlns:o=3D"urn:schemas-microsoft-com:office:office" xmlns:w=3D"urn:schema=
s-microsoft-com:office:word" xmlns:m=3D"http://schemas.microsoft.com/office/20=
04/12/omml" xmlns=3D"http://www.w3.org/TR/REC-html40">
<head>
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
<meta name=3D"Generator" content=3D"Microsoft Word 15 (filtered medium)">
<style><!--
/* Font Definitions */
@font-face
	{font-family:=E6=96=B0=E7=B4=B0=E6=98=8E=E9=AB=94;
	panose-1:2 2 5 0 0 0 0 0 0 0;}
@font-face
	{font-family:"Cambria Math";
	panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
	{font-family:Calibri;
	panose-1:2 15 5 2 2 2 4 3 2 4;}
@font-face
	{font-family:"\@=E6=96=B0=E7=B4=B0=E6=98=8E=E9=AB=94";
	panose-1:2 1 6 1 0 1 1 1 1 1;}
/* Style Definitions */
p.MsoNormal, li.MsoNormal, div.MsoNormal
	{margin:0cm;
	margin-bottom:.0001pt;
	font-size:12.0pt;
	font-family:"Calibri",sans-serif;}
a:link, span.MsoHyperlink
	{mso-style-priority:99;
	color:#0563C1;
	text-decoration:underline;}
a:visited, span.MsoHyperlinkFollowed
	{mso-style-priority:99;
	color:#954F72;
	text-decoration:underline;}
span.EmailStyle17
	{mso-style-type:personal-compose;
	font-family:"Calibri",sans-serif;
	color:windowtext;}
.MsoChpDefault
	{mso-style-type:export-only;
	font-family:"Calibri",sans-serif;}
@page WordSection1
	{size:612.0pt 792.0pt;
	margin:72.0pt 90.0pt 72.0pt 90.0pt;}
div.WordSection1
	{page:WordSection1;}
--></style>
</head>
<body lang=3D"ZH-TW" link=3D"#0563C1" vlink=3D"#954F72">
<div class=3D"WordSection1">
<p class=3D"MsoNormal"><span lang=3D"EN-US">Test body<o:p></o:p></span></p>
</div>
</body>
</html>


--B_3692947709_2010432344--

我們可以從上往下一點一點來解讀這封 Mail

eml header

User-Agent: Microsoft-MacOutlook/10.10.2.180910
Date: Fri, 8 Jan 2021 10:48:18 +0800
Subject: Test Subject
From: "Rance Jen (Joker-TW)" <rance_jen@outlook.com>
To: "Daniel Yu (RoleModel-TW)" <daniel_Yu@outlook.com>
CC: "Xu Hsin Liu (Monster-TW)" <xu_hsin_liu@outlook.com>
Message-ID: <E84DE573-6B9F-4C1C-95BD-E6CD40633C14@outlook.com>
Thread-Topic: Test Subject
MIME-Version: 1.0
Content-type: multipart/alternative;
	boundary="B_3692947709_2010432344"

這邊屬於 eml header,郵箱顯示的 寄件人/收件人/副本/主旨 一般都是由這裡產生的。

雖然這裡的收件人可以亂填,但大多數郵件系統都會比對 SMTP_FROM 和 EML 內的 From 是否來自相同 Domain,以及 Domain 對應的 SPF 和 Sender IP是否符合。

SPF 是一種特定格式的 DNS Record,詳細請見下方章節。

以下只列出比較常顯示的 Header

  • Date: 寄件時間
  • Subject: 標題
  • From: 寄件人
  • To: 收件人
  • CC: 副本
  • Thread-Topic: 所屬信件群組
  • MIME-Version: 信件格式版本 MIME 的 RFC
    • Content-type: 內文格式
    • boundary: 內文區塊

再強調一次,這裡的資訊都是「顯示用」的,郵件信箱實際要把信件寄給誰是按照 SMTP_TO 是判斷的。
這也是為何按下回覆信件時,我們甚至可以去修改下面顯示的過往信件內容/時間/寄件者,就是因為這些顯示的真的都只是純文字而已。

MIME boundary

MIME 將信件內文分割成多個 boundary,並且一個 boundary 可以有多個表示形式,需連在一起表示並用 --{BOUNDARY_NAME}-- 作為結尾

--B_3692947709_2010432344
Content-type: text/plain;
	charset="UTF-8"
Content-transfer-encoding: 7bit

Test body

上敘是 B_3692947709_2010432344text/plain 類型表示方式。

--B_3692947709_2010432344
Content-type: text/html;
	charset="UTF-8"
Content-transfer-encoding: quoted-printable
以下過多省略
...
...
...
--B_3692947709_2010432344--

上敘是 B_3692947709_2010432344text/html 類型表示方式。

由於現在的信箱大多支援 html email,因此寄件者通常會提供一種以上的顯示方式讓郵件顯示器來選擇。

建議

通常提供了 text/plain 的正規郵件(指非詐騙/釣魚/垃圾等信件) 被封鎖的機率較低,因為針對 text/plain 的掃描遠比 text/html 容易也更成熟,並且也可以透過比對 text/html

以 Tiktok 的驗證碼信件來說

From: TikTok <noreply@account.tiktok.com>
Message-ID: <2d8e30ff-44f9-470e-8417-8c18662ecb22.noreply@account.tiktok.com>
Subject: Verifieer jou e-posadres met TikTok
Reply-To: noreply@account.tiktok.com
To: bodommoon@gmail.com
X-Entity-ID: /iZ+76rxFgdu7mfOm3BSYQ==
Content-Type: multipart/alternative; boundary="GoBoundary"

--GoBoundary
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: 8bit

Nope, 沒有 text/plain 的部分,根據目前遇到的問題不同,建議可以在這方面再多做研究及測試。

根據目的為「避免被分類為垃圾信件」或「希望被分到主要信件」,這兩種需求的不同,參考方向也會不同。

不過以個人信箱,以下幾種創作平台的通知信件為例子。

  • ci-en
  • patreon
  • fanita
  • pixivFanbox

同樣是訊息的創作推送,同樣是非中文,僅有包含 ontent-Type: text/plain; charset=UTF-8 的 pixivFanbox 會自動被分類到「主要信件」,其他被分到了次要顯示的「最新快訊」去,以個人經驗來說確實有測試的價值。

信件驗證機制

以下三種檢驗機制都是基於 DNS 來實作的。

SPF

參考這裡
驗證發送方的可靠性

Google.com(Receiver) 收到宣稱來自 outlook.com(Sender) 的信件時,Receiver 會做去查詢對方的 txt record

dig outlook.com txt
outlook.com.            299     IN      TXT     "v=spf1 include:spf-a.outlook.com include:spf-b.outlook.com ip4:157.55.9.128/25 include:spf.protection.outlook.com include:spf-a.hotmail.com include:_spf-ssg-b.microsoft.com include:_spf-ssg-c.microsoft.com ~all"

其中這段 ip4:157.55.9.128/25 直接說明了 outlook.com 的合法 Sender IP,也可以透過 include:spf-a.hotmail.com 這種字串來指向其他 domain 的 txt record

example:

當我今天想打開 Tiktok 收到的驗證信

為什麼通過 SPF 呢? 因為

dig account.tiktok.com txt
"v=spf1 include:mailgun.org include:spf1.dm.aliyun.com include:spf.onlarksuite.com include:_netblocks.m.feishu.cn ~all"

然後取 mailgun.org 的 spf 來看

dig mailgun.org txt
"v=spf1 include:spf1.mailgun.org include:spf2.mailgun.org -all"

再取對應的 spf2.mailgun.org 的 spf 來看

dig spf2.mailgun.org txt
"v=spf1 ... ... ip4:69.72.32.0/20 ~all"

而 69.72.44.186 確實在 69.72.32.0/20 的 cidr range 之內,故 spf 認證通過。

DKIM

參考這裡
驗證內文的可靠性,完整性

DKIM 的格式參考 RFC 4871

具體可從 eml header 直接組出來,具體如下

DKIM-Signature: a=rsa-sha256; v=1; c=relaxed/relaxed; d=register.account.tiktok.com; q=dns/txt; s=krs; t=1610072789; h=Mime-Version: Content-Type: Subject: From: To: Reply-To: Message-Id: Date: Content-Transfer-Encoding: X-Feedback-Id; bh=XxxII4AYM0IUdadfjM2cN5gYFMs9HOGPdJ1KPieNa14=; b=a2Sxf2Fu/ySkkSoVF0SVMno1KbAQJbTmyU+1EE2UmdzzeGfIzmI4xjN2wRl97+vd8m4DIr0A TxVaGvcEad0oeIAA5GqytInDkXtkzPQI4NqZ/s3MFtiA/32/MDXXBYicdzfkwyY6ITKMYRDV QJck7KEo+y34aQ4kuz2Vj2FbBwk=

對應查詢指令為 dig {s}._domainkey.{d} txt
也就是 dig krs._domainkey.register.account.tiktok.com txt

就會拿到對應的 public key "k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwLL5RYgbmUEqP7mz40wLuYuiNZfuFqrEJIwNr7LpcszmY2+zF1Pa8XBgDyoVKoAMC+IcKmRUR6pso4MhR3fuwJmXCZXHMezXi0WyRdmPsGAqG++1z3Y6hxafm0LYIJaaXRQ0iZQEdZkKwn5OWmTQGpSIEi6TF4rW6H37EUar1twIDAQAB"

並用此 public key 去驗證 b=bh= 欄位的簽名即可。

DMARC

參考這裡
針對上敘 SPF/DKIM 驗證失敗信件的處理方式。

格式一樣參考 RFC 7489

一樣用案例出發

dig _dmarc.tiktok.com  txt
"v=DMARC1; p=quarantine; pct=100; rua=mailto:mailauth-reports@bytedance.com"

就代表了上面的 SPF 跟 DKIM 若是沒通過建議發到垃圾信箱去,處理的比例為 100%,並且同時把信件資料回報到 mailauth-reports@bytedance.com

ESMTP

全名是:Extended SMTP

SMTP 的擴充協議,增加了以下幾種指令。

  • EHLO
    • ESMTP 的 HELO,用來詢問對方是否支援 ESMTP
    • 後續還有 AUTH/HELP 等不同伺服器支援的不同操作
  • STARTTLS
    • 開始進行 TLS 交握,SMTP 加密的第一步,加密後方可執行 SMTP AUTH 等指令。

STARTTLS 尤其重要,如果沒有在連線後首先進行 TLS 交握,後續信件內容及認證內容都會以明文傳遞的。

email tracking

SMTP

現在常見的手法,藉由是否拉下特定圖片為 tracking 的標的

以 tiktok 驗證信為例,在整個 html mail 的最後面有

</html><img src=3D"https://tiktok.email-messaging.com/tracking/1/open/F1GLt=
796">

這目的就明顯啦,或是像 shopback 也有

<tr>
<td height=3D"1" style=3D"font-size: 1px; line-height: 1px; padding: 0px;">
<br><img src=3D"https://email.shopback.com/pub/as?_ri_=3DX0Gzc2X%3DAQpglLjH=
JlTQGsvBzd8KzfN2kGTMJBzcRzg6kygKzdJotqzg92YLFvC4dzbBhKe1KYqKzgOVXHkMX%3Dw&_=
ei_=3DEolaGGF4SNMvxFF7KucKuWPDjrbedtEC1CFTWQ01D8khfLu9ztEv0-B7m2NRhKcM-v7Af=
TLpHLoRe4D6F5Nh8t9YlTe4VcLEgkWV6tzR-DWNYaCNqtylSR9l2kiSthZTL2c8MELRRYw0."><=
/img>
</td>
</tr>

特別顯示的超小,這意圖我覺得就很明顯 la

小節

TL;DR