Try   HackMD

Computer Networking — 2.3 Electronic Mail in the Internet

contributed by <kaeteyaruyo>

tags: Computer Networking

電子郵件這個服務打從網際網路誕生以來就存在到現在。這是網際網路在草創期的時候最受歡迎的一個應用服務 [Segaller 1998],而且近年來又變得更精緻且功能強大,可以說是網際網路中最重要又最好用的一個應用。

就跟一般的郵政系統一樣,email 是一種非同步的 (asynchronous) 通訊媒介 —— 意思就是通信的雙方只要在自己方便的時候回信就可以了,不需要跟對方同時搭在線上一來一往。相較於紙本的信件,email 傳遞快速、方便上手,而且成本低廉。現代的 email 還有很多很強大的功能,像是夾帶附檔、超連結、HTML 格式化文字,還有內嵌圖片。

在本節,我們要來認識網際網路電子郵件最核心的應用層協定。但在我們真的鑽進去深究細節之前,我們先概略性地看一下電子郵件系統以及它有哪些關鍵元件。

Figure 2.14 概略性地看一下網際網路電子郵件系統

Figure 2.14 概略性地描述了電子郵件系統的長相。從這張圖我們可以看到他有三個主要元件:使用者代理 (user agent)信件伺服器 (mail server),以及簡單郵件傳輸協定(Simple Mail Transfer Protocol, SMTP)。現在我們用以下情境來解釋這些元件的功能:假設有一個寄件人 Alice,她要寄信給一個收信人 Bob。使用者代理讓使用者可以讀信、回信、轉發、儲存,以及寫信,例如: Microsoft Outlook 和 Apple Mail 就是常見的 email 使用者代理。當 Alice 寫好了信之後,她的使用者代理就會把信寄給她的信件伺服器,這封信會待在信件伺服器的寄出訊息隊伍中。當 Bob 想要讀信時,他的使用者代理就會從他的信件伺服器中的信箱抓取信件(到本地端)。

信件伺服器是 email 的基礎建設的核心。每一個收信人(像是 Bob),都會有一個信箱 (mailbox) 在信件伺服器中,用來管理與維護寄給他的信。一封典型的電子郵件會經過以下路徑:從寄信人的使用者代理出發,接著到寄信人的信箱,然後抵達收信人的信件伺服器,被存放到收信人的信箱。當 Bob 想要讀信時,信件伺服器上的 Bob 的信箱就需要先認証 Bob 的身份(打個帳號密碼之類的)。Alice 的信件伺服器也一定要可以應付 Bob 的信件伺服器收信失敗的狀況。如果 Alice 的信件伺服器不能成功把信送到 Bob 的信件伺服器,那 Alice 的信件伺服器就必須要先把這封信暫存在信件隊伍 (message queue) 中,稍後再嘗試重送。重送的動作通常每 30 分鐘左右會做一次,如果過了好幾天都沒有成功,那麼信件伺服器就會把那封信刪掉然後寄信跟 Alice 說。

SMTP 是 email 最重要的應用層協定。它使用 TCP 可靠資料傳輸的特性來保證信件可以從寄信人的信件伺服器寄到收信人的信件伺服器。就跟許多應用層協定一樣,SMTP 也有分客戶端(運作在寄信人的信件伺服器上)和伺服器端(運作在收信人的信件伺服器上)。不管是客戶端還是伺服器端,每一個信件伺服器都是運作 SMTP,當一台信件伺服器要寄信給別人時,他就是 SMTP 客戶端;而當他從別人那邊收到信件時,它就是 SMTP 伺服器端。

2.3.1 SMTP

SMTP,其規格定義在 RFC 5321,是網際網路電子郵件的核心。就如同前文所述,SMTP 負責把電子郵件從寄信人的信件伺服器送到收信人的信件伺服器。SMTP 比 HTTP 還要更老(SMTP 的原始 RFC 文件是在 1982 年出版的,而 SMTP 本身的出現就要追溯到更早以前去了)。雖然 SMTP 有許多很讚的特色(這個服務在網路上無所不在就是一個最好的證明),但他畢竟還是一個古老的科技,有許多網路上古時期的特徵。例如:所有電子郵件的封包內文(不是只有標頭喔)都只能使用 7-bit ASCII 編碼。這樣的限制在 1980 年代早期還算合理,因為當時的傳輸頻寬是稀有資源,不會有人用 email 傳很大的檔案附件或是圖片、音樂,甚至影片檔案。但在現在這個多媒體的世代,7-bit ASCII 的限制就讓人很頭痛了,因為所有的多媒體檔案在透過 SMTP 傳送之前都必須要被編碼成對應的 ASCII 訊息,收到的時候還要再從 ASCII 解碼回去原本的編碼才行。回想我們在 2.2 節所提到的,HTTP 並沒有要求多媒體資料必須要用 ASCII 編碼才能傳輸。

