[インデックス 16676] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/smtp
パッケージにおける認証エラーの取り扱いを改善するものです。具体的には、SMTP認証プロセス中に発生したエラーが適切に保持されず、破棄されてしまう問題を修正しています。これにより、認証失敗時のデバッグやエラーハンドリングがより正確に行えるようになります。
コミット
commit 64441d6d6605a9fbf27f163afbc359d9cb1af0cc
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date: Fri Jun 28 12:24:45 2013 -0700
net/smtp: preserve Auth errors
If authentication failed, the initial error was being thrown away.
Fixes #5700.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/10744043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/64441d6d6605a9fbf27f163afbc359d9cb1af0cc
元コミット内容
このコミットの元の内容は以下の通りです。
net/smtp: preserve Auth errors
If authentication failed, the initial error was being thrown away.
Fixes #5700.
これは、net/smtp
パッケージにおいて、認証プロセス中に発生したエラーが適切に保持されずに破棄されてしまうという問題に対する修正であることを示しています。特に、GitHub Issue #5700を解決することを目的としています。
変更の背景
この変更の背景には、Go言語のnet/smtp
パッケージがSMTPサーバーとの認証を行う際に、認証失敗時にサーバーから返されるエラーメッセージが適切に処理されず、失われてしまうという問題がありました。具体的には、Issue #5700「Truncated multiline error response from smtp.SendMail」で報告されたように、smtp.SendMail
がSMTPサーバーからの複数行にわたるエラー応答を途中で切り捨ててしまい、認証失敗の原因を特定することが困難になるという状況が発生していました。
SMTPプロトコルでは、認証プロセス中にサーバーがエラーコード(例: 535 Invalid credentials)とそれに続く詳細なメッセージを返すことがあります。しかし、既存の実装では、このエラー情報がAuth
インターフェースのNext
メソッドに渡される前に、内部で上書きされたり、適切に伝播されなかったりするケースがありました。その結果、アプリケーション側では認証が失敗したことしか分からず、具体的な失敗理由(例: 無効な認証情報、アカウントロックなど)を把握できないという問題が生じていました。
このコミットは、このエラー情報の破棄を防ぎ、認証プロセス中に発生したエラーがAuth
インターフェースを通じて適切に呼び出し元に伝達されるようにすることで、デバッグの容易性とエラーハンドリングの精度を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
-
SMTP (Simple Mail Transfer Protocol):
- 電子メールの送信に使用されるインターネット標準プロトコルです。クライアント(メール送信者)とサーバー(メールサーバー)間でメールを転送する際に使用されます。
- SMTPセッションは、通常、クライアントがサーバーに接続し、EHLO/HELOコマンドで自己紹介し、MAIL FROM、RCPT TO、DATAなどのコマンドでメールの内容を送信する流れで進行します。
- サーバーは、各コマンドに対して数値コード(例: 250 OK, 550 Requested action not taken)とテキストメッセージで応答します。
-
SMTP認証 (SMTP AUTH):
- SMTPサーバーがメール送信を許可する前に、クライアントの身元を確認するためのメカニズムです。これにより、スパム送信などを防ぎます。
- AUTHコマンドに続いて、PLAIN、LOGIN、CRAM-MD5などの認証メカニズムが使用されます。
- 認証プロセスは、クライアントとサーバー間でチャレンジ・レスポンス形式でやり取りされることがあります。例えば、サーバーがチャレンジ(例: 334 VXNlcm5hbWU6)を送り、クライアントがそれに対する応答(例: ユーザー名やパスワードのBase64エンコード)を返す、といった形です。
-
Go言語の
net/smtp
パッケージ:- Go言語でSMTPクライアントを実装するための標準ライブラリです。
Client
構造体はSMTPサーバーとの接続を管理し、EHLO、Auth、Mail、Rcpt、Dataなどのメソッドを提供します。Auth
インターフェース: SMTP認証メカニズムを抽象化するためのインターフェースです。type Auth interface { Start(server *ServerInfo) (proto string, toServer []byte, err error) Next(fromServer []byte, more bool) (toServer []byte, err error) }
Start
: 認証プロセスの開始時に呼び出され、使用する認証プロトコルと最初のクライアント応答を返します。Next
: サーバーからの応答を受け取り、次のクライアント応答を生成します。more
がtrueの場合、サーバーはさらなる応答を期待しています。このメソッドがエラーを返すと、認証プロセスは中断されます。
-
net/textproto
パッケージ:- Go言語でテキストベースのネットワークプロトコル(HTTP、SMTP、NNTPなど)を扱うための低レベルなユーティリティを提供するパッケージです。
textproto.Error
構造体は、プロトコルエラーを表すために使用されます。これには、数値コード(例: SMTPの応答コード)とメッセージが含まれます。
-
エラーハンドリングの重要性:
- Go言語では、エラーは戻り値として明示的に扱われます。関数がエラーを返す可能性がある場合、そのエラーを適切にチェックし、処理することが重要です。
- 特にネットワーク通信においては、様々な理由でエラーが発生する可能性があるため、エラーメッセージを正確に把握することは問題の診断に不可欠です。
技術的詳細
このコミットの技術的な核心は、net/smtp
パッケージのClient.Auth
メソッドにおけるエラー処理ロジックの変更にあります。
Client.Auth
メソッドは、SMTPサーバーとの認証プロセスを管理します。このプロセスは、クライアントがAUTHコマンドを送信し、サーバーからの応答に基づいてAuth
インターフェースのNext
メソッドを繰り返し呼び出すことで進行します。
変更前のコードでは、Client.Auth
メソッド内でサーバーからの応答を処理する際に、textproto.Error
型のerr
変数が使用されていました。このerr
変数は、サーバーからの応答コードがエラーを示す場合(例: 5xx番台)に設定されます。しかし、その後のa.Next(msg, code == 334)
の呼び出しにおいて、a.Next
が返すエラーが、既に設定されていたerr
変数を上書きしてしまう可能性がありました。
具体的には、以下のようなシナリオが考えられます。
- クライアントがAUTHコマンドを送信。
- サーバーが認証失敗を示すエラーコード(例: 535 "Invalid credentials")を返す。この時点で、
err
変数にはこのサーバーエラーが設定される。 - しかし、その直後に
a.Next(msg, code == 334)
が呼び出される。もしa.Next
メソッドが何らかの理由でnil
ではないエラーを返した場合、そのエラーが元のサーバーエラーを上書きしてしまう。 - 結果として、
Client.Auth
メソッドから返されるエラーは、サーバーが返した認証失敗の具体的なエラーではなく、a.Next
が生成した別のエラー、あるいはnil
になってしまう可能性があった。
この問題は、特にSMTPサーバーが認証失敗時に詳細なエラーメッセージを返す場合(例: "535 Invalid credentials\nplease see www.example.com")に顕著でした。元のコードでは、この詳細なエラーがa.Next
の呼び出しによって失われ、デバッグが困難になっていました。
このコミットでは、a.Next
メソッドを呼び出す前に、既存のerr
変数がnil
であるかどうかをチェックする条件を追加することで、この問題を解決しています。
if err == nil {
resp, err = a.Next(msg, code == 334)
}
この変更により、もしサーバーが既にエラーを返している場合(つまり、err
がnil
ではない場合)、a.Next
は呼び出されません。これにより、サーバーから返された認証失敗のエラー情報が保持され、Client.Auth
メソッドの最終的な戻り値として適切に伝播されるようになります。
また、このコミットにはテストケースTestAuthFailed
が追加されており、認証失敗時にサーバーから返されたエラーメッセージが正しくキャプチャされることを検証しています。このテストは、擬似的なSMTPサーバーとクライアントのやり取りをシミュレートし、認証失敗時のエラーメッセージが期待通りに取得できることを確認しています。
コアとなるコードの変更箇所
変更はsrc/pkg/net/smtp/smtp.go
ファイルとsrc/pkg/net/smtp/smtp_test.go
ファイルにあります。
src/pkg/net/smtp/smtp.go
--- a/src/pkg/net/smtp/smtp.go
+++ b/src/pkg/net/smtp/smtp.go
@@ -196,7 +196,9 @@ func (c *Client) Auth(a Auth) error {
default:
err = &textproto.Error{Code: code, Msg: msg64}
}
- resp, err = a.Next(msg, code == 334)
+ if err == nil {
+ resp, err = a.Next(msg, code == 334)
+ }
if err != nil {
// abort the AUTH
c.cmd(501, "*")
src/pkg/net/smtp/smtp_test.go
テストケースTestAuthFailed
が追加されています。このテストは、認証失敗時のSMTPサーバーの応答をシミュレートし、Client.Auth
が正しいエラーを返すことを検証します。
--- a/src/pkg/net/smtp/smtp_test.go
+++ b/src/pkg/net/smtp/smtp_test.go
@@ -504,3 +504,47 @@ SendMail is working for me.\n .\n QUIT\n `
+\n+func TestAuthFailed(t *testing.T) {\n+ server := strings.Join(strings.Split(authFailedServer, "\\n"), "\\r\\n")\n+ client := strings.Join(strings.Split(authFailedClient, "\\n"), "\\r\\n")\n+ var cmdbuf bytes.Buffer\n+ bcmdbuf := bufio.NewWriter(&cmdbuf)\n+ var fake faker\n+ fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)\n+ c, err := NewClient(fake, "fake.host")\n+ if err != nil {\n+ t.Fatalf("NewClient: %v", err)\n+ }\n+ defer c.Close()\n+\n+\tc.tls = true\n+\tc.serverName = "smtp.google.com"\n+\terr = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))\n+\n+\tif err == nil {\n+\t\tt.Error("Auth: expected error; got none")\n+\t} else if err.Error() != "535 Invalid credentials\\nplease see www.example.com" {\n+\t\tt.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\\nplease see www.example.com")\n+\t}\n+\n+\tbcmdbuf.Flush()\n+\tactualcmds := cmdbuf.String()\n+\tif client != actualcmds {\n+\t\tt.Errorf("Got:\\n%s\\nExpected:\\n%s", actualcmds, client)\n+\t}\n+}\n+\n+var authFailedServer = `220 hello world\n+250-mx.google.com at your service\n+250 AUTH LOGIN PLAIN\n+535-Invalid credentials\n+535 please see www.example.com\n+221 Goodbye\n+`\n+\n+var authFailedClient = `EHLO localhost\n+AUTH PLAIN AHVzZXIAcGFzcw==\n+*\n+QUIT\n+`\n```
## コアとなるコードの解説
### `src/pkg/net/smtp/smtp.go`の変更点
変更の中心は、`Client.Auth`メソッド内の以下の部分です。
```go
default:
err = &textproto.Error{Code: code, Msg: msg64}
}
- resp, err = a.Next(msg, code == 334)
+ if err == nil {
+ resp, err = a.Next(msg, code == 334)
+ }
if err != nil {
// abort the AUTH
c.cmd(501, "*")
-
変更前:
resp, err = a.Next(msg, code == 334)
- この行は、サーバーからの応答(
msg
)と、サーバーがさらなる応答を期待しているか(code == 334
、これはSMTPのチャレンジコード)に基づいて、Auth
インターフェースのNext
メソッドを呼び出します。 - 問題は、この呼び出しが常に実行され、
a.Next
が返すエラーが、その直前のerr = &textproto.Error{...}
で設定されたエラーを無条件に上書きしてしまう可能性があったことです。もしサーバーが認証失敗のエラーを返した後、a.Next
がエラーを返さなかった場合、元のサーバーエラーが失われてしまいます。
- この行は、サーバーからの応答(
-
変更後:
if err == nil { resp, err = a.Next(msg, code == 334) }
- この変更により、
a.Next
メソッドの呼び出しが条件付きになりました。 if err == nil
という条件は、「もし現時点でエラーがまだ発生していない場合のみ、a.Next
を呼び出す」という意味です。- これにより、もしSMTPサーバーが既に認証失敗のエラー(例: 535)を返しており、そのエラーが
err
変数に設定されている場合、a.Next
は呼び出されません。その結果、サーバーから返された認証失敗の具体的なエラー情報が保持され、Client.Auth
メソッドの呼び出し元に正確に伝達されるようになります。 - この修正は、エラーが「破棄される」ことを防ぎ、認証プロセスにおけるエラーハンドリングの堅牢性を向上させます。
- この変更により、
src/pkg/net/smtp/smtp_test.go
の追加テスト
TestAuthFailed
関数は、この修正が正しく機能することを検証するための重要なテストケースです。
-
authFailedServer
とauthFailedClient
:- これらは、擬似的なSMTPサーバーとクライアントのやり取りを文字列として定義しています。
authFailedServer
は、サーバーがAUTH LOGIN PLAIN
コマンドに応答して535-Invalid credentials
と535 please see www.example.com
というエラーを返すシナリオをシミュレートしています。これは、認証失敗時に詳細なエラーメッセージが返される典型的なケースです。authFailedClient
は、クライアントがEHLO
とAUTH PLAIN
コマンドを送信し、その後QUIT
する流れを示しています。
-
faker
構造体とNewClient
:- テストでは、実際のネットワーク接続の代わりに
faker
構造体を使用しています。これは、io.ReadWriter
インターフェースを実装しており、事前に定義されたサーバー応答を読み込み、クライアントのコマンドをバッファに書き込むことができます。 NewClient(fake, "fake.host")
は、この擬似的な接続を使用してsmtp.Client
インスタンスを作成します。
- テストでは、実際のネットワーク接続の代わりに
-
c.Auth(PlainAuth(...))
:PlainAuth
は、ユーザー名とパスワードを用いたプレーンテキスト認証メカニズムを実装するAuth
インターフェースのインスタンスを返します。- このテストでは、意図的に認証が失敗するように設定されています。
-
エラーの検証:
if err == nil { t.Error("Auth: expected error; got none") } else if err.Error() != "535 Invalid credentials\\nplease see www.example.com" { t.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\\nplease www.example.com") }
- この部分がテストの核心です。
c.Auth
がエラーを返さない場合はテストが失敗します。 - さらに重要なのは、返されたエラーメッセージが期待される「
535 Invalid credentials\nplease see www.example.com
」と完全に一致するかどうかを検証している点です。これにより、認証失敗時にサーバーから返された詳細なエラーメッセージが、修正によって正しく保持されていることが確認されます。
- この部分がテストの核心です。
-
コマンドの検証:
bcmdbuf.Flush() actualcmds := cmdbuf.String() if client != actualcmds { t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client) }
- この部分は、クライアントがSMTPサーバーに送信したコマンドが、期待される
authFailedClient
の文字列と一致するかどうかを検証しています。これにより、認証プロセス中のクライアントの挙動が正しいことも確認されます。
- この部分は、クライアントがSMTPサーバーに送信したコマンドが、期待される
このテストケースの追加は、修正が意図した通りに機能し、認証失敗時のエラー情報が正確に伝達されることを保証する上で不可欠です。
関連リンク
- Go Issue #5700: https://github.com/golang/go/issues/5700
- Go CL 10744043: https://golang.org/cl/10744043 (このコミットに対応するGoの変更リスト)
- Go
net/smtp
パッケージのドキュメント: https://pkg.go.dev/net/smtp
参考にした情報源リンク
- Go Issue #5700: Truncated multiline error response from smtp.SendMail - https://github.com/golang/go/issues/5700
- Go
net/smtp
package documentation - https://pkg.go.dev/net/smtp - Simple Mail Transfer Protocol (SMTP) - RFC 5321: https://datatracker.ietf.org/doc/html/rfc5321
- SMTP Service Extension for Authentication - RFC 4954: https://datatracker.ietf.org/doc/html/rfc4954
- Go
net/textproto
package documentation - https://pkg.go.dev/net/textproto - Web検索結果: "Go net/smtp Auth errors #5700" (Google Search) - 上記の「変更の背景」および「前提知識の解説」セクションで参照した情報源。