Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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コマンドで送信するホスト名をカスタマイズできるようにしています。

  1. Client 構造体の変更:

    • localName string: EHLO/HELOコマンドで使用するクライアントのホスト名を保持する新しいフィールドです。NewClient 関数でデフォルト値として localhost が設定されます。
    • didHello bool: EHLO/HELO交換が既に行われたかどうかを示すフラグです。これにより、Hello メソッドが複数回呼び出されたり、他のSMTPコマンドの後に呼び出されたりするのを防ぎます。
    • helloError error: EHLO/HELO交換中に発生したエラーを保持します。これにより、hello() メソッドが複数回呼び出されても、最初のエラーが保持され、不必要な再試行を防ぎます。
  2. NewClient 関数の変更:

    • NewClient 関数は、Client 構造体の初期化時に localNamelocalhost に設定するようになりました。以前は、NewClient の中でEHLO/HELOが自動的に実行されていましたが、この変更により、EHLO/HELOの実行は hello() メソッドに委譲され、Hello() メソッドが明示的に呼び出されるまで遅延されるようになりました。
  3. hello() メソッドの追加:

    • このプライベートメソッドは、EHLO/HELO交換を一度だけ実行するためのロジックをカプセル化します。
    • didHello フラグを使用して、既に交換が行われている場合は何もせず、helloError を返します。
    • 最初に ehlo() を試行し、失敗した場合は helo() を試行します。これにより、EHLOをサポートしない古いSMTPサーバーとの互換性が維持されます。
  4. Hello(localName string) error メソッドの追加:

    • このパブリックメソッドは、ユーザーがEHLO/HELOコマンドで送信するホスト名を明示的に設定するために使用されます。
    • didHellotrue の場合(既にEHLO/HELO交換が行われている場合)、エラーを返します。これは、Hello メソッドが他のSMTPコマンドの前に呼び出される必要があるという制約を強制するためです。
    • 引数で受け取った localNamec.localName に設定し、hello() メソッドを呼び出してEHLO/HELO交換を実行します。
  5. helo() および ehlo() メソッドの変更:

    • これらのメソッドは、ハードコードされていた localhost の代わりに、c.localName フィールドを使用するように変更されました。これにより、Hello メソッドで設定されたカスタムホスト名がEHLO/HELOコマンドで送信されるようになります。
  6. 他の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

  1. 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
    }
    
  2. 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() が呼び出されていたが、削除された)

  3. 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
    }
    
  4. 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()
    }
    
  5. 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 へ変更
    	// ... 既存のコード ...
    }
    
  6. 他のメソッドへの 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 フィールドは、クライアントが自身を識別するために使用するホスト名を格納します。didHellohelloError は、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() の呼び出し: StartTLSMail などの他のSMTPコマンドの前に c.hello() を呼び出すことで、ユーザーが Hello メソッドを明示的に呼び出さなかった場合でも、これらのコマンドが実行される前にEHLO/HELO交換が自動的に行われることが保証されます。これは、SMTPプロトコルの要件を満たすための重要な変更です。

これらの変更により、Goの net/smtp パッケージは、より現実世界のSMTPサーバーとの互換性と柔軟性を提供できるようになりました。

関連リンク

参考にした情報源リンク