為了展示 SMTP 的基本操作,我們一樣用一個最常見的情境作為舉例:假設 Alice 要寄一封簡單的 ASCII 訊息給 Bob。

  1. Alice 打開了她的使用者代理,提供了 Bob 的信箱地址(例如:bob@someschool.edu),寫了一封信,然後要使用者代理把她的信寄出去。
  2. Alice 的使用者代理把這封信送到了她的信件伺服器,這封信被放在該伺服器的信件隊伍裏面。
  3. 跑在 Alice 的信件伺服器上的 SMTP 客戶端看到信件隊伍裡面有一封信,於是它建立了一個通往 Bob 的信件伺服器上的 SMTP 伺服器的 TCP 連線。
  4. 在完成了 SMTP 的交握之後,SMTP 客戶端把 Alice 的信送進了 TCP 連線。
  5. 在 Bob 的信件伺服器上,SMTP 的伺服器收到了信件,於是 Bob 的信件伺服器就把這封信放進 Bob 的信箱裡。
  6. Bob 某一天有空的時候,打開了他的使用者代理並讀了這封信。

以上這個情境被展示在 Figure 2.15 當中。

Figure 2.15 Alice 要寄一封信給 Bob

值得注意的一點是,SMTP 並不像其他的協定一樣,會使用中介的信件伺服器來傳送信件,就算要傳輸的兩台伺服器,一個在香港,一個在聖路易斯(在美國),SMTP 都會在這兩台電腦之間建立一個直通的 TCP 連線。因此如果 Bob 的信件伺服器沒上線,Alice 的信件伺服器就必須要等待重送的機會 —— 這封信不會被留在任何其他的中介伺服器上。

現在我們來看看 SMTP 訊息是怎麼從寄信的伺服器跑到收信的伺服器的。我們會注意到,SMTP 在很多方面都表現得像人與人面對面的互動方式。首先,SMTP 的客戶端(跑在寄信人的信件伺服器主機上)必須先建立一個通往 SMTP 的伺服器端(跑在收信人的信件伺服器主機上)埠號 25 的 TCP 連線。如果伺服器端沒上線,那麼客戶端就必須等一下再重試。一旦連線成功建立,伺服器端和客戶端就會進行應用層的交握 —— 就像人類在進行對話之前也都會先跟對方自我介紹一樣,SMTP 的客戶端和伺服器端也會在傳輸訊息之前先自我介紹一下。在這個交握的階段,SMTP 的客戶端會出示寄信人和送信人的 email 地址。一旦客戶端和伺服器端完成了自我介紹之後,客戶端就可以毫無錯誤地把信件寄給伺服器端。如果還有其他信件要送,那麼客戶端就會繼續在同樣的 TCP 連線上重複這些動作直到把信寄完,否則,這個 TCP 連線就會被關閉。

接著我們來看看一個 SMTP 訊息交換的通信紀錄範例(以下令客戶端為 C,伺服器端為 S)。客戶端的主機名稱是 crepes.fr 而伺服器端則是 hamburger.edu,以下的每一行 ASCII 文字都是真的被送進 TCP 插座當中的原文,從 TCP 連線一建立好就開始進行傳輸:

S:  220 hamburger.edu
C:  HELO crepes.fr
S:  250 Hello crepes.fr, pleased to meet you
C:  MAIL FROM: <alice@crepes.fr>
S:  250 alice@crepes.fr ... Sender ok
C:  RCPT TO: <bob@hamburger.edu>
S:  250 bob@hamburger.edu ... Recipient ok
C:  DATA
S:  354 Enter mail, end with ”.” on a line by itself
C:  Do you like ketchup?
C:  How about pickles?
C:  .
S:  250 Message accepted for delivery
C:  QUIT
S:  221 hamburger.edu closing connection

