【Cloud TIPS】雲端主機與 Email 寄送 by qrtt1 | CodeData
top

【Cloud TIPS】雲端主機與 Email 寄送

分享:

 【AWS TIPS】帳單的管理 << 前情

著使用雲端供應商的選擇越來越多,除了最早接觸的 AWS 外,現在還有 Microsoft Azure 與 Google Cloud Platform (以下簡稱 Azure 與 GCP)可供選擇。

不過,將程式往上搬之後會發現有些不同,特別是使用到寄發 Email 的相關功能,就可能動彈不得。例如 AWS 有限制地(一個定額下無需申請開通)允許你在主機上發送 Email,Azure 建議您採用第三方服務 How to Send Email Using SendGrid from Java 進行 Email 發送,而 GCP 嚴格限制 smtp 常用的 port 網路流出的流量。

在 GCP 文件 Sending Email from an Instance 提到:

Google Compute Engine does not allow outbound connections on ports 25, 465, and 587 but you can still set up your instances to send mail through ports 587 and 465 using servers provided through partner services, such as SendGrid. This document discusses how to set up your instances to send email using SendGrid.

有許多情況我們會直接在 Server 上使用 local 的 smtp server 寄發 Email 作為偵測到系統的通知方式之一,下例程式就簡單將 Javamail API 包裝起來,使用 localhost 上的 smtp 發信:

MailBuilder builder = new MailBuilder();
builder.from("gce@qrtt1.org")
    .to("chingyichan.tw@gmail.com")
    .withTitle("寄一封 Mail")
    .hasContent("我在 GCE 裡");
builder.send();
Nov 21, 2014 8:12:23 AM cloud.codedata.util.MailSender sendMail
INFO: notify to: [chingyichan.tw@gmail.com]
Nov 21, 2014 8:12:23 AM cloud.codedata.util.MailSender sendMail
INFO: subject: 寄一封 Mail
Nov 21, 2014 8:12:23 AM cloud.codedata.util.MailSender sendMail
INFO: body: 我在 GCE 裡

當上面這簡單的功能在受限的環境裡,行為就會跟你想得不太一樣,有些情況會 Hang 住,有些情況像是正常寄出,但完全沒有人收到信,底下是在 GCE 上使用 CentOS 6.5 寄信後產生的 Log:

[root@my-server MT]# cat /var/log/maillog
Nov 21 08:12:23 my-server postfix/smtpd[13493]: connect from localhost[127.0.0.1]
Nov 21 08:12:23 my-server postfix/smtpd[13493]: 8053C23A3A: client=localhost[127.0.0.1]
Nov 21 08:12:23 my-server postfix/cleanup[13499]: 8053C23A3A: message-id=<1655137665.0.1416528743543.JavaMail.root@my-server>
Nov 21 08:12:23 my-server postfix/smtpd[13493]: disconnect from localhost[127.0.0.1]
Nov 21 08:12:23 my-server postfix/qmgr[1282]: 8053C23A3A: from=<gce@qrtt1.org>, size=567, nrcpt=1 (queue active)
Nov 21 08:12:23 my-server postfix/smtp[13502]: connect to gmail-smtp-in.l.google.com[2404:6800:4008:c04::1a]:25: Network is unreachable

做這個簡單的實驗是想驗證 GCE 文件與實際測試的結果是否一致,目前的情況看來確定無法透過 PORT 25 將信寄出,Network is unreachable 顯然是主因。

您也可以試試不同的方法,像是直接使用 Linux 指令寄信。結果會是一致的,至少能當作「不是程式本身的問題」的一種驗算:

