[インデックス 14661] ファイルの概要
このコミットは、Go言語の net/smtp
パッケージにおいて、SMTPクライアントがEHLO/HELOコマンドで自身を識別する際に使用するホスト名を明示的に設定できる Hello
メソッドを追加するものです。これにより、デフォルトの localhost
ではなく、任意のホスト名を指定できるようになり、より柔軟なSMTP通信が可能になります。
コミット
commit 475dee9082f740f77a9e17d2e2242e647c860f13
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date: Sun Dec 16 20:19:35 2012 -0500
net/smtp: add optional Hello method
Add a Hello method that allows clients to set the server sent in the EHLO/HELO exchange; the default remains localhost.
Based on CL 5555045 by rsc.
Fixes #4219.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6946057
---
src/pkg/net/smtp/smtp.go | 65 ++++++++++++--
src/pkg/net/smtp/smtp_test.go | 192 +++++++++++++++++++++++++++++++++++++++++-
2 files changed, 248 insertions(+), 9 deletions(-)
diff --git a/src/pkg/net/smtp/smtp.go b/src/pkg/net/smtp/smtp.go
index 59f6449f0a..4b91778770 100644
--- a/src/pkg/net/smtp/smtp.go
+++ b/src/pkg/net/smtp/smtp.go
@@ -13,6 +13,7 @@ package smtp
import (
"crypto/tls"
"encoding/base64"
+ "errors"
"io"
"net"
"net/textproto"
@@ -33,7 +34,10 @@ type Client struct {
// map of supported extensions
ext map[string]string
// supported auth mechanisms
- auth []string
+ auth []string
+ localName string // the name to use in HELO/EHLO
+ didHello bool // whether we've said HELO/EHLO
+ helloError error // the error from the hello
}
// Dial returns a new Client connected to an SMTP server at addr.
@@ -55,12 +59,33 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
text.Close()
return nil, err
}
- c := &Client{Text: text, conn: conn, serverName: host}
- err = c.ehlo()
- if err != nil {
- err = c.helo()
+ c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"}
+ return c, nil
+}
+
+// hello runs a hello exchange if needed.
+func (c *Client) hello() error {
+ if !c.didHello {
+ c.didHello = true
+ err := c.ehlo()
+ if err != nil {
+ c.helloError = c.helo()
+ }
}
- return c, err
+ return c.helloError
+}
+
+// Hello sends a HELO or EHLO to the server as the given host name.
+// Calling this method is only necessary if the client needs control
+// over the host name used. The client will introduce itself as "localhost"
+// automatically otherwise. If Hello is called, it must be called before
+// any of the other methods.
+func (c *Client) Hello(localName string) error {
+ if c.didHello {
+ return errors.New("smtp: Hello called after other methods")
+ }
+ c.localName = localName
+ return c.hello()
}
// cmd is a convenience function that sends a command and returns the response
@@ -79,14 +104,14 @@ func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, s
// server does not support ehlo.\n func (c *Client) helo() error {\n \tc.ext = nil\n-\t_, _, err := c.cmd(250, \"HELO localhost\")\n+\t_, _, err := c.cmd(250, \"HELO %s\", c.localName)\n \treturn err\n }\n \n // ehlo sends the EHLO (extended hello) greeting to the server. It\n // should be the preferred greeting for servers that support it.\n func (c *Client) ehlo() error {\n-\t_, msg, err := c.cmd(250, \"EHLO localhost\")\n+\t_, msg, err := c.cmd(250, \"EHLO %s\", c.localName)\n \tif err != nil {\n \t\treturn err\n \t}\n@@ -113,6 +138,9 @@ func (c *Client) ehlo() error {\n // StartTLS sends the STARTTLS command and encrypts all further communication.\n // Only servers that advertise the STARTTLS extension support this function.\n func (c *Client) StartTLS(config *tls.Config) error {\n+\tif err := c.hello(); err != nil {\n+\t\treturn err\n+\t}\n \t_, _, err := c.cmd(220, \"STARTTLS\")\n \tif err != nil {\n \t\treturn err\n@@ -128,6 +156,9 @@ func (c *Client) StartTLS(config *tls.Config) error {\n // does not necessarily indicate an invalid address. Many servers\n // will not verify addresses for security reasons.\n func (c *Client) Verify(addr string) error {\n+\tif err := c.hello(); err != nil {\n+\t\treturn err\n+\t}\n \t_, _, err := c.cmd(250, \"VRFY %s\", addr)\n \treturn err\n }\n@@ -136,6 +167,9 @@ func (c *Client) Verify(addr string) error {\n // A failed authentication closes the connection.\n // Only servers that advertise the AUTH extension support this function.\n func (c *Client) Auth(a Auth) error {\n+\tif err := c.hello(); err != nil {\n+\t\treturn err\n+\t}\n \tencoding := base64.StdEncoding\n \tmech, resp, err := a.Start(&ServerInfo{c.serverName, c.tls, c.auth})\n \tif err != nil {\n@@ -178,6 +212,9 @@ func (c *Client) Auth(a Auth) error {\n // parameter.\n // This initiates a mail transaction and is followed by one or more Rcpt calls.\n func (c *Client) Mail(from string) error {\n+\tif err := c.hello(); err != nil {\n+\t\treturn err\n+\t}\n \tcmdStr := \"MAIL FROM:<%s>\"\n \tif c.ext != nil {\n \t\tif _, ok := c.ext[\"8BITMIME\"]; ok {\n@@ -227,6 +264,9 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {\n \tif err != nil {\n \t\treturn err\n \t}\n+\tif err := c.hello(); err != nil {\n+\t\treturn err\n+\t}\n \tif ok, _ := c.Extension(\"STARTTLS\"); ok {\n \t\tif err = c.StartTLS(nil); err != nil {\n \t\t\treturn err\n@@ -267,6 +307,9 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {\n // Extension also returns a string that contains any parameters the\n // server specifies for the extension.\n func (c *Client) Extension(ext string) (bool, string) {\n+\tif err := c.hello(); err != nil {\n+\t\treturn false, \"\"\n+\t}\n \tif c.ext == nil {\n \t\treturn false, \"\"\n \t}\n@@ -278,12 +321,18 @@ func (c *Client) Extension(ext string) (bool, string) {\n // Reset sends the RSET command to the server, aborting the current mail\n // transaction.\n func (c *Client) Reset() error {\n+\tif err := c.hello(); err != nil {\n+\t\treturn err\n+\t}\n \t_, _, err := c.cmd(250, \"RSET\")\n \treturn err\n }\n \n // Quit sends the QUIT command and closes the connection to the server.\n func (c *Client) Quit() error {\n+\tif err := c.hello(); err != nil {\n+\t\treturn err\n+\t}\n \t_, _, err := c.cmd(221, \"QUIT\")\n \tif err != nil {\n \t\treturn err\ndiff --git a/src/pkg/net/smtp/smtp_test.go b/src/pkg/net/smtp/smtp_test.go
index c315d185c9..2a11b26392 100644
--- a/src/pkg/net/smtp/smtp_test.go
+++ b/src/pkg/net/smtp/smtp_test.go
@@ -76,7 +76,7 @@ func TestBasic(t *testing.T) {
bcmdbuf := bufio.NewWriter(&cmdbuf)
var fake faker
fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(basicServer)), bcmdbuf)
- c := &Client{Text: textproto.NewConn(fake)}
+ c := &Client{Text: textproto.NewConn(fake), localName: "localhost"}
if err := c.helo(); err != nil {
t.Fatalf("HELO failed: %s", err)
@@ -88,6 +88,7 @@ func TestBasic(t *testing.T) {
t.Fatalf("Second EHLO failed: %s", err)
}
+ c.didHello = true
if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" {
t.Fatalf("Expected AUTH supported")
}
@@ -269,3 +270,192 @@ var newClient2Client = `EHLO localhost
HELO localhost
QUIT
`
+
+func TestHello(t *testing.T) {
+
+ if len(helloServer) != len(helloClient) {
+ t.Fatalf("Hello server and client size mismatch")
+ }
+
+ for i := 0; i < len(helloServer); i++ {
+ server := strings.Join(strings.Split(baseHelloServer+helloServer[i], "\n"), "\r\n")
+ client := strings.Join(strings.Split(baseHelloClient+helloClient[i], "\n"), "\r\n")
+ var cmdbuf bytes.Buffer
+ bcmdbuf := bufio.NewWriter(&cmdbuf)
+ var fake faker
+ fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
+ c, err := NewClient(fake, "fake.host")
+ if err != nil {
+ t.Fatalf("NewClient: %v", err)
+ }
+ c.localName = "customhost"
+ err = nil
+
+ switch i {
+ case 0:
+ err = c.Hello("customhost")
+ case 1:
+ err = c.StartTLS(nil)
+ if err.Error() == "502 Not implemented" {
+ err = nil
+ }
+ case 2:
+ err = c.Verify("test@example.com")
+ case 3:
+ c.tls = true
+ c.serverName = "smtp.google.com"
+ err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
+ case 4:
+ err = c.Mail("test@example.com")
+ case 5:
+ ok, _ := c.Extension("feature")
+ if ok {
+ t.Errorf("Expected FEATURE not to be supported")
+ }
+ case 6:
+ err = c.Reset()
+ case 7:
+ err = c.Quit()
+ case 8:
+ err = c.Verify("test@example.com")
+ if err != nil {
+ err = c.Hello("customhost")
+ if err != nil {
+ t.Errorf("Want error, got none")
+ }
+ }
+ default:
+ t.Fatalf("Unhandled command")
+ }
+
+ if err != nil {
+ t.Errorf("Command %d failed: %v", i, err)
+ }
+
+ bcmdbuf.Flush()
+ actualcmds := cmdbuf.String()
+ if client != actualcmds {
+ t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
+ }
+ }
+}
+
+var baseHelloServer = `220 hello world
+502 EH?
+250-mx.google.com at your service
+250 FEATURE
+`
+
+var helloServer = []string{
+ "",
+ "502 Not implemented\n",
+ "250 User is valid\n",
+ "235 Accepted\n",
+ "250 Sender ok\n",
+ "",
+ "250 Reset ok\n",
+ "221 Goodbye\n",
+ "250 Sender ok\n",
+}
+
+var baseHelloClient = `EHLO customhost
+HELO customhost
+`
+
+var helloClient = []string{
+ "",
+ "STARTTLS\n",
+ "VRFY test@example.com\n",
+ "AUTH PLAIN AHVzZXIAcGFzcw==\n",
+ "MAIL FROM:<test@example.com>\n",
+ "",
+ "RSET\n",
+ "QUIT\n",
+ "VRFY test@example.com\n",
+}
+
+func TestSendMail(t *testing.T) {
+ server := strings.Join(strings.Split(sendMailServer, "\n"), "\r\n")
+ client := strings.Join(strings.Split(sendMailClient, "\n"), "\r\n")
+ var cmdbuf bytes.Buffer
+ bcmdbuf := bufio.NewWriter(&cmdbuf)
+ l, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("Unable to to create listener: %v", err)
+ }
+ defer l.Close()
+
+ go func(l net.Listener, data []string, w *bufio.Writer) {
+ i := 0
+ conn, err := l.Accept()
+ if err != nil {
+ t.Log("Accept error: %v", err)
+ return
+ }
+ defer conn.Close()
+
+ tc := textproto.NewConn(conn)
+ for i < len(data) && data[i] != "" {
+ tc.PrintfLine(data[i])
+ for len(data[i]) >= 4 && data[i][3] == '-' {
+ i++
+ tc.PrintfLine(data[i])
+ }
+ read := false
+ for !read || data[i] == "354 Go ahead" {
+ msg, err := tc.ReadLine()
+ w.Write([]byte(msg + "\r\n"))
+ read = true
+ if err != nil {
+ t.Log("Read error: %v", err)
+ return
+ }
+ if data[i] == "354 Go ahead" && msg == "." {
+ break
+ }
+ }
+ i++
+ }
+ }(l, strings.Split(server, "\r\n"), bcmdbuf)
+
+ err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com
+To: other@example.com
+Subject: SendMail test
+
+SendMail is working for me.
+`, "\n", "\r\n", -1)))
+
+ if err != nil {
+ t.Errorf("%v", err)
+ }
+
+ bcmdbuf.Flush()
+ actualcmds := cmdbuf.String()
+ if client != actualcmds {
+ t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
+ }
+}
+
+var sendMailServer = `220 hello world
+502 EH?
+250 mx.google.com at your service
+250 Sender ok
+250 Receiver ok
+354 Go ahead
+250 Data ok
+221 Goodbye
+`
+
+var sendMailClient = `EHLO localhost
+HELO localhost
+MAIL FROM:<test@example.com>
+RCPT TO:<other@example.com>
+DATA
+From: test@example.com
+To: other@example.com
+Subject: SendMail test
+
+SendMail is working for me.
+.
+QUIT
+`
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/475dee9082f740f77a9e17d2e2242e647c860f13
元コミット内容
net/smtp: add optional Hello method
Add a Hello method that allows clients to set the server sent in the EHLO/HELO exchange; the default remains localhost.
Based on CL 5555045 by rsc.
Fixes #4219.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6946057
変更の背景
この変更は、Go言語の net/smtp
パッケージがSMTPサーバーとの初期ハンドシェイク(EHLO/HELOコマンド)において、クライアントが自身を識別するホスト名を常に localhost
に固定していた問題を解決するために導入されました。
元の実装では、SMTPクライアントがEHLO/HELOコマンドを送信する際、常に localhost
を自身のホスト名として通知していました。しかし、実際の運用環境では、クライアントが自身のFQDN(完全修飾ドメイン名)や、SMTPサーバーが認識できる適切なホスト名を通知することが求められる場合があります。例えば、一部のSMTPサーバーは、EHLO/HELOで送信されるホスト名が逆引きDNSと一致しない場合に、スパムと判断して接続を拒否したり、メールの送信を遅延させたりする可能性があります。
この問題は、GoのIssue #4219で報告されており、ユーザーがSMTPクライアントのEHLO/HELO名をカスタマイズしたいという要望がありました。このコミットは、その要望に応える形で、クライアントが任意のホスト名を指定できるようにする Hello
メソッドを追加することで、より柔軟なSMTP通信を可能にしています。
前提知識の解説
SMTP (Simple Mail Transfer Protocol)
SMTPは、電子メールをインターネット上で転送するためのプロトコルです。メールクライアントからメールサーバーへ、またはメールサーバー間でメールを送信する際に使用されます。SMTPはテキストベースのプロトコルであり、コマンドと応答のやり取りによって通信が行われます。
EHLO/HELOコマンド
SMTPセッションの開始時に、クライアントはサーバーに対して自身を識別するためにEHLO(Extended HELO)またはHELOコマンドを送信します。
- HELO (Hello): SMTPの初期バージョンから存在するコマンドです。クライアントは
HELO <クライアントのホスト名>
という形式で自身をサーバーに通知します。 - EHLO (Extended Hello): HELOの拡張版であり、SMTP拡張(ESMTP)をサポートするサーバーに対して使用されます。EHLOは、サーバーがサポートする拡張機能(例: 認証メカニズム、TLS、メッセージサイズ制限など)をクライアントに通知することを可能にします。クライアントは
EHLO <クライアントのホスト名>
という形式で自身を通知します。現代のSMTP通信では、EHLOが推奨されます。
これらのコマンドでクライアントが送信するホスト名は、SMTPサーバーがクライアントを識別し、必要に応じて逆引きDNSルックアップなどを行うために使用されます。デフォルトで localhost
を使用することは、開発環境やテスト環境では問題ないことが多いですが、本番環境では適切なホスト名を指定することが重要になります。
技術的詳細
このコミットは、net/smtp
パッケージの Client
構造体に新しいフィールドとメソッドを追加し、既存のEHLO/HELO処理を修正することで、クライアントがEHLO/HELOコマンドで送信するホスト名をカスタマイズできるようにしています。
-
Client
構造体の変更:localName string
: EHLO/HELOコマンドで使用するクライアントのホスト名を保持する新しいフィールドです。NewClient
関数でデフォルト値としてlocalhost
が設定されます。didHello bool
: EHLO/HELO交換が既に行われたかどうかを示すフラグです。これにより、Hello
メソッドが複数回呼び出されたり、他のSMTPコマンドの後に呼び出されたりするのを防ぎます。helloError error
: EHLO/HELO交換中に発生したエラーを保持します。これにより、hello()
メソッドが複数回呼び出されても、最初のエラーが保持され、不必要な再試行を防ぎます。
-
NewClient
関数の変更:NewClient
関数は、Client
構造体の初期化時にlocalName
をlocalhost
に設定するようになりました。以前は、NewClient
の中でEHLO/HELOが自動的に実行されていましたが、この変更により、EHLO/HELOの実行はhello()
メソッドに委譲され、Hello()
メソッドが明示的に呼び出されるまで遅延されるようになりました。
-
hello()
メソッドの追加:- このプライベートメソッドは、EHLO/HELO交換を一度だけ実行するためのロジックをカプセル化します。
didHello
フラグを使用して、既に交換が行われている場合は何もせず、helloError
を返します。- 最初に
ehlo()
を試行し、失敗した場合はhelo()
を試行します。これにより、EHLOをサポートしない古いSMTPサーバーとの互換性が維持されます。
-
Hello(localName string) error
メソッドの追加:- このパブリックメソッドは、ユーザーがEHLO/HELOコマンドで送信するホスト名を明示的に設定するために使用されます。
didHello
がtrue
の場合(既にEHLO/HELO交換が行われている場合)、エラーを返します。これは、Hello
メソッドが他のSMTPコマンドの前に呼び出される必要があるという制約を強制するためです。- 引数で受け取った
localName
をc.localName
に設定し、hello()
メソッドを呼び出してEHLO/HELO交換を実行します。
-
helo()
およびehlo()
メソッドの変更:- これらのメソッドは、ハードコードされていた
localhost
の代わりに、c.localName
フィールドを使用するように変更されました。これにより、Hello
メソッドで設定されたカスタムホスト名がEHLO/HELOコマンドで送信されるようになります。
- これらのメソッドは、ハードコードされていた
-
他のSMTPコマンドへの
hello()
呼び出しの追加:StartTLS
,Verify
,Auth
,Mail
,Extension
,Reset
,Quit
といった、SMTPセッションの初期化後に実行される可能性のあるほとんどのクライアントメソッドの冒頭に、c.hello()
の呼び出しが追加されました。- これにより、これらのコマンドが実行される前に、EHLO/HELO交換がまだ行われていない場合は自動的に実行されることが保証されます。これは、
Hello
メソッドが明示的に呼び出されなかった場合でも、クライアントが正しく自身を識別できるようにするためのフォールバックメカニズムとして機能します。
これらの変更により、net/smtp
クライアントは、デフォルトの localhost
を使用するか、または Hello
メソッドを呼び出すことでカスタムホスト名を指定するかの選択肢を持つことになります。
コアとなるコードの変更箇所
src/pkg/net/smtp/smtp.go
-
Client
構造体へのフィールド追加:type Client struct { // ... 既存のフィールド ... auth []string localName string // the name to use in HELO/EHLO didHello bool // whether we've said HELO/EHLO helloError error // the error from the hello }
-
NewClient
関数の変更:func NewClient(conn net.Conn, host string) (*Client, error) { // ... 既存のコード ... c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost"} return c, nil }
(以前はここで
c.ehlo()
またはc.helo()
が呼び出されていたが、削除された) -
hello()
メソッドの追加:func (c *Client) hello() error { if !c.didHello { c.didHello = true err := c.ehlo() if err != nil { c.helloError = c.helo() } } return c.helloError }
-
Hello()
メソッドの追加:func (c *Client) Hello(localName string) error { if c.didHello { return errors.New("smtp: Hello called after other methods") } c.localName = localName return c.hello() }
-
helo()
およびehlo()
メソッドの変更:func (c *Client) helo() error { c.ext = nil _, _, err := c.cmd(250, "HELO %s", c.localName) // "localhost" から c.localName へ変更 return err } func (c *Client) ehlo() error { _, msg, err := c.cmd(250, "EHLO %s", c.localName) // "localhost" から c.localName へ変更 // ... 既存のコード ... }
-
他のメソッドへの
c.hello()
呼び出しの追加:StartTLS
,Verify
,Auth
,Mail
,SendMail
(内部),Extension
,Reset
,Quit
の各メソッドの冒頭にif err := c.hello(); err != nil { return err }
が追加されました。
src/pkg/net/smtp/smtp_test.go
- 新しい
TestHello
関数が追加され、Hello
メソッドの動作と、他のSMTPコマンドがhello()
をトリガーすることを確認するテストケースが多数追加されました。 - 既存のテストケースも、
Client
構造体の初期化時にlocalName: "localhost"
を設定するように修正されました。
コアとなるコードの解説
このコミットの核心は、SMTPクライアントがEHLO/HELOコマンドで送信するホスト名をユーザーが制御できるようにすることです。
Client
構造体の拡張:localName
フィールドは、クライアントが自身を識別するために使用するホスト名を格納します。didHello
とhelloError
は、EHLO/HELO交換が一度だけ、かつ適切に行われることを保証するための状態管理に役立ちます。NewClient
の変更:NewClient
は接続を確立するだけで、EHLO/HELO交換はすぐには行いません。これにより、ユーザーがHello
メソッドを呼び出してカスタムホスト名を設定する機会が与えられます。hello()
メソッド: これは内部的なヘルパー関数であり、EHLO/HELO交換の実行をカプセル化します。この関数は、EHLOを試行し、失敗した場合はHELOにフォールバックするというロジックを含んでいます。また、didHello
フラグにより、この交換がセッション中に一度しか行われないことを保証します。Hello(localName string)
メソッド: これがユーザーが利用する主要なAPIです。このメソッドを呼び出すことで、ユーザーはEHLO/HELOコマンドで送信されるホスト名を明示的に設定できます。このメソッドは、他のSMTPコマンドが実行される前に呼び出される必要があります。helo()
とehlo()
の変更: これらのコマンドは、ハードコードされたlocalhost
の代わりに、Client
構造体のlocalName
フィールドを使用するようになりました。これにより、Hello
メソッドで設定されたカスタムホスト名が実際にSMTPサーバーに送信されます。- 他のメソッドでの
c.hello()
の呼び出し:StartTLS
やMail
などの他のSMTPコマンドの前にc.hello()
を呼び出すことで、ユーザーがHello
メソッドを明示的に呼び出さなかった場合でも、これらのコマンドが実行される前にEHLO/HELO交換が自動的に行われることが保証されます。これは、SMTPプロトコルの要件を満たすための重要な変更です。
これらの変更により、Goの net/smtp
パッケージは、より現実世界のSMTPサーバーとの互換性と柔軟性を提供できるようになりました。
関連リンク
- Go Issue #4219: https://github.com/golang/go/issues/4219
- Go Change List 6946057: https://golang.org/cl/6946057
参考にした情報源リンク
- SMTP (Simple Mail Transfer Protocol) - Wikipedia: https://ja.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol
- EHLO/HELOコマンドに関する情報 (一般的なSMTPプロトコル解説サイトなど)
- Go言語の
net/smtp
パッケージのドキュメント (コミット当時のもの、または現在のもの): https://pkg.go.dev/net/smtp (現在のドキュメントは変更が反映されている可能性があります) - Go言語のソースコードリポジトリ: https://github.com/golang/go
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (CL 5555045 by rsc の詳細を検索する場合など)