(這是什麼爛搭訕文 XDD)

在上述的例子當中,客戶端從 crepes.fr 的信件伺服器送了一條訊息("Do you like ketchup? How about pickles?")到 hamburger.edu 的信件伺服器。在對話過程中,客戶端送了五條指令:HELO (HELLO 的縮寫), MAIL FROM, RCPT TO, DATA, QUIT。這些指令的意思都很直白。客戶端還送了一條只有一個句點的訊息,用來代表他要傳送的信件內容已經結束了(在 ASCII 的術語中,每一條訊息都是以 CRLF 作為結尾的,其中 CR 代表回車,LF 代表換行)。伺服器會針對每一條指令都給予一個回應代碼,並且(有可能)會有一些英文的說明訊息。在這裡我們要提到,SMTP 會使用持續的連線:如果有很多封信要送,那麼所有的訊息都會用同一個 TCP 連線進行傳輸。對於每一封信,客戶端都會用 MAIL FROM: crepes.fr 指令作為開頭,並且用只有一個句點的一行訊息作為結尾。只有在所有信件都送出後,客戶端才會送出 QUIT 指令。

我們很推薦你使用 Telnet 直接送出一段對話內容給一個 SMTP 伺服器。你可以輸入以下指令:

$ telnet serverName 25

(沒有主機可以試 Q)

其中 serverName 是一台本地的信件伺服器的主機名稱。當你執行了這條指令,你就是在建立一條從你的主機通往該信件伺服器的 TCP 連線。當你送出這條指令,應該就會馬上收到該伺服器回覆 220。接著你就可以試試看手動送出 HELO, MAIL FROM, RCPT TO, DATA, QUIT 這些指令看看。我們也很推薦你做做看本章節的程式作業 3。在該作業中,你會試著寫出一個簡單的使用者代理,來實作 SMTP 的客戶端。這個使用者代理要可以藉由本地的信件伺服器對任何一個收信人送出 email。

2.3.2 Comparison with HTTP

我們來比較一下 SMTP 和 HTTP 之間的異同。這兩個協定都是用來從一台主機傳輸檔案到另一台主機用的:HTTP 在 Web 伺服器和客戶端(通常是瀏覽器)之間傳送檔案(被稱為物件);而 SMTP 則是在兩台信件伺服器之間傳送檔案(也就是 email 訊息)。當它們在傳輸檔案的時候,這兩個協定都是使用持續的 TCP 連線。因此可以說,這兩個協定有很多的共同之處。但它們之間也是有幾個很重要的差異:

  1. HTTP 是一個 pull protocol —— 先有某人把資訊放到 Web 伺服器上,使用者再於自己想看的時候去伺服器下載檔案。具體來說,其特徵就是「傳輸的 TCP 連線是由收信方建立的」。而 SMTP 是一個 push protocol —— 寄信方的信件伺服器負責把檔案送去收信的信件伺服器。具體來說,其特徵就是「傳輸的 TCP 連線是由送信方建立的」。
  2. 如同我們先前提到的,SMTP 規定每一條訊息,包含訊息本體在內,都必須使用 7-bit ASCII 來編碼。如果有訊息包含了不是 7-bit ASCII 的字元(像是有口音標號的法文字母之類的),或是有包含了二進位資料(例如圖片檔),那麼這些訊息都必須要先重新編碼成 7-bit ASCII。 HTTP 則沒有這些限制。
  3. 兩者在處理同時包含多種不同檔案類型(例如:文字和圖片,以及其他的多媒體類型)的訊息時,所使用的方法也有不同。就像我們在 2.2 節學到的,HTTP 把各個物件封裝在它自己的 HTTP 回應訊息內(每個物件一個訊息),而 SMTP 則是把所有的訊息都全部放在同一條訊息裏面。

2.3.3 Mail Message Formats