[root@my-server MT]# sendmail -t -f gce@qrtt1.org
To: chingyichan.tw@gmail.com
Subject: [GCE] 現在放棄,比賽就結束惹
安西教練,我想發從 GCE 下發 Mail 啊!
.
Nov 21 08:26:21 my-server postfix/smtp[14028]: connect to gmail-smtp-in.l.google.com[74.125.204.26]:25: Connection timed out
Nov 21 08:26:21 my-server postfix/smtp[14028]: connect to alt1.gmail-smtp-in.l.google.com[2607:f8b0:400e:c03::1a]:25: Network is unreachable
Nov 21 08:26:32 my-server postfix/pickup[11638]: E4CD823A7B: uid=0 from=<gce@qrtt1.org>
Nov 21 08:26:32 my-server postfix/cleanup[14024]: E4CD823A7B: message-id=<20141121002632.E4CD823A7B@my-server.localdomain>
Nov 21 08:26:32 my-server postfix/qmgr[1282]: E4CD823A7B: from=<gce@qrtt1.org>, size=389, nrcpt=1 (queue active)
Nov 21 08:26:32 my-server postfix/smtp[14066]: connect to gmail-smtp-in.l.google.com[2404:6800:4008:c04::1a]:25: Network is unreachable

在 GCE 上觀察到的現象如同文件描述的,無法使用預設的 PORT 25 進行 Email 寄送。

檢視 Email 寄送替代方案文件

在前頭的實驗,我們簡單驗證了目前管理比較嚴格的 GCE 確實無法使用 PORT 25 寄送 Email。另外,在文件中有提到使用 partner 的服務 PORT 587 與 PORT 465 則是允許的(當然,這些被管制的 PORT 之外的 PORT 也是可行的)。所以單純就規則來想,我們的替代方案有:

  1. 使用文件建議的 partner service 與在白名單內的 PORT 號
  2. 使用公司內的 smtp server,但必需要增 PORT 號,不能是預設的 25,或會被管制的 PORT 號。
  3. 使用其他雲端 send mail service,不限於文件內推薦的 partner service,使用白名單的 PORT 號,或非管制的 PORT 號。

每一個方法都是可行的,但延伸的問題程式的寫法需要變更。事實上不然,這其實是一種「錯覺」,雲端服務商在文件中貼心地準備了給各種語言開發者的實作方法,若沒有全盤理解就直接將解法套上,那就會有必需得修改程式碼的錯覺:

  1. 像是 GCE 的 Sending Email from an Instance
  2. 或是 Azure 給各種語言開發者的專頁 How to Send Email Using SendGrid from Java

若靜下心來多看一點文件,發現似乎有系統層面的解法:

  1. GCE 文件 Sending Email from an Instance 的 Postfix 段落
  2. AWS SES 文件 Integrating Amazon SES with Postfix

簡單地說,我們可以使用 local 的 smtp server 寄信,透過文件的指引完成設定後,程式發給 local smtp server 寄信的 request 會透過其他 Send Mail Service 寄送,而不是像原本的方式透過 local smtp server 寄發 Email。這只是一種將「改變」由程式實作轉移至系統設定的部分,透過 Postfix Relay 需要寄送的 Email 讓我們保持程式不變,依然符合限制條件。

由於本身是開發者對於系統方面的解法不太熟悉,第一回看到挺神奇的,於是做了實驗親手看看真的是會動的。例如:

  1. 在 GCE 上使用 Postfix 後端接 AWS SES(SES 除了 PORT 25 亦支援 587 與 2587
  2. 在 GCE 上使用 Postfix 後端接 SendGrid

方案選擇

上面列出的方法都是可能的選項,而到底該選哪一種呢?就如多數情況,得適情況而定。你可以列出你需要考量的各種因素:

  1. 可以修改系統設定
  2. 不可以修改系統設定
  3. 可以修改程式
  4. 不可以修改程式

對於是否要修改作業系統設定得看 System Administrator 的態度而定,若是純粹人工的那就得去請求他的協助,若是有將伺服器設定整合至 DevOps 工具內能自動化安裝完成。相反的情況,像是沒有足夠的權限能改變系統設定那就只能考慮修改程式的情況了。修改程式則是最不得已的選項,若是要改它那得思考著透過引入間接層來隔離實作的相依性,或是透過設定檔決定選用的實作方案。像是在受限的環境下,則選擇特定的實作,或是決定無法何種環境,都用第三方的 Email 寄送服務。

JavaMail

在系統無法修改,又覺得修改程式難以進行情況下,我們針對 Java Application 有另外的建議。在 JavaMail 的規格有制定出一套擴充的規範,只要依規格加入適當的檔案,就能取代原有的 SMTP 實作。

JavaMail 定義出 Provider Registry 的規範,讓第三方實作者可以動態增加實作。最初認識這個功能是使用 mock-javamail 專案進行寄發 EMail 的單元測試,用起來很「神奇」因為不用修改程式碼,就能讓 Mock Object 收到寄信的資訊,於是好奇研究了一下它的程式碼,再對照一下 JavaMail 規格,原來這就是 Provider Registry:

透過 Provider Registry 的方式「加入」新的寄送方法,讓你無需修改舊有的 JavaMail 寄送。以下是 mock-javamail 的 META-INF/javamail.providers

# See http://javamail.kenai.com/nonav/javadocs/javax/mail/Session.html

protocol=smtp; type=transport; class=org.jvnet.mock_javamail.MockTransport; vendor=java.net mock-javamail project;
protocol=pop3; type=store; class=org.jvnet.mock_javamail.MockStore; vendor=java.net mock-javamail project;
protocol=imap; type=store; class=org.jvnet.mock_javamail.MockStore; vendor=java.net mock-javamail project;

它實作了 3 種 protocol 的 provider,其中 smtp 是我們替換掉預設實作可以參考的範本,也就是 org.jvnet.mock_javamail.MockTransport 類別,它的實作也相當簡單:

package org.jvnet.mock_javamail;

import javax.mail.Address;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.URLName;
import javax.mail.internet.MimeMessage;

/**
 * Mock {@link Transport} to deliver to {@link Mailbox}.
 *
 * @author Kohsuke Kawaguchi
 */
public class MockTransport extends Transport {
    public MockTransport(Session session, URLName urlname) {
        super(session, urlname);
    }

    public void connect(String host, int port, String user, String password) throws MessagingException {
        // noop
    }

    public void sendMessage(Message msg, Address[] addresses) throws MessagingException {
        for (Address a : addresses) {
            // create a copy to isolate the sender and the receiver
            Mailbox mailbox = Mailbox.get(Aliases.getInstance().resolve(a));
            if(mailbox.isError())
                throw new MessagingException("Simulated error sending message to "+a);
            mailbox.add(new MimeMessage((MimeMessage)msg));
        }
    }
}

開發者主要的工作是覆寫 sendMessage 方法,而 connect 有 JavaMail 預設實作,將它的行為覆寫為空的是避免它直接連原來的 PORT 25。例如,使用 SendGrid 或 mailgun 等第三方服務都有簡單的 Restful API 能呼叫,它們多數為標準的 http 或 https 都是在非流量管制的名單內。

寄 Mail 有這麼難嗎

曾有人說

「雲端」是個照妖鏡,當你的服務搬上雲端或搬不上雲端時,問題的原形就出現了。

在試著處理 GCE 上無法寄送 Mail 的過程,發現了許多自身專案的問題。雖然寄發 Mail 是個簡單的功能,也許就是太簡單,而沒有受到良好的照顧。套句同事說過的「每個專案都自己的寄 Email 實作」,這無疑的是跨專案的 duplicated code。在一開始,覺得寫了 JavaMail Provider 能在自己管理的專案搞定寄送問題很得意,但問題發生在其他開發者紛紛表示他們無法在新的環境寄送 Email,才知道我們「原形畢露」。於是才回頭仔細看看文件,有系統層面的解法(修改 Postfix 由它 relay)。

這篇寄發 Email 的分享,試著將我們試過的方法與簡單的評估方式記錄,並在最後讓大家知道為何我們無法使用需要修改程式碼的解決之道。

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

關於作者

目前在一家網路應用軟體公司擔任開發工作,對多媒體處理與雲端應用充滿興趣,工作之餘亦常整理開發經驗分享於網路或於社群活動時進行分享。

熱門論壇文章

熱門技術文章