[インデックス 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.go
NewClient
関数のロジックが変更されました。
-
src/pkg/net/smtp/smtp_test.go
faker
構造体に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/