當 Alice 要寫一封紙本信給 Bob 時,她必須要在信封袋上寫上所有必要的相關資訊,例如:Bob 的地址,她自己的回信地址,還有寄信日期等等。同樣的,當一封 email 要被寄出時,包含在標頭中所有必要的相關資訊也都要先於信件本體被寄出。這些必要資訊包含在一系列的標頭行中,其規範定義於 RFC 5322。標頭行和信件本體之間由一個換行(也就是 CRLF)分隔。RFC 5322 明訂了信件標頭行的格式以及他們的語意。在 HTTP 中,每個標頭行都是由可讀的文字組成的,包含了一個關鍵字、一個冒號,和該標頭行的值。有些關鍵字需要有值,但有些不需要。而在 SMTP 中,所有標頭都必須要包含 From:To: 這兩行;Subject: 行可包含也可不含,當然也還有許多其他的可選欄位。重要的是,這些標頭行和先前在 2.3.1 所提到的 SMTP 指令是不一樣的(雖然他們有相同的關鍵字,像是 "from" 和 "to")。在該小節所提到的指令是 SMTP 交握協定的一部份;而這裡提到的標頭行則是信件訊息內文的一部份。

一個典型的訊息標頭會長得像這樣:

From: alice@crepes.fr
To: bob@hamburger.edu
Subject: Searching for the meaning of life.

在標頭結束後會有一行換行,接著才是訊息本體(用 ASCII 編碼)。你可以試試看用 Telnet 傳送一些包含一些標頭行(包含 Subject: 這行)的訊息到某台信件伺服器看看。你可以執行 telnet serverName 25,就像我們在 2.3.1 所說的一樣。

2.3.4 Mail Access Protocols

一旦 SMTP 成功把訊息從 Alice 的信件伺服器送到 Bob 的信件伺服器上,這封信就會躺在 Bob 的信箱裡了。在整個上述的討論過程,我們基本上都默認 Bob 接下來會登入他的信箱伺服器,然後在那台主機上執行某個可以讀信的程式來讀他收到的信。直到 1990 年代早期,這樣的作法都還是標準作法。但時至今日,收信服務基本上都已經變成了主從式架構了 —— 使用者通常會在自己的終端系統,像是辦公室的個人電腦、自己的筆電、手機等等,在這些機器上執行收信系統的客戶端,並且在上面讀信。一旦收信系統是執行在使用者自己的電腦上,使用者就可以享受更多樣化的信件內容,像是可以看多媒體訊息,可以收附檔等等。

如果說 Bob (收信人)都要在他自己的電腦上執行收信的使用者代理了,那麼把他的信件伺服器也一起放在他自己的電腦上也是很合情合理的作法。如果這麼做,那麼 Alice 的信件伺服器要寄信時,就要直接跟 Bob 的個人電腦建立連線。這樣會有個問題:信件伺服器的職責是負責管理信箱,並且執行 SMTP 的客戶端與伺服器端。如果信件伺服器跑在 Bob 的個人電腦上,那這台電腦就不可以關機、要一直連在網際網路上,因為我們不知道什麼時候會有信寄來,不這麼做就有可能會漏掉信件沒收到。這對很多網際網路使用者來說是做不到的。所以與其這麼做,我們不如把信件伺服器放在一台不會關機、持續上線的主機上,然後讓使用者只需要在個人電腦上執行使用者代理去對這台主機存取信件。這台信件伺服器的主機通常會是很多個使用者共享的(上面會有很多人的信箱),而且通常會由使用者的 ISP(像是公司或學校)來維護。

接著我們來思考看看一封 email 從寄出到送達會經過什麼樣的路徑。就像我們剛剛學到的,在這條路徑上的某處,這封信一定會被丟進 Bob 的信件伺服器當中。我們當然可以簡單地讓 Alice 的使用者代理直接透過 SMTP 把信傳過去就好了,但是通常來說,寄信人的使用者代理並不會像這樣直接對信件伺服器送信。相反的,如同 Figure 2.16 所示,Alice 的使用者代理會先透過 SMTP 把信送去她自己的信件伺服器,再由這台信件伺服器透過 SMTP(作為客戶端)把信轉傳到 Bob 的伺服器。為什麼要這麼麻煩還分兩步呢?主要是因為如果不靠信件伺服器做轉傳的話,Alice 自己的個人電腦通常不會有足夠多的資源來處理 Bob 的信件伺服器連不到的狀況。但如果先把信放在 Alice 的信件伺服器上,這台伺服器(因為不會關機)就有資源可以不斷的重送(像是每 30 分送一次),直到 Bob 的信件伺服器上線。(啊如果是 Alice 自己的信件伺服器連不到,她也會有資源可以去跟他自己的系統管理員抱怨 XD)SMTP 的 RFC 當中有規範要如何使用 SMTP 指令在多個 SMTP 伺服器之間轉傳訊息。

Figure 2.16 E-mail 協定以及當中所使用的通訊實體

但這段路還是沒走完!啊所以像 Bob 這樣的收信人要怎麼透過使用者代理把信從信件伺服器上拿回來自己的電腦上呢?要知道 Bob 的使用者代理可不能使用 SMTP 來拿信,因為拿信是一個 pull 的動作,但是 SMTP 是一個 push protocol。最後這一段路就要由特別的信件存取協定來完成。現今常用的信件存取協定有好幾個,其中包含:郵局協定第 3 版 (Post Office Protocol — Version 3, POP3)網際網路郵件存取協定 (Internet Mail Access Protocol, IMAP) 和 HTTP。

Figure 2.16 總結了電子郵件會用到的所有協定: SMTP 負責用來在寄信人和收信人的信件伺服器之間傳送信件;SMTP 也被用在將信件從寄信人的使用者代理送到寄信人自己的信件伺服器上。而信件存取協定,像是 POP3, 則是用來將信件從收信人的信件伺服器傳送到收信人的使用者代理上。

POP3

POP3 是一個超簡單的信件存取協定,它被定義在 [RFC 1939] 裏面,非常短而且簡單易讀。因為這個協定真的很簡單,所以他的功能也相對有限。POP3 的執行流程是以下這樣:

  1. 使用者代理(客戶端)開啟一個通往信件伺服器(伺服器端)埠號 110 的 TCP 連線。一旦連線建立完成,就可以執行以下三個階段:身份認証 (authorization)、交易 (transaction)、更新 (update)。
  2. 在身份認証階段,使用者代理必須傳送正確的使用者名稱和密碼以認証使用者的身份
  3. 在交易階段,使用者代理可以獲取信件、標記哪些信件要刪除、移除刪除信件的標記,還有獲取信件統計資料。
  4. 更新階段會在客戶端送出 quit 指令,結束了 POP3 的 session 之後觸發。在這個階段,信件伺服器會把那些標注了要刪除的信件給刪掉。

在 POP3 的交易過程中,使用者代理會送出指令,而伺服器則會一一回覆這些指令。回應有兩種:+OK (有時候會在伺服器傳資料給客戶端之後送出),用來表示前一個指令成功執行;-ERR,用來表示前一個指令執行時出了問題。

身份認証階段有兩個主要的指令:user<username>pass<password>。為了說明這兩個指令怎麼用,我們建議你直接 Telnet 到某個支援 POP3 的伺服器,連到埠號 110,然後送出以下指令。假設那台信件伺服器的名字叫 mailServer,你應該會看到以下內容:

$ telnet mailServer 110
+OK POP3 server ready
user bob
+OK
pass hungry
+OK user successfully logged on

如果你把指令拼錯了,那麼 POP3 伺服器就會回應 -ERR 訊息。

現在我們來看看交易階段。一個支援 POP3 的使用者代理通常可以讓使用者選擇要設定成「下載並刪除」模式還是「下載並保留」模式,而在這兩個不同的模式下,使用者代理所送出的一連串指令也會不一樣。如果是在「下載並刪除」模式,使用者代理會送出 list, retrdele 指令。舉個例子:假設使用者的信箱裡面有兩條訊息。下文中的 C 代表使用者代理,S 代表信件伺服器。整個交易過程會長得像這樣:

C: list
S: 1 498
S: 2 912
S: .
C: retr 1
S: (blah blah ...
S: .................
S: ..........blah)
S: .
C: dele 1
C: retr 2
S: (blah blah ...
S: .................
S: ..........blah)
S: .
C: dele 2
C: quit
S: +OK POP3 server signing off

使用者代理會先要求信件伺服器列出每一封信的大小,然後取得並刪除這些信件。注意到在身份認証結束後,使用者代理就只有可能使用四種指令: list, retr, delequit。這些指令的語法被定義在 RFC 1939 當中。在執行完 quit 指令之後,POP3 伺服器就會進入更新階段,並把信件 1 和 2 從信箱中刪除。

這個「下載並刪除」模式的問題在於:收信人 Bob 可能同時擁有很多個裝置,而且他可能會想在不同的裝置上閱讀這些信件,像是他辦公室的電腦、他家裡的電腦,還有他的筆電之類的。「下載並刪除」模式會導致他的信箱內容被分裂成三個部份,例如:如果他早上在辦公室的電腦收了信,那麼下午回到家他就沒辦法在自己家裡的電腦或是筆電上看那些早上收過的信了。如果是使用「下載並保留」模式,那麼使用者代理下載完這些信之後,信件還是會繼續留在信件伺服器上。如此一來,Bob 就可以在不同的機器上都讀到這些信件了。

當使用者代理和信件伺服器在進行通訊時,POP3 的伺服器端會維護一些使用者的狀態資訊。具體來說,他會紀錄下哪些使用者的信件是要被刪除的。但是 POP3 伺服器並不會將這些狀態資訊保留到不同的 session。由於不需要跨 session 來維護狀態資訊,這樣的設計大幅簡化了 POP3 伺服器的實作。

IMAP

在使用 POP3 的情況下,一旦 Bob 把信件下載到自己的機器上後,他就可以在自己的電腦上創資料夾,然後把下載下來的信放進這些資料夾當中。他可以刪除信件、把信件移動到不同資料夾,或是對信件做搜尋等。但這個範例(也就是在本地的機器上存放資料夾和信件)對於有很多台裝置的使用者來說很不方便。這些人可能會希望信件被放在一台遠端的電腦上,好讓他們可以從各個裝置來存取這些信件。這在使用 POP3 的情況下是不可能做到的,因為 POP3 協定並沒有提供任何方法讓使用者可以創遠端資料夾或是把信件放進遠端資料夾。

為了解決這個問題(還有其他的問題),IMAP 協定就被發明了,他被定義在 [RFC 3501] 中。就像 POP3 一樣,IMAP 也是一個信件存取協定。他比 POP3 功能更多,但相較起來也非常複雜(也因此他的客戶端和伺服器端實作起來就更複雜了)。

IMAP 伺服器會把每一封信都放在一個資料夾裏面。當信件進到一台伺服器時,他首先會被放在收信人的 INBOX 資料夾。接著收信人可以自己把這些信件移到使用者自己創建的資料夾中、讀這些信件,或是把他們刪掉等等。IMAP 協定有提供使用者創建資料夾、移動信件等等的指令,也有提供使用者在遠端資料夾中搜尋符合特定條件的信件的功能。要注意,跟 POP3 不一樣,IMAP 需要跨 session 維護使用者的狀態資訊,像是資料夾的名字、各個信件放在哪個資料夾等等。

IMAP 的另一個重要功能是,他允許使用者代理只取得信件的一部份元件。例如:使用者代理可以只要下載信件的標頭,或是只下載 multipart MIME 信件的其中一個 part。這個功能在使用者代理和信件伺服器之間的連線頻寬很低的時候很有用。當頻寬很低時,使用者或許不會想要把信箱裡的所有信件都下載下來,像那些很大的信件檔案(像是有包含影音內容的)他可能就不會想要下載。

Web-based E-Mail

現今有愈來愈多的使用者是透過網頁瀏覽器在收發 email 的。Hotmail 在 1990 年代引入了 Web-based 信件存取方式。到了現在,Google、Yahoo!,還有各大企業學校等也都有提供 Web-based 的信件存取。在這種服務下,email 的使用者代理就直接是網頁瀏覽器了,而使用者則是用 HTTP 來跟遠端的信箱進行通訊。當收信人想要讀信時,他的信就是透過 HTTP(而不是 POP3 或是 IMAP 這些協定)從信件伺服器傳到他自己的網頁瀏覽器中。而當寄信人想要寄信時,他的信也是透過 HTTP(而不是 SMTP)從他的網頁瀏覽器送到他的信件伺服器中。不過在信件伺服器之間的信件傳送,依然是使用 SMTP 沒有錯。


<< 2.2 The Web and HTTP | 目錄 | 2.4 DNS—The Internet’s Directory Service >>