1// Copyright 2010 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package smtp
6
7import (
8	"bufio"
9	"bytes"
10	"io"
11	"net"
12	"net/textproto"
13	"strings"
14	"testing"
15	"time"
16)
17
18type authTest struct {
19	auth       Auth
20	challenges []string
21	name       string
22	responses  []string
23}
24
25var authTests = []authTest{
26	{PlainAuth("", "user", "pass", "testserver"), []string{}, "PLAIN", []string{"\x00user\x00pass"}},
27	{PlainAuth("foo", "bar", "baz", "testserver"), []string{}, "PLAIN", []string{"foo\x00bar\x00baz"}},
28	{CRAMMD5Auth("user", "pass"), []string{"<123456.1322876914@testserver>"}, "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}},
29}
30
31func TestAuth(t *testing.T) {
32testLoop:
33	for i, test := range authTests {
34		name, resp, err := test.auth.Start(&ServerInfo{"testserver", true, nil})
35		if name != test.name {
36			t.Errorf("#%d got name %s, expected %s", i, name, test.name)
37		}
38		if !bytes.Equal(resp, []byte(test.responses[0])) {
39			t.Errorf("#%d got response %s, expected %s", i, resp, test.responses[0])
40		}
41		if err != nil {
42			t.Errorf("#%d error: %s", i, err)
43		}
44		for j := range test.challenges {
45			challenge := []byte(test.challenges[j])
46			expected := []byte(test.responses[j+1])
47			resp, err := test.auth.Next(challenge, true)
48			if err != nil {
49				t.Errorf("#%d error: %s", i, err)
50				continue testLoop
51			}
52			if !bytes.Equal(resp, expected) {
53				t.Errorf("#%d got %s, expected %s", i, resp, expected)
54				continue testLoop
55			}
56		}
57	}
58}
59
60func TestAuthPlain(t *testing.T) {
61	auth := PlainAuth("foo", "bar", "baz", "servername")
62
63	tests := []struct {
64		server *ServerInfo
65		err    string
66	}{
67		{
68			server: &ServerInfo{Name: "servername", TLS: true},
69		},
70		{
71			// Okay; explicitly advertised by server.
72			server: &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}},
73		},
74		{
75			server: &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
76			err:    "unencrypted connection",
77		},
78		{
79			server: &ServerInfo{Name: "attacker", TLS: true},
80			err:    "wrong host name",
81		},
82	}
83	for i, tt := range tests {
84		_, _, err := auth.Start(tt.server)
85		got := ""
86		if err != nil {
87			got = err.Error()
88		}
89		if got != tt.err {
90			t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
91		}
92	}
93}
94
95type faker struct {
96	io.ReadWriter
97}
98
99func (f faker) Close() error                     { return nil }
100func (f faker) LocalAddr() net.Addr              { return nil }
101func (f faker) RemoteAddr() net.Addr             { return nil }
102func (f faker) SetDeadline(time.Time) error      { return nil }
103func (f faker) SetReadDeadline(time.Time) error  { return nil }
104func (f faker) SetWriteDeadline(time.Time) error { return nil }
105
106func TestBasic(t *testing.T) {
107	server := strings.Join(strings.Split(basicServer, "\n"), "\r\n")
108	client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
109
110	var cmdbuf bytes.Buffer
111	bcmdbuf := bufio.NewWriter(&cmdbuf)
112	var fake faker
113	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
114	c := &Client{Text: textproto.NewConn(fake), localName: "localhost"}
115
116	if err := c.helo(); err != nil {
117		t.Fatalf("HELO failed: %s", err)
118	}
119	if err := c.ehlo(); err == nil {
120		t.Fatalf("Expected first EHLO to fail")
121	}
122	if err := c.ehlo(); err != nil {
123		t.Fatalf("Second EHLO failed: %s", err)
124	}
125
126	c.didHello = true
127	if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" {
128		t.Fatalf("Expected AUTH supported")
129	}
130	if ok, _ := c.Extension("DSN"); ok {
131		t.Fatalf("Shouldn't support DSN")
132	}
133
134	if err := c.Mail("user@gmail.com"); err == nil {
135		t.Fatalf("MAIL should require authentication")
136	}
137
138	if err := c.Verify("user1@gmail.com"); err == nil {
139		t.Fatalf("First VRFY: expected no verification")
140	}
141	if err := c.Verify("user2@gmail.com"); err != nil {
142		t.Fatalf("Second VRFY: expected verification, got %s", err)
143	}
144
145	// fake TLS so authentication won't complain
146	c.tls = true
147	c.serverName = "smtp.google.com"
148	if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")); err != nil {
149		t.Fatalf("AUTH failed: %s", err)
150	}
151
152	if err := c.Mail("user@gmail.com"); err != nil {
153		t.Fatalf("MAIL failed: %s", err)
154	}
155	if err := c.Rcpt("golang-nuts@googlegroups.com"); err != nil {
156		t.Fatalf("RCPT failed: %s", err)
157	}
158	msg := `From: user@gmail.com
159To: golang-nuts@googlegroups.com
160Subject: Hooray for Go
161
162Line 1
163.Leading dot line .
164Goodbye.`
165	w, err := c.Data()
166	if err != nil {
167		t.Fatalf("DATA failed: %s", err)
168	}
169	if _, err := w.Write([]byte(msg)); err != nil {
170		t.Fatalf("Data write failed: %s", err)
171	}
172	if err := w.Close(); err != nil {
173		t.Fatalf("Bad data response: %s", err)
174	}
175
176	if err := c.Quit(); err != nil {
177		t.Fatalf("QUIT failed: %s", err)
178	}
179
180	bcmdbuf.Flush()
181	actualcmds := cmdbuf.String()
182	if client != actualcmds {
183		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
184	}
185}
186
187var basicServer = `250 mx.google.com at your service
188502 Unrecognized command.
189250-mx.google.com at your service
190250-SIZE 35651584
191250-AUTH LOGIN PLAIN
192250 8BITMIME
193530 Authentication required
194252 Send some mail, I'll try my best
195250 User is valid
196235 Accepted
197250 Sender OK
198250 Receiver OK
199354 Go ahead
200250 Data OK
201221 OK
202`
203
204var basicClient = `HELO localhost
205EHLO localhost
206EHLO localhost
207MAIL FROM:<user@gmail.com> BODY=8BITMIME
208VRFY user1@gmail.com
209VRFY user2@gmail.com
210AUTH PLAIN AHVzZXIAcGFzcw==
211MAIL FROM:<user@gmail.com> BODY=8BITMIME
212RCPT TO:<golang-nuts@googlegroups.com>
213DATA
214From: user@gmail.com
215To: golang-nuts@googlegroups.com
216Subject: Hooray for Go
217
218Line 1
219..Leading dot line .
220Goodbye.
221.
222QUIT
223`
224
225func TestNewClient(t *testing.T) {
226	server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n")
227	client := strings.Join(strings.Split(newClientClient, "\n"), "\r\n")
228
229	var cmdbuf bytes.Buffer
230	bcmdbuf := bufio.NewWriter(&cmdbuf)
231	out := func() string {
232		bcmdbuf.Flush()
233		return cmdbuf.String()
234	}
235	var fake faker
236	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
237	c, err := NewClient(fake, "fake.host")
238	if err != nil {
239		t.Fatalf("NewClient: %v\n(after %v)", err, out())
240	}
241	if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" {
242		t.Fatalf("Expected AUTH supported")
243	}
244	if ok, _ := c.Extension("DSN"); ok {
245		t.Fatalf("Shouldn't support DSN")
246	}
247	if err := c.Quit(); err != nil {
248		t.Fatalf("QUIT failed: %s", err)
249	}
250
251	actualcmds := out()
252	if client != actualcmds {
253		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
254	}
255}
256
257var newClientServer = `220 hello world
258250-mx.google.com at your service
259250-SIZE 35651584
260250-AUTH LOGIN PLAIN
261250 8BITMIME
262221 OK
263`
264
265var newClientClient = `EHLO localhost
266QUIT
267`
268
269func TestNewClient2(t *testing.T) {
270	server := strings.Join(strings.Split(newClient2Server, "\n"), "\r\n")
271	client := strings.Join(strings.Split(newClient2Client, "\n"), "\r\n")
272
273	var cmdbuf bytes.Buffer
274	bcmdbuf := bufio.NewWriter(&cmdbuf)
275	var fake faker
276	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
277	c, err := NewClient(fake, "fake.host")
278	if err != nil {
279		t.Fatalf("NewClient: %v", err)
280	}
281	if ok, _ := c.Extension("DSN"); ok {
282		t.Fatalf("Shouldn't support DSN")
283	}
284	if err := c.Quit(); err != nil {
285		t.Fatalf("QUIT failed: %s", err)
286	}
287
288	bcmdbuf.Flush()
289	actualcmds := cmdbuf.String()
290	if client != actualcmds {
291		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
292	}
293}
294
295var newClient2Server = `220 hello world
296502 EH?
297250-mx.google.com at your service
298250-SIZE 35651584
299250-AUTH LOGIN PLAIN
300250 8BITMIME
301221 OK
302`
303
304var newClient2Client = `EHLO localhost
305HELO localhost
306QUIT
307`
308
309func TestHello(t *testing.T) {
310
311	if len(helloServer) != len(helloClient) {
312		t.Fatalf("Hello server and client size mismatch")
313	}
314
315	for i := 0; i < len(helloServer); i++ {
316		server := strings.Join(strings.Split(baseHelloServer+helloServer[i], "\n"), "\r\n")
317		client := strings.Join(strings.Split(baseHelloClient+helloClient[i], "\n"), "\r\n")
318		var cmdbuf bytes.Buffer
319		bcmdbuf := bufio.NewWriter(&cmdbuf)
320		var fake faker
321		fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
322		c, err := NewClient(fake, "fake.host")
323		if err != nil {
324			t.Fatalf("NewClient: %v", err)
325		}
326		c.localName = "customhost"
327		err = nil
328
329		switch i {
330		case 0:
331			err = c.Hello("customhost")
332		case 1:
333			err = c.StartTLS(nil)
334			if err.Error() == "502 Not implemented" {
335				err = nil
336			}
337		case 2:
338			err = c.Verify("test@example.com")
339		case 3:
340			c.tls = true
341			c.serverName = "smtp.google.com"
342			err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
343		case 4:
344			err = c.Mail("test@example.com")
345		case 5:
346			ok, _ := c.Extension("feature")
347			if ok {
348				t.Errorf("Expected FEATURE not to be supported")
349			}
350		case 6:
351			err = c.Reset()
352		case 7:
353			err = c.Quit()
354		case 8:
355			err = c.Verify("test@example.com")
356			if err != nil {
357				err = c.Hello("customhost")
358				if err != nil {
359					t.Errorf("Want error, got none")
360				}
361			}
362		default:
363			t.Fatalf("Unhandled command")
364		}
365
366		if err != nil {
367			t.Errorf("Command %d failed: %v", i, err)
368		}
369
370		bcmdbuf.Flush()
371		actualcmds := cmdbuf.String()
372		if client != actualcmds {
373			t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
374		}
375	}
376}
377
378var baseHelloServer = `220 hello world
379502 EH?
380250-mx.google.com at your service
381250 FEATURE
382`
383
384var helloServer = []string{
385	"",
386	"502 Not implemented\n",
387	"250 User is valid\n",
388	"235 Accepted\n",
389	"250 Sender ok\n",
390	"",
391	"250 Reset ok\n",
392	"221 Goodbye\n",
393	"250 Sender ok\n",
394}
395
396var baseHelloClient = `EHLO customhost
397HELO customhost
398`
399
400var helloClient = []string{
401	"",
402	"STARTTLS\n",
403	"VRFY test@example.com\n",
404	"AUTH PLAIN AHVzZXIAcGFzcw==\n",
405	"MAIL FROM:<test@example.com>\n",
406	"",
407	"RSET\n",
408	"QUIT\n",
409	"VRFY test@example.com\n",
410}
411
412func TestSendMail(t *testing.T) {
413	server := strings.Join(strings.Split(sendMailServer, "\n"), "\r\n")
414	client := strings.Join(strings.Split(sendMailClient, "\n"), "\r\n")
415	var cmdbuf bytes.Buffer
416	bcmdbuf := bufio.NewWriter(&cmdbuf)
417	l, err := net.Listen("tcp", "127.0.0.1:0")
418	if err != nil {
419		t.Fatalf("Unable to to create listener: %v", err)
420	}
421	defer l.Close()
422
423	// prevent data race on bcmdbuf
424	var done = make(chan struct{})
425	go func(data []string) {
426
427		defer close(done)
428
429		conn, err := l.Accept()
430		if err != nil {
431			t.Errorf("Accept error: %v", err)
432			return
433		}
434		defer conn.Close()
435
436		tc := textproto.NewConn(conn)
437		for i := 0; i < len(data) && data[i] != ""; i++ {
438			tc.PrintfLine(data[i])
439			for len(data[i]) >= 4 && data[i][3] == '-' {
440				i++
441				tc.PrintfLine(data[i])
442			}
443			if data[i] == "221 Goodbye" {
444				return
445			}
446			read := false
447			for !read || data[i] == "354 Go ahead" {
448				msg, err := tc.ReadLine()
449				bcmdbuf.Write([]byte(msg + "\r\n"))
450				read = true
451				if err != nil {
452					t.Errorf("Read error: %v", err)
453					return
454				}
455				if data[i] == "354 Go ahead" && msg == "." {
456					break
457				}
458			}
459		}
460	}(strings.Split(server, "\r\n"))
461
462	err = SendMail(l.Addr().String(), nil, "test@example.com", []string{"other@example.com"}, []byte(strings.Replace(`From: test@example.com
463To: other@example.com
464Subject: SendMail test
465
466SendMail is working for me.
467`, "\n", "\r\n", -1)))
468
469	if err != nil {
470		t.Errorf("%v", err)
471	}
472
473	<-done
474	bcmdbuf.Flush()
475	actualcmds := cmdbuf.String()
476	if client != actualcmds {
477		t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
478	}
479}
480
481var sendMailServer = `220 hello world
482502 EH?
483250 mx.google.com at your service
484250 Sender ok
485250 Receiver ok
486354 Go ahead
487250 Data ok
488221 Goodbye
489`
490
491var sendMailClient = `EHLO localhost
492HELO localhost
493MAIL FROM:<test@example.com>
494RCPT TO:<other@example.com>
495DATA
496From: test@example.com
497To: other@example.com
498Subject: SendMail test
499
500SendMail is working for me.
501.
502QUIT
503`
504