[インデックス 12112] ファイルの概要
このコミットは、Go言語の net/smtp パッケージにおけるSMTPクライアントの挙動を改善するものです。具体的には、SMTPサーバーとの初期接続時に EHLO コマンドを優先的に使用し、それが失敗した場合に HELO コマンドにフォールバックするロジックを導入しています。これにより、RFCに厳密に従わない一部のSMTPサーバー(例: smtp.yandex.ru)との互換性が向上し、メール送信の問題が解決されます。
コミット
commit 2110fadd12a37d0ff4e899c8d3211dacc6332c5b
Author: Russ Cox <rsc@golang.org>
Date: Tue Feb 21 16:39:02 2012 -0500
net/smtp: use EHLO then HELO
Before we were using "ESMTP" in the banner as a clue,
but that is not required by the RFC and breaks mailing
to smtp.yandex.ru.
Fixes #3045.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5687066
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2110fadd12a37d0ff4e899c8d3211dacc6332c5b
元コミット内容
net/smtp: EHLOを使い、その後HELOを使うように変更。
以前はバナーに"ESMTP"が含まれているかどうかを手がかりにしていたが、これはRFCで必須とされておらず、smtp.yandex.ruへのメール送信を妨げていた。
Issue #3045を修正。
レビュー担当者: golang-dev, bradfitz CC: golang-dev 関連チェンジリスト: https://golang.org/cl/5687066
変更の背景
この変更は、Go言語の net/smtp パッケージが特定のSMTPサーバー(具体的には smtp.yandex.ru)に対してメールを送信できないという問題(Issue #3045)を解決するために行われました。
従来の net/smtp パッケージでは、SMTPサーバーから最初に受け取るバナーメッセージ(220応答)に "ESMTP" という文字列が含まれているかどうかをチェックし、その文字列が存在する場合にのみ EHLO コマンドを使用していました。しかし、SMTPの仕様を定義するRFC(Request for Comments)では、サーバーが EHLO をサポートしている場合に、そのバナーメッセージに必ずしも "ESMTP" という文字列を含めることを義務付けていません。
smtp.yandex.ru のような一部のSMTPサーバーは、EHLO をサポートしているにもかかわらず、初期バナーに "ESMTP" を含んでいませんでした。このため、GoのSMTPクライアントは EHLO を試行せず、代わりに古い HELO コマンドを使用していました。HELO コマンドでは利用できない拡張機能(例えば認証メカニズムなど)に依存する後続の処理が失敗し、結果としてメール送信ができないという問題が発生していました。
このコミットは、この非互換性を解消し、より堅牢なSMTPクライアントの挙動を実現するために、EHLO をまず試行し、それが失敗した場合に HELO にフォールバックするという標準的なアプローチを採用しました。
前提知識の解説
このコミットを理解するためには、以下のSMTPプロトコルに関する基本的な知識が必要です。
-
SMTP (Simple Mail Transfer Protocol): 電子メールをインターネット上で転送するための標準的なプロトコルです。メールクライアントからメールサーバーへ、またはメールサーバー間でメールを送信する際に使用されます。SMTPの通信は、クライアントとサーバー間のコマンドと応答のやり取りによって行われます。
-
HELO コマンド: SMTPセッションを開始するためにクライアントがサーバーに送信する最初のコマンドです。クライアントは自身のドメイン名またはIPアドレスをサーバーに通知します。これはSMTPの初期のRFC 821で定義された基本的な挨拶コマンドです。
HELOは、基本的なSMTP接続を確立しますが、拡張機能のネゴシエーションは行いません。 -
EHLO コマンド (Extended HELO):
HELOコマンドの拡張版であり、ESMTP (Extended Simple Mail Transfer Protocol) をサポートするクライアントが使用します。EHLOコマンドを送信することで、クライアントは自身がESMTPをサポートしていることをサーバーに伝え、サーバーがサポートするSMTPサービス拡張機能のリストを要求します。サーバーは、EHLO応答として、認証 (AUTH)、TLS (STARTTLS)、メッセージサイズ宣言 (SIZE)、8BITMIMEなどの拡張機能のリストを返します。これにより、クライアントはより高度な機能を利用できるようになります。EHLOはRFC 1869で導入され、RFC 5321でサーバーでのサポートが必須となりました。 -
ESMTP (Extended Simple Mail Transfer Protocol): 元のSMTP標準に様々な拡張機能を追加したものです。ESMTPは、元のSMTPにはなかった認証、暗号化、より大きなメッセージの送信、国際化されたメールアドレスなどの機能を提供します。
EHLOコマンドは、ESMTPの機能を利用するための鍵となります。 -
RFC (Request for Comments): インターネット技術の標準や仕様を定義する文書シリーズです。IETF (Internet Engineering Task Force) によって発行されます。SMTPおよびESMTPに関連する主要なRFCには以下のようなものがあります。
- RFC 821: 元のSMTPの仕様。
- RFC 1869: ESMTPと
EHLOコマンドを定義。 - RFC 2821: RFC 821を置き換え、ESMTP形式を再定義。
- RFC 5321: SMTPの最新の定義であり、RFC 2821を更新し、
EHLOサポートを必須としました。
このコミットのポイントは、RFCが EHLO をサポートするサーバーの初期バナーに "ESMTP" という文字列を含めることを義務付けていないという点です。そのため、バナーの文字列に依存するのではなく、まず EHLO を試行し、失敗した場合に HELO にフォールバックするという、より堅牢な実装が必要とされました。
技術的詳細
このコミットが解決しようとしている問題は、SMTPクライアントがサーバーとの初期ハンドシェイクを行う際のプロトコルネゴシエーションの不備にありました。
-
旧来の挙動:
net/smtpパッケージのNewClient関数は、SMTPサーバーから220応答(サービス準備完了のバナーメッセージ)を受け取った後、そのメッセージの内容を解析していました。具体的には、strings.Contains(msg, "ESMTP")を使用して、バナーメッセージに "ESMTP" という文字列が含まれているかどうかをチェックしていました。- もし "ESMTP" が含まれていれば、クライアントは
c.ehlo()を呼び出してEHLOコマンドを送信していました。 - "ESMTP" が含まれていなければ、クライアントは
c.helo()を呼び出してHELOコマンドを送信していました。
- もし "ESMTP" が含まれていれば、クライアントは
-
問題点: このアプローチの問題点は、RFCがSMTPサーバーに対して、
EHLOをサポートしている場合に必ずしも220応答のバナーに "ESMTP" という文字列を含めることを義務付けていない点にありました。多くのSMTPサーバーは慣習的に "ESMTP" を含んでいましたが、smtp.yandex.ruのように、EHLOをサポートしているにもかかわらず、この文字列を含まないサーバーも存在しました。 このようなサーバーに対しては、GoのSMTPクライアントは "ESMTP" 文字列を見つけられないため、HELOコマンドを送信してしまいます。その結果、サーバーが提供するEHLO経由でしか利用できない拡張機能(例えば、認証メカニズム)がクライアントに認識されず、後続の認証やメール送信処理が失敗する原因となっていました。 -
解決策: このコミットでは、この問題を解決するために、プロトコルネゴシエーションのロジックをより堅牢なものに変更しました。新しいロジックは以下の通りです。
- まず、サーバーからの220応答を受け取った後、バナーメッセージの内容に関わらず、常に
c.ehlo()を呼び出してEHLOコマンドを試行します。 EHLOコマンドの送信が成功した場合(エラーが返されない場合)、そのまま処理を続行します。EHLOコマンドの送信が失敗した場合(エラーが返された場合)、そのエラーを無視し、代わりにc.helo()を呼び出してHELOコマンドを試行します。
- まず、サーバーからの220応答を受け取った後、バナーメッセージの内容に関わらず、常に
この「まず EHLO を試行し、失敗したら HELO にフォールバックする」というアプローチは、SMTPプロトコルにおける一般的なベストプラクティスであり、RFC 5321にも準拠しています。これにより、バナーメッセージの内容に依存することなく、サーバーが EHLO をサポートしていればその機能を利用し、サポートしていなければ HELO に切り替えることで、幅広いSMTPサーバーとの互換性を確保できるようになりました。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、以下の2つのファイルにあります。
-
src/pkg/net/smtp/smtp.goNewClient関数のロジックが変更されました。
-
src/pkg/net/smtp/smtp_test.gofaker構造体にnet.Connインターフェースのメソッドが追加されました。TestNewClientおよびTestNewClient2という新しいテストケースが追加されました。
src/pkg/net/smtp/smtp.go の変更点
--- a/src/pkg/net/smtp/smtp.go
+++ b/src/pkg/net/smtp/smtp.go
@@ -50,15 +50,14 @@ func Dial(addr string) (*Client, error) {
// server name to be used when authenticating.
func NewClient(conn net.Conn, host string) (*Client, error) {
text := textproto.NewConn(conn)
- _, msg, err := text.ReadResponse(220)
+ _, _, err := text.ReadResponse(220)
if err != nil {
text.Close()
return nil, err
}
c := &Client{Text: text, conn: conn, serverName: host}
- if strings.Contains(msg, "ESMTP") {
- err = c.ehlo()
- } else {
+ err = c.ehlo()
+ if err != nil {
err = c.helo()
}
return c, err
src/pkg/net/smtp/smtp_test.go の変更点
--- a/src/pkg/net/smtp/smtp_test.go
+++ b/src/pkg/net/smtp/smtp_test.go
@@ -8,9 +8,11 @@ import (
"bufio"
"bytes"
"io"
+ "net"
"net/textproto"
"strings"
"testing"
+ "time"
)
type authTest struct {
@@ -59,9 +61,12 @@ type faker struct {
io.ReadWriter
}
-func (f faker) Close() error {
- return nil
-}
+func (f faker) Close() error { return nil }
+func (f faker) LocalAddr() net.Addr { return nil }
+func (f faker) RemoteAddr() net.Addr { return nil }
+func (f faker) SetDeadline(time.Time) error { return nil }
+func (f faker) SetReadDeadline(time.Time) error { return nil }
+func (f faker) SetWriteDeadline(time.Time) error { return nil }
func TestBasic(t *testing.T) {
basicServer = strings.Join(strings.Split(basicServer, "\n"), "\r\n")
@@ -180,3 +185,87 @@ Goodbye.
.
QUIT
`
+
+func TestNewClient(t *testing.T) {
+ newClientServer = strings.Join(strings.Split(newClientServer, "\n"), "\r\n")
+ newClientClient = strings.Join(strings.Split(newClientClient, "\n"), "\r\n")
+
+ var cmdbuf bytes.Buffer
+ bcmdbuf := bufio.NewWriter(&cmdbuf)
+ out := func() string {
+ bcmdbuf.Flush()
+ return cmdbuf.String()
+ }
+ var fake faker
+ fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(newClientServer)), bcmdbuf)
+ c, err := NewClient(fake, "fake.host")
+ if err != nil {
+ t.Fatalf("NewClient: %v\n(after %v)", err, out())
+ }
+ if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" {
+ t.Fatalf("Expected AUTH supported")
+ }
+ if ok, _ := c.Extension("DSN"); ok {
+ t.Fatalf("Shouldn't support DSN")
+ }
+ if err := c.Quit(); err != nil {
+ t.Fatalf("QUIT failed: %s", err)
+ }
+
+ actualcmds := out()
+ if newClientClient != actualcmds {
+ t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, newClientClient)
+ }
+}
+
+var newClientServer = `220 hello world
+250-mx.google.com at your service
+250-SIZE 35651584
+250-AUTH LOGIN PLAIN
+250 8BITMIME
+221 OK
+`
+
+var newClientClient = `EHLO localhost
+QUIT
+`
+
+func TestNewClient2(t *testing.T) {
+ newClient2Server = strings.Join(strings.Split(newClient2Server, "\n"), "\r\n")
+ newClient2Client = strings.Join(strings.Split(newClient2Client, "\n"), "\r\n")
+
+ var cmdbuf bytes.Buffer
+ bcmdbuf := bufio.NewWriter(&cmdbuf)
+ var fake faker
+ fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(newClient2Server)), bcmdbuf)
+ c, err := NewClient(fake, "fake.host")
+ if err != nil {
+ t.Fatalf("NewClient: %v", err)
+ }
+ if ok, _ := c.Extension("DSN"); ok {
+ t.Fatalf("Shouldn't support DSN")
+ }
+ if err := c.Quit(); err != nil {
+ t.Fatalf("QUIT failed: %s", err)
+ }
+
+ bcmdbuf.Flush()
+ actualcmds := cmdbuf.String()
+ if newClient2Client != actualcmds {
+ t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, newClient2Client)
+ }
+}
+
+var newClient2Server = `220 hello world
+502 EH?
+250-mx.google.com at your service
+250-SIZE 35651584
+250-AUTH LOGIN PLAIN
+250 8BITMIME
+221 OK
+`
+
+var newClient2Client = `EHLO localhost
+HELO localhost
+QUIT
+`
コアとなるコードの解説
src/pkg/net/smtp/smtp.go の変更点
NewClient 関数は、SMTPクライアントの新しいインスタンスを作成し、サーバーとの初期ハンドシェイクを行います。
-
変更前:
_, msg, err := text.ReadResponse(220) // ... if strings.Contains(msg, "ESMTP") { err = c.ehlo() } else { err = c.helo() }このコードでは、サーバーからの220応答メッセージ (
msg) を解析し、"ESMTP" という文字列が含まれているかどうかでehlo()を呼び出すかhelo()を呼び出すかを決定していました。これが、前述の問題の原因となっていました。 -
変更後:
_, _, err := text.ReadResponse(220) // msg変数は不要になったため破棄 // ... err = c.ehlo() // まずEHLOを試行 if err != nil { // EHLOが失敗した場合 err = c.helo() // HELOにフォールバック }この変更により、
NewClient関数はまずc.ehlo()を呼び出してEHLOコマンドを試行します。ehlo()メソッドがエラーを返した場合(つまり、サーバーがEHLOをサポートしていない、または何らかの理由でEHLOコマンドの処理に失敗したと判断された場合)、そのエラーを無視してc.helo()を呼び出し、HELOコマンドにフォールバックします。このロジックは、SMTPプロトコルのベストプラクティスに沿ったものであり、より堅牢なクライアントの挙動を実現します。
src/pkg/net/smtp/smtp_test.go の変更点
テストファイルには、net.Conn インターフェースを模倣するための faker 構造体への追加と、新しいテストケースが導入されています。
-
faker構造体への追加:net.Connインターフェースは、ネットワーク接続を表すGoの標準インターフェースです。net/smtpパッケージのNewClient関数はnet.Conn型の引数を取ります。テストにおいて実際のネットワーク接続を確立する代わりに、faker構造体を使ってnet.Connの挙動をシミュレートしています。 変更前はClose()メソッドしか実装されていませんでしたが、LocalAddr(),RemoteAddr(),SetDeadline(),SetReadDeadline(),SetWriteDeadline()といったnet.Connインターフェースの他のメソッドも追加されました。これらはテストの目的上、単にnilやnilエラーを返すダミーの実装ですが、これによりfakerがnet.Connインターフェースを完全に満たすようになり、より広範なテストシナリオに対応できるようになります。 -
TestNewClientの追加: このテストケースは、サーバーがEHLOを正常に処理し、拡張機能(例: AUTH)を返す一般的なシナリオをシミュレートします。newClientServer変数で定義されたサーバー応答は、220バナーの後にEHLO応答として250-AUTH LOGIN PLAINなどの拡張機能を含んでいます。 テストでは、NewClientがEHLOを送信し、AUTH拡張が正しく認識されることを検証しています。newClientClient変数には、クライアントが送信すべきコマンド(EHLO localhostとQUIT)が定義されており、実際の送信コマンドと比較されます。 -
TestNewClient2の追加: このテストケースは、サーバーがEHLOコマンドに対してエラー応答(例:502 EH?)を返し、クライアントがHELOにフォールバックするシナリオをシミュレートします。newClient2Server変数で定義されたサーバー応答は、220バナーの後にEHLOに対して502 EH?というエラーを返します。その後、HELOに対しては正常な応答を返します。 テストでは、NewClientがまずEHLOを試行し、エラーを受け取った後にHELOにフォールバックし、最終的にQUITコマンドを送信する一連の挙動を検証しています。newClient2Client変数には、クライアントが送信すべきコマンド(EHLO localhost、HELO localhost、QUIT)が定義されており、実際の送信コマンドと比較されます。
これらのテストケースは、EHLO と HELO のネゴシエーションロジックが意図通りに機能し、特に EHLO が失敗した場合のフォールバックメカニズムが正しく動作することを保証するために重要です。
関連リンク
- Go CL (Change List): https://golang.org/cl/5687066
- Go Issue: #3045 (https://code.google.com/p/go/issues/detail?id=3045) - このコミットが修正した問題のトラッキング
参考にした情報源リンク
- SMTP (Simple Mail Transfer Protocol) - Wikipedia: https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol
- HELO - Computer Hope: https://www.computerhope.com/jargon/h/helo.htm
- EHLO - Computer Hope: https://www.computerhope.com/jargon/e/ehlo.htm
- ESMTP - Wikipedia: https://en.wikipedia.org/wiki/Extended_Simple_Mail_Transfer_Protocol
- RFC 5321 - Simple Mail Transfer Protocol: https://datatracker.ietf.org/doc/html/rfc5321
- SMTP Commands - Mailtrap: https://mailtrap.io/blog/smtp-commands/
- SMTP vs ESMTP: What's the Difference? - Mystrika: https://mystrika.com/blog/smtp-vs-esmtp
- What is the difference between HELO and EHLO in SMTP? - Stack Overflow: https://stackoverflow.com/questions/1007797/what-is-the-difference-between-helo-and-ehlo-in-smtp
- SMTP Commands - Samlogic: https://www.samlogic.net/articles/smtp-commands-reference.htm
- SMTP - Microsoft Learn: https://learn.microsoft.com/en-us/windows/win32/winsock/smtp-commands
- SMTP - Broadcom: https://techdocs.broadcom.com/us/en/symantec-security-software/email-security/brightmail-gateway/10-6/about-the-product/smtp-commands.html
- SMTP - curl.se: https://curl.se/libcurl/c/CURLOPT_MAIL_AUTH.html
- SMTP - Mailtrap: https://mailtrap.io/blog/smtp-protocol/
- SMTP - GitHub: https://github.com/mailhog/MailHog/blob/master/docs/SMTP.md
- SMTP - Server Fault: https://serverfault.com/questions/103600/what-is-the-difference-between-helo-and-ehlo-in-smtp
- SMTP - SMTP2GO: https://www.smtp2go.com/blog/smtp-commands/
- SMTP - MailSlurp: https://www.mailslurp.com/blog/smtp-commands/
- SMTP - Lenovo: https://support.lenovo.com/us/en/solutions/ht507000-smtp-commands
- SMTP - YP.TO: https://yp.to/smtp-commands/