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	"crypto/tls"
11	"crypto/x509"
12	"fmt"
13	"internal/testenv"
14	"io"
15	"net"
16	"net/textproto"
17	"runtime"
18	"strings"
19	"testing"
20	"time"
21)
22
23type authTest struct {
24	auth       Auth
25	challenges []string
26	name       string
27	responses  []string
28}
29
30var authTests = []authTest{
31	{PlainAuth("", "user", "pass", "testserver"), []string{}, "PLAIN", []string{"\x00user\x00pass"}},
32	{PlainAuth("foo", "bar", "baz", "testserver"), []string{}, "PLAIN", []string{"foo\x00bar\x00baz"}},
33	{CRAMMD5Auth("user", "pass"), []string{"<123456.1322876914@testserver>"}, "CRAM-MD5", []string{"", "user 287eb355114cf5c471c26a875f1ca4ae"}},
34}
35
36func TestAuth(t *testing.T) {
37testLoop:
38	for i, test := range authTests {
39		name, resp, err := test.auth.Start(&ServerInfo{"testserver", true, nil})
40		if name != test.name {
41			t.Errorf("#%d got name %s, expected %s", i, name, test.name)
42		}
43		if !bytes.Equal(resp, []byte(test.responses[0])) {
44			t.Errorf("#%d got response %s, expected %s", i, resp, test.responses[0])
45		}
46		if err != nil {
47			t.Errorf("#%d error: %s", i, err)
48		}
49		for j := range test.challenges {
50			challenge := []byte(test.challenges[j])
51			expected := []byte(test.responses[j+1])
52			resp, err := test.auth.Next(challenge, true)
53			if err != nil {
54				t.Errorf("#%d error: %s", i, err)
55				continue testLoop
56			}
57			if !bytes.Equal(resp, expected) {
58				t.Errorf("#%d got %s, expected %s", i, resp, expected)
59				continue testLoop
60			}
61		}
62	}
63}
64
65func TestAuthPlain(t *testing.T) {
66
67	tests := []struct {
68		authName string
69		server   *ServerInfo
70		err      string
71	}{
72		{
73			authName: "servername",
74			server:   &ServerInfo{Name: "servername", TLS: true},
75		},
76		{
77			// OK to use PlainAuth on localhost without TLS
78			authName: "localhost",
79			server:   &ServerInfo{Name: "localhost", TLS: false},
80		},
81		{
82			// NOT OK on non-localhost, even if server says PLAIN is OK.
83			// (We don't know that the server is the real server.)
84			authName: "servername",
85			server:   &ServerInfo{Name: "servername", Auth: []string{"PLAIN"}},
86			err:      "unencrypted connection",
87		},
88		{
89			authName: "servername",
90			server:   &ServerInfo{Name: "servername", Auth: []string{"CRAM-MD5"}},
91			err:      "unencrypted connection",
92		},
93		{
94			authName: "servername",
95			server:   &ServerInfo{Name: "attacker", TLS: true},
96			err:      "wrong host name",
97		},
98	}
99	for i, tt := range tests {
100		auth := PlainAuth("foo", "bar", "baz", tt.authName)
101		_, _, err := auth.Start(tt.server)
102		got := ""
103		if err != nil {
104			got = err.Error()
105		}
106		if got != tt.err {
107			t.Errorf("%d. got error = %q; want %q", i, got, tt.err)
108		}
109	}
110}
111
112// Issue 17794: don't send a trailing space on AUTH command when there's no password.
113func TestClientAuthTrimSpace(t *testing.T) {
114	server := "220 hello world\r\n" +
115		"200 some more"
116	var wrote strings.Builder
117	var fake faker
118	fake.ReadWriter = struct {
119		io.Reader
120		io.Writer
121	}{
122		strings.NewReader(server),
123		&wrote,
124	}
125	c, err := NewClient(fake, "fake.host")
126	if err != nil {
127		t.Fatalf("NewClient: %v", err)
128	}
129	c.tls = true
130	c.didHello = true
131	c.Auth(toServerEmptyAuth{})
132	c.Close()
133	if got, want := wrote.String(), "AUTH FOOAUTH\r\n*\r\nQUIT\r\n"; got != want {
134		t.Errorf("wrote %q; want %q", got, want)
135	}
136}
137
138// toServerEmptyAuth is an implementation of Auth that only implements
139// the Start method, and returns "FOOAUTH", nil, nil. Notably, it returns
140// zero bytes for "toServer" so we can test that we don't send spaces at
141// the end of the line. See TestClientAuthTrimSpace.
142type toServerEmptyAuth struct{}
143
144func (toServerEmptyAuth) Start(server *ServerInfo) (proto string, toServer []byte, err error) {
145	return "FOOAUTH", nil, nil
146}
147
148func (toServerEmptyAuth) Next(fromServer []byte, more bool) (toServer []byte, err error) {
149	panic("unexpected call")
150}
151
152type faker struct {
153	io.ReadWriter
154}
155
156func (f faker) Close() error                     { return nil }
157func (f faker) LocalAddr() net.Addr              { return nil }
158func (f faker) RemoteAddr() net.Addr             { return nil }
159func (f faker) SetDeadline(time.Time) error      { return nil }
160func (f faker) SetReadDeadline(time.Time) error  { return nil }
161func (f faker) SetWriteDeadline(time.Time) error { return nil }
162
163func TestBasic(t *testing.T) {
164	server := strings.Join(strings.Split(basicServer, "\n"), "\r\n")
165	client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
166
167	var cmdbuf strings.Builder
168	bcmdbuf := bufio.NewWriter(&cmdbuf)
169	var fake faker
170	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
171	c := &Client{Text: textproto.NewConn(fake), localName: "localhost"}
172
173	if err := c.helo(); err != nil {
174		t.Fatalf("HELO failed: %s", err)
175	}
176	if err := c.ehlo(); err == nil {
177		t.Fatalf("Expected first EHLO to fail")
178	}
179	if err := c.ehlo(); err != nil {
180		t.Fatalf("Second EHLO failed: %s", err)
181	}
182
183	c.didHello = true
184	if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" {
185		t.Fatalf("Expected AUTH supported")
186	}
187	if ok, _ := c.Extension("DSN"); ok {
188		t.Fatalf("Shouldn't support DSN")
189	}
190
191	if err := c.Mail("[email protected]"); err == nil {
192		t.Fatalf("MAIL should require authentication")
193	}
194
195	if err := c.Verify("[email protected]"); err == nil {
196		t.Fatalf("First VRFY: expected no verification")
197	}
198	if err := c.Verify("[email protected]>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil {
199		t.Fatalf("VRFY should have failed due to a message injection attempt")
200	}
201	if err := c.Verify("[email protected]"); err != nil {
202		t.Fatalf("Second VRFY: expected verification, got %s", err)
203	}
204
205	// fake TLS so authentication won't complain
206	c.tls = true
207	c.serverName = "smtp.google.com"
208	if err := c.Auth(PlainAuth("", "user", "pass", "smtp.google.com")); err != nil {
209		t.Fatalf("AUTH failed: %s", err)
210	}
211
212	if err := c.Rcpt("[email protected]>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"); err == nil {
213		t.Fatalf("RCPT should have failed due to a message injection attempt")
214	}
215	if err := c.Mail("[email protected]>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n"); err == nil {
216		t.Fatalf("MAIL should have failed due to a message injection attempt")
217	}
218	if err := c.Mail("[email protected]"); err != nil {
219		t.Fatalf("MAIL failed: %s", err)
220	}
221	if err := c.Rcpt("[email protected]"); err != nil {
222		t.Fatalf("RCPT failed: %s", err)
223	}
224	msg := `From: user@gmail.com
225To: golang-nuts@googlegroups.com
226Subject: Hooray for Go
227
228Line 1
229.Leading dot line .
230Goodbye.`
231	w, err := c.Data()
232	if err != nil {
233		t.Fatalf("DATA failed: %s", err)
234	}
235	if _, err := w.Write([]byte(msg)); err != nil {
236		t.Fatalf("Data write failed: %s", err)
237	}
238	if err := w.Close(); err != nil {
239		t.Fatalf("Bad data response: %s", err)
240	}
241
242	if err := c.Quit(); err != nil {
243		t.Fatalf("QUIT failed: %s", err)
244	}
245
246	bcmdbuf.Flush()
247	actualcmds := cmdbuf.String()
248	if client != actualcmds {
249		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
250	}
251}
252
253var basicServer = `250 mx.google.com at your service
254502 Unrecognized command.
255250-mx.google.com at your service
256250-SIZE 35651584
257250-AUTH LOGIN PLAIN
258250 8BITMIME
259530 Authentication required
260252 Send some mail, I'll try my best
261250 User is valid
262235 Accepted
263250 Sender OK
264250 Receiver OK
265354 Go ahead
266250 Data OK
267221 OK
268`
269
270var basicClient = `HELO localhost
271EHLO localhost
272EHLO localhost
273MAIL FROM:<user@gmail.com> BODY=8BITMIME
274VRFY user1@gmail.com
275VRFY user2@gmail.com
276AUTH PLAIN AHVzZXIAcGFzcw==
277MAIL FROM:<user@gmail.com> BODY=8BITMIME
278RCPT TO:<golang-nuts@googlegroups.com>
279DATA
280From: user@gmail.com
281To: golang-nuts@googlegroups.com
282Subject: Hooray for Go
283
284Line 1
285..Leading dot line .
286Goodbye.
287.
288QUIT
289`
290
291func TestExtensions(t *testing.T) {
292	fake := func(server string) (c *Client, bcmdbuf *bufio.Writer, cmdbuf *strings.Builder) {
293		server = strings.Join(strings.Split(server, "\n"), "\r\n")
294
295		cmdbuf = &strings.Builder{}
296		bcmdbuf = bufio.NewWriter(cmdbuf)
297		var fake faker
298		fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
299		c = &Client{Text: textproto.NewConn(fake), localName: "localhost"}
300
301		return c, bcmdbuf, cmdbuf
302	}
303
304	t.Run("helo", func(t *testing.T) {
305		const (
306			basicServer = `250 mx.google.com at your service
307250 Sender OK
308221 Goodbye
309`
310
311			basicClient = `HELO localhost
312MAIL FROM:<user@gmail.com>
313QUIT
314`
315		)
316
317		c, bcmdbuf, cmdbuf := fake(basicServer)
318
319		if err := c.helo(); err != nil {
320			t.Fatalf("HELO failed: %s", err)
321		}
322		c.didHello = true
323		if err := c.Mail("[email protected]"); err != nil {
324			t.Fatalf("MAIL FROM failed: %s", err)
325		}
326		if err := c.Quit(); err != nil {
327			t.Fatalf("QUIT failed: %s", err)
328		}
329
330		bcmdbuf.Flush()
331		actualcmds := cmdbuf.String()
332		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
333		if client != actualcmds {
334			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
335		}
336	})
337
338	t.Run("ehlo", func(t *testing.T) {
339		const (
340			basicServer = `250-mx.google.com at your service
341250 SIZE 35651584
342250 Sender OK
343221 Goodbye
344`
345
346			basicClient = `EHLO localhost
347MAIL FROM:<user@gmail.com>
348QUIT
349`
350		)
351
352		c, bcmdbuf, cmdbuf := fake(basicServer)
353
354		if err := c.Hello("localhost"); err != nil {
355			t.Fatalf("EHLO failed: %s", err)
356		}
357		if ok, _ := c.Extension("8BITMIME"); ok {
358			t.Fatalf("Shouldn't support 8BITMIME")
359		}
360		if ok, _ := c.Extension("SMTPUTF8"); ok {
361			t.Fatalf("Shouldn't support SMTPUTF8")
362		}
363		if err := c.Mail("[email protected]"); err != nil {
364			t.Fatalf("MAIL FROM failed: %s", err)
365		}
366		if err := c.Quit(); err != nil {
367			t.Fatalf("QUIT failed: %s", err)
368		}
369
370		bcmdbuf.Flush()
371		actualcmds := cmdbuf.String()
372		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
373		if client != actualcmds {
374			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
375		}
376	})
377
378	t.Run("ehlo 8bitmime", func(t *testing.T) {
379		const (
380			basicServer = `250-mx.google.com at your service
381250-SIZE 35651584
382250 8BITMIME
383250 Sender OK
384221 Goodbye
385`
386
387			basicClient = `EHLO localhost
388MAIL FROM:<user@gmail.com> BODY=8BITMIME
389QUIT
390`
391		)
392
393		c, bcmdbuf, cmdbuf := fake(basicServer)
394
395		if err := c.Hello("localhost"); err != nil {
396			t.Fatalf("EHLO failed: %s", err)
397		}
398		if ok, _ := c.Extension("8BITMIME"); !ok {
399			t.Fatalf("Should support 8BITMIME")
400		}
401		if ok, _ := c.Extension("SMTPUTF8"); ok {
402			t.Fatalf("Shouldn't support SMTPUTF8")
403		}
404		if err := c.Mail("[email protected]"); err != nil {
405			t.Fatalf("MAIL FROM failed: %s", err)
406		}
407		if err := c.Quit(); err != nil {
408			t.Fatalf("QUIT failed: %s", err)
409		}
410
411		bcmdbuf.Flush()
412		actualcmds := cmdbuf.String()
413		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
414		if client != actualcmds {
415			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
416		}
417	})
418
419	t.Run("ehlo smtputf8", func(t *testing.T) {
420		const (
421			basicServer = `250-mx.google.com at your service
422250-SIZE 35651584
423250 SMTPUTF8
424250 Sender OK
425221 Goodbye
426`
427
428			basicClient = `EHLO localhost
429MAIL FROM:<user+��@gmail.com> SMTPUTF8
430QUIT
431`
432		)
433
434		c, bcmdbuf, cmdbuf := fake(basicServer)
435
436		if err := c.Hello("localhost"); err != nil {
437			t.Fatalf("EHLO failed: %s", err)
438		}
439		if ok, _ := c.Extension("8BITMIME"); ok {
440			t.Fatalf("Shouldn't support 8BITMIME")
441		}
442		if ok, _ := c.Extension("SMTPUTF8"); !ok {
443			t.Fatalf("Should support SMTPUTF8")
444		}
445		if err := c.Mail("user+��@gmail.com"); err != nil {
446			t.Fatalf("MAIL FROM failed: %s", err)
447		}
448		if err := c.Quit(); err != nil {
449			t.Fatalf("QUIT failed: %s", err)
450		}
451
452		bcmdbuf.Flush()
453		actualcmds := cmdbuf.String()
454		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
455		if client != actualcmds {
456			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
457		}
458	})
459
460	t.Run("ehlo 8bitmime smtputf8", func(t *testing.T) {
461		const (
462			basicServer = `250-mx.google.com at your service
463250-SIZE 35651584
464250-8BITMIME
465250 SMTPUTF8
466250 Sender OK
467221 Goodbye
468	`
469
470			basicClient = `EHLO localhost
471MAIL FROM:<user+��@gmail.com> BODY=8BITMIME SMTPUTF8
472QUIT
473`
474		)
475
476		c, bcmdbuf, cmdbuf := fake(basicServer)
477
478		if err := c.Hello("localhost"); err != nil {
479			t.Fatalf("EHLO failed: %s", err)
480		}
481		c.didHello = true
482		if ok, _ := c.Extension("8BITMIME"); !ok {
483			t.Fatalf("Should support 8BITMIME")
484		}
485		if ok, _ := c.Extension("SMTPUTF8"); !ok {
486			t.Fatalf("Should support SMTPUTF8")
487		}
488		if err := c.Mail("user+��@gmail.com"); err != nil {
489			t.Fatalf("MAIL FROM failed: %s", err)
490		}
491		if err := c.Quit(); err != nil {
492			t.Fatalf("QUIT failed: %s", err)
493		}
494
495		bcmdbuf.Flush()
496		actualcmds := cmdbuf.String()
497		client := strings.Join(strings.Split(basicClient, "\n"), "\r\n")
498		if client != actualcmds {
499			t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
500		}
501	})
502}
503
504func TestNewClient(t *testing.T) {
505	server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n")
506	client := strings.Join(strings.Split(newClientClient, "\n"), "\r\n")
507
508	var cmdbuf strings.Builder
509	bcmdbuf := bufio.NewWriter(&cmdbuf)
510	out := func() string {
511		bcmdbuf.Flush()
512		return cmdbuf.String()
513	}
514	var fake faker
515	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
516	c, err := NewClient(fake, "fake.host")
517	if err != nil {
518		t.Fatalf("NewClient: %v\n(after %v)", err, out())
519	}
520	defer c.Close()
521	if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" {
522		t.Fatalf("Expected AUTH supported")
523	}
524	if ok, _ := c.Extension("DSN"); ok {
525		t.Fatalf("Shouldn't support DSN")
526	}
527	if err := c.Quit(); err != nil {
528		t.Fatalf("QUIT failed: %s", err)
529	}
530
531	actualcmds := out()
532	if client != actualcmds {
533		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
534	}
535}
536
537var newClientServer = `220 hello world
538250-mx.google.com at your service
539250-SIZE 35651584
540250-AUTH LOGIN PLAIN
541250 8BITMIME
542221 OK
543`
544
545var newClientClient = `EHLO localhost
546QUIT
547`
548
549func TestNewClient2(t *testing.T) {
550	server := strings.Join(strings.Split(newClient2Server, "\n"), "\r\n")
551	client := strings.Join(strings.Split(newClient2Client, "\n"), "\r\n")
552
553	var cmdbuf strings.Builder
554	bcmdbuf := bufio.NewWriter(&cmdbuf)
555	var fake faker
556	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
557	c, err := NewClient(fake, "fake.host")
558	if err != nil {
559		t.Fatalf("NewClient: %v", err)
560	}
561	defer c.Close()
562	if ok, _ := c.Extension("DSN"); ok {
563		t.Fatalf("Shouldn't support DSN")
564	}
565	if err := c.Quit(); err != nil {
566		t.Fatalf("QUIT failed: %s", err)
567	}
568
569	bcmdbuf.Flush()
570	actualcmds := cmdbuf.String()
571	if client != actualcmds {
572		t.Fatalf("Got:\n%s\nExpected:\n%s", actualcmds, client)
573	}
574}
575
576var newClient2Server = `220 hello world
577502 EH?
578250-mx.google.com at your service
579250-SIZE 35651584
580250-AUTH LOGIN PLAIN
581250 8BITMIME
582221 OK
583`
584
585var newClient2Client = `EHLO localhost
586HELO localhost
587QUIT
588`
589
590func TestNewClientWithTLS(t *testing.T) {
591	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
592	if err != nil {
593		t.Fatalf("loadcert: %v", err)
594	}
595
596	config := tls.Config{Certificates: []tls.Certificate{cert}}
597
598	ln, err := tls.Listen("tcp", "127.0.0.1:0", &config)
599	if err != nil {
600		ln, err = tls.Listen("tcp", "[::1]:0", &config)
601		if err != nil {
602			t.Fatalf("server: listen: %v", err)
603		}
604	}
605
606	go func() {
607		conn, err := ln.Accept()
608		if err != nil {
609			t.Errorf("server: accept: %v", err)
610			return
611		}
612		defer conn.Close()
613
614		_, err = conn.Write([]byte("220 SIGNS\r\n"))
615		if err != nil {
616			t.Errorf("server: write: %v", err)
617			return
618		}
619	}()
620
621	config.InsecureSkipVerify = true
622	conn, err := tls.Dial("tcp", ln.Addr().String(), &config)
623	if err != nil {
624		t.Fatalf("client: dial: %v", err)
625	}
626	defer conn.Close()
627
628	client, err := NewClient(conn, ln.Addr().String())
629	if err != nil {
630		t.Fatalf("smtp: newclient: %v", err)
631	}
632	if !client.tls {
633		t.Errorf("client.tls Got: %t Expected: %t", client.tls, true)
634	}
635}
636
637func TestHello(t *testing.T) {
638
639	if len(helloServer) != len(helloClient) {
640		t.Fatalf("Hello server and client size mismatch")
641	}
642
643	for i := 0; i < len(helloServer); i++ {
644		server := strings.Join(strings.Split(baseHelloServer+helloServer[i], "\n"), "\r\n")
645		client := strings.Join(strings.Split(baseHelloClient+helloClient[i], "\n"), "\r\n")
646		var cmdbuf strings.Builder
647		bcmdbuf := bufio.NewWriter(&cmdbuf)
648		var fake faker
649		fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
650		c, err := NewClient(fake, "fake.host")
651		if err != nil {
652			t.Fatalf("NewClient: %v", err)
653		}
654		defer c.Close()
655		c.localName = "customhost"
656		err = nil
657
658		switch i {
659		case 0:
660			err = c.Hello("hostinjection>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n")
661			if err == nil {
662				t.Errorf("Expected Hello to be rejected due to a message injection attempt")
663			}
664			err = c.Hello("customhost")
665		case 1:
666			err = c.StartTLS(nil)
667			if err.Error() == "502 Not implemented" {
668				err = nil
669			}
670		case 2:
671			err = c.Verify("[email protected]")
672		case 3:
673			c.tls = true
674			c.serverName = "smtp.google.com"
675			err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
676		case 4:
677			err = c.Mail("[email protected]")
678		case 5:
679			ok, _ := c.Extension("feature")
680			if ok {
681				t.Errorf("Expected FEATURE not to be supported")
682			}
683		case 6:
684			err = c.Reset()
685		case 7:
686			err = c.Quit()
687		case 8:
688			err = c.Verify("[email protected]")
689			if err != nil {
690				err = c.Hello("customhost")
691				if err != nil {
692					t.Errorf("Want error, got none")
693				}
694			}
695		case 9:
696			err = c.Noop()
697		default:
698			t.Fatalf("Unhandled command")
699		}
700
701		if err != nil {
702			t.Errorf("Command %d failed: %v", i, err)
703		}
704
705		bcmdbuf.Flush()
706		actualcmds := cmdbuf.String()
707		if client != actualcmds {
708			t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
709		}
710	}
711}
712
713var baseHelloServer = `220 hello world
714502 EH?
715250-mx.google.com at your service
716250 FEATURE
717`
718
719var helloServer = []string{
720	"",
721	"502 Not implemented\n",
722	"250 User is valid\n",
723	"235 Accepted\n",
724	"250 Sender ok\n",
725	"",
726	"250 Reset ok\n",
727	"221 Goodbye\n",
728	"250 Sender ok\n",
729	"250 ok\n",
730}
731
732var baseHelloClient = `EHLO customhost
733HELO customhost
734`
735
736var helloClient = []string{
737	"",
738	"STARTTLS\n",
739	"VRFY [email protected]\n",
740	"AUTH PLAIN AHVzZXIAcGFzcw==\n",
741	"MAIL FROM:<[email protected]>\n",
742	"",
743	"RSET\n",
744	"QUIT\n",
745	"VRFY [email protected]\n",
746	"NOOP\n",
747}
748
749func TestSendMail(t *testing.T) {
750	server := strings.Join(strings.Split(sendMailServer, "\n"), "\r\n")
751	client := strings.Join(strings.Split(sendMailClient, "\n"), "\r\n")
752	var cmdbuf strings.Builder
753	bcmdbuf := bufio.NewWriter(&cmdbuf)
754	l, err := net.Listen("tcp", "127.0.0.1:0")
755	if err != nil {
756		t.Fatalf("Unable to create listener: %v", err)
757	}
758	defer l.Close()
759
760	// prevent data race on bcmdbuf
761	var done = make(chan struct{})
762	go func(data []string) {
763
764		defer close(done)
765
766		conn, err := l.Accept()
767		if err != nil {
768			t.Errorf("Accept error: %v", err)
769			return
770		}
771		defer conn.Close()
772
773		tc := textproto.NewConn(conn)
774		for i := 0; i < len(data) && data[i] != ""; i++ {
775			tc.PrintfLine("%s", data[i])
776			for len(data[i]) >= 4 && data[i][3] == '-' {
777				i++
778				tc.PrintfLine("%s", data[i])
779			}
780			if data[i] == "221 Goodbye" {
781				return
782			}
783			read := false
784			for !read || data[i] == "354 Go ahead" {
785				msg, err := tc.ReadLine()
786				bcmdbuf.Write([]byte(msg + "\r\n"))
787				read = true
788				if err != nil {
789					t.Errorf("Read error: %v", err)
790					return
791				}
792				if data[i] == "354 Go ahead" && msg == "." {
793					break
794				}
795			}
796		}
797	}(strings.Split(server, "\r\n"))
798
799	err = SendMail(l.Addr().String(), nil, "[email protected]", []string{"[email protected]>\n\rDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"}, []byte(strings.Replace(`From: test@example.com
800To: other@example.com
801Subject: SendMail test
802
803SendMail is working for me.
804`, "\n", "\r\n", -1)))
805	if err == nil {
806		t.Errorf("Expected SendMail to be rejected due to a message injection attempt")
807	}
808
809	err = SendMail(l.Addr().String(), nil, "[email protected]", []string{"[email protected]"}, []byte(strings.Replace(`From: test@example.com
810To: other@example.com
811Subject: SendMail test
812
813SendMail is working for me.
814`, "\n", "\r\n", -1)))
815
816	if err != nil {
817		t.Errorf("%v", err)
818	}
819
820	<-done
821	bcmdbuf.Flush()
822	actualcmds := cmdbuf.String()
823	if client != actualcmds {
824		t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
825	}
826}
827
828var sendMailServer = `220 hello world
829502 EH?
830250 mx.google.com at your service
831250 Sender ok
832250 Receiver ok
833354 Go ahead
834250 Data ok
835221 Goodbye
836`
837
838var sendMailClient = `EHLO localhost
839HELO localhost
840MAIL FROM:<test@example.com>
841RCPT TO:<other@example.com>
842DATA
843From: test@example.com
844To: other@example.com
845Subject: SendMail test
846
847SendMail is working for me.
848.
849QUIT
850`
851
852func TestSendMailWithAuth(t *testing.T) {
853	l, err := net.Listen("tcp", "127.0.0.1:0")
854	if err != nil {
855		t.Fatalf("Unable to create listener: %v", err)
856	}
857	defer l.Close()
858
859	errCh := make(chan error)
860	go func() {
861		defer close(errCh)
862		conn, err := l.Accept()
863		if err != nil {
864			errCh <- fmt.Errorf("Accept: %v", err)
865			return
866		}
867		defer conn.Close()
868
869		tc := textproto.NewConn(conn)
870		tc.PrintfLine("220 hello world")
871		msg, err := tc.ReadLine()
872		if err != nil {
873			errCh <- fmt.Errorf("ReadLine error: %v", err)
874			return
875		}
876		const wantMsg = "EHLO localhost"
877		if msg != wantMsg {
878			errCh <- fmt.Errorf("unexpected response %q; want %q", msg, wantMsg)
879			return
880		}
881		err = tc.PrintfLine("250 mx.google.com at your service")
882		if err != nil {
883			errCh <- fmt.Errorf("PrintfLine: %v", err)
884			return
885		}
886	}()
887
888	err = SendMail(l.Addr().String(), PlainAuth("", "user", "pass", "smtp.google.com"), "[email protected]", []string{"[email protected]"}, []byte(strings.Replace(`From: test@example.com
889To: other@example.com
890Subject: SendMail test
891
892SendMail is working for me.
893`, "\n", "\r\n", -1)))
894	if err == nil {
895		t.Error("SendMail: Server doesn't support AUTH, expected to get an error, but got none ")
896	}
897	if err.Error() != "smtp: server doesn't support AUTH" {
898		t.Errorf("Expected: smtp: server doesn't support AUTH, got: %s", err)
899	}
900	err = <-errCh
901	if err != nil {
902		t.Fatalf("server error: %v", err)
903	}
904}
905
906func TestAuthFailed(t *testing.T) {
907	server := strings.Join(strings.Split(authFailedServer, "\n"), "\r\n")
908	client := strings.Join(strings.Split(authFailedClient, "\n"), "\r\n")
909	var cmdbuf strings.Builder
910	bcmdbuf := bufio.NewWriter(&cmdbuf)
911	var fake faker
912	fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf)
913	c, err := NewClient(fake, "fake.host")
914	if err != nil {
915		t.Fatalf("NewClient: %v", err)
916	}
917	defer c.Close()
918
919	c.tls = true
920	c.serverName = "smtp.google.com"
921	err = c.Auth(PlainAuth("", "user", "pass", "smtp.google.com"))
922
923	if err == nil {
924		t.Error("Auth: expected error; got none")
925	} else if err.Error() != "535 Invalid credentials\nplease see www.example.com" {
926		t.Errorf("Auth: got error: %v, want: %s", err, "535 Invalid credentials\nplease see www.example.com")
927	}
928
929	bcmdbuf.Flush()
930	actualcmds := cmdbuf.String()
931	if client != actualcmds {
932		t.Errorf("Got:\n%s\nExpected:\n%s", actualcmds, client)
933	}
934}
935
936var authFailedServer = `220 hello world
937250-mx.google.com at your service
938250 AUTH LOGIN PLAIN
939535-Invalid credentials
940535 please see www.example.com
941221 Goodbye
942`
943
944var authFailedClient = `EHLO localhost
945AUTH PLAIN AHVzZXIAcGFzcw==
946*
947QUIT
948`
949
950func TestTLSClient(t *testing.T) {
951	if runtime.GOOS == "freebsd" || runtime.GOOS == "js" || runtime.GOOS == "wasip1" {
952		testenv.SkipFlaky(t, 19229)
953	}
954	ln := newLocalListener(t)
955	defer ln.Close()
956	errc := make(chan error)
957	go func() {
958		errc <- sendMail(ln.Addr().String())
959	}()
960	conn, err := ln.Accept()
961	if err != nil {
962		t.Fatalf("failed to accept connection: %v", err)
963	}
964	defer conn.Close()
965	if err := serverHandle(conn, t); err != nil {
966		t.Fatalf("failed to handle connection: %v", err)
967	}
968	if err := <-errc; err != nil {
969		t.Fatalf("client error: %v", err)
970	}
971}
972
973func TestTLSConnState(t *testing.T) {
974	ln := newLocalListener(t)
975	defer ln.Close()
976	clientDone := make(chan bool)
977	serverDone := make(chan bool)
978	go func() {
979		defer close(serverDone)
980		c, err := ln.Accept()
981		if err != nil {
982			t.Errorf("Server accept: %v", err)
983			return
984		}
985		defer c.Close()
986		if err := serverHandle(c, t); err != nil {
987			t.Errorf("server error: %v", err)
988		}
989	}()
990	go func() {
991		defer close(clientDone)
992		c, err := Dial(ln.Addr().String())
993		if err != nil {
994			t.Errorf("Client dial: %v", err)
995			return
996		}
997		defer c.Quit()
998		cfg := &tls.Config{ServerName: "example.com"}
999		testHookStartTLS(cfg) // set the RootCAs
1000		if err := c.StartTLS(cfg); err != nil {
1001			t.Errorf("StartTLS: %v", err)
1002			return
1003		}
1004		cs, ok := c.TLSConnectionState()
1005		if !ok {
1006			t.Errorf("TLSConnectionState returned ok == false; want true")
1007			return
1008		}
1009		if cs.Version == 0 || !cs.HandshakeComplete {
1010			t.Errorf("ConnectionState = %#v; expect non-zero Version and HandshakeComplete", cs)
1011		}
1012	}()
1013	<-clientDone
1014	<-serverDone
1015}
1016
1017func newLocalListener(t *testing.T) net.Listener {
1018	ln, err := net.Listen("tcp", "127.0.0.1:0")
1019	if err != nil {
1020		ln, err = net.Listen("tcp6", "[::1]:0")
1021	}
1022	if err != nil {
1023		t.Fatal(err)
1024	}
1025	return ln
1026}
1027
1028type smtpSender struct {
1029	w io.Writer
1030}
1031
1032func (s smtpSender) send(f string) {
1033	s.w.Write([]byte(f + "\r\n"))
1034}
1035
1036// smtp server, finely tailored to deal with our own client only!
1037func serverHandle(c net.Conn, t *testing.T) error {
1038	send := smtpSender{c}.send
1039	send("220 127.0.0.1 ESMTP service ready")
1040	s := bufio.NewScanner(c)
1041	for s.Scan() {
1042		switch s.Text() {
1043		case "EHLO localhost":
1044			send("250-127.0.0.1 ESMTP offers a warm hug of welcome")
1045			send("250-STARTTLS")
1046			send("250 Ok")
1047		case "STARTTLS":
1048			send("220 Go ahead")
1049			keypair, err := tls.X509KeyPair(localhostCert, localhostKey)
1050			if err != nil {
1051				return err
1052			}
1053			config := &tls.Config{Certificates: []tls.Certificate{keypair}}
1054			c = tls.Server(c, config)
1055			defer c.Close()
1056			return serverHandleTLS(c, t)
1057		default:
1058			t.Fatalf("unrecognized command: %q", s.Text())
1059		}
1060	}
1061	return s.Err()
1062}
1063
1064func serverHandleTLS(c net.Conn, t *testing.T) error {
1065	send := smtpSender{c}.send
1066	s := bufio.NewScanner(c)
1067	for s.Scan() {
1068		switch s.Text() {
1069		case "EHLO localhost":
1070			send("250 Ok")
1071		case "MAIL FROM:<[email protected]>":
1072			send("250 Ok")
1073		case "RCPT TO:<[email protected]>":
1074			send("250 Ok")
1075		case "DATA":
1076			send("354 send the mail data, end with .")
1077			send("250 Ok")
1078		case "Subject: test":
1079		case "":
1080		case "howdy!":
1081		case ".":
1082		case "QUIT":
1083			send("221 127.0.0.1 Service closing transmission channel")
1084			return nil
1085		default:
1086			t.Fatalf("unrecognized command during TLS: %q", s.Text())
1087		}
1088	}
1089	return s.Err()
1090}
1091
1092func init() {
1093	testRootCAs := x509.NewCertPool()
1094	testRootCAs.AppendCertsFromPEM(localhostCert)
1095	testHookStartTLS = func(config *tls.Config) {
1096		config.RootCAs = testRootCAs
1097	}
1098}
1099
1100func sendMail(hostPort string) error {
1101	from := "[email protected]"
1102	to := []string{"[email protected]"}
1103	return SendMail(hostPort, nil, from, to, []byte("Subject: test\n\nhowdy!"))
1104}
1105
1106// localhostCert is a PEM-encoded TLS cert generated from src/crypto/tls:
1107//
1108//	go run generate_cert.go --rsa-bits 1024 --host 127.0.0.1,::1,example.com \
1109//		--ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
1110var localhostCert = []byte(`
1111-----BEGIN CERTIFICATE-----
1112MIICFDCCAX2gAwIBAgIRAK0xjnaPuNDSreeXb+z+0u4wDQYJKoZIhvcNAQELBQAw
1113EjEQMA4GA1UEChMHQWNtZSBDbzAgFw03MDAxMDEwMDAwMDBaGA8yMDg0MDEyOTE2
1114MDAwMFowEjEQMA4GA1UEChMHQWNtZSBDbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
1115gYkCgYEA0nFbQQuOWsjbGtejcpWz153OlziZM4bVjJ9jYruNw5n2Ry6uYQAffhqa
1116JOInCmmcVe2siJglsyH9aRh6vKiobBbIUXXUU1ABd56ebAzlt0LobLlx7pZEMy30
1117LqIi9E6zmL3YvdGzpYlkFRnRrqwEtWYbGBf3znO250S56CCWH2UCAwEAAaNoMGYw
1118DgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEwEB/wQF
1119MAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAAAAAAAAAA
1120AAAAAAEwDQYJKoZIhvcNAQELBQADgYEAbZtDS2dVuBYvb+MnolWnCNqvw1w5Gtgi
1121NmvQQPOMgM3m+oQSCPRTNGSg25e1Qbo7bgQDv8ZTnq8FgOJ/rbkyERw2JckkHpD4
1122n4qcK27WkEDBtQFlPihIM8hLIuzWoi/9wygiElTy/tVL3y7fGCvY2/k1KBthtZGF
1123tN8URjVmyEo=
1124-----END CERTIFICATE-----`)
1125
1126// localhostKey is the private key for localhostCert.
1127var localhostKey = []byte(testingKey(`
1128-----BEGIN RSA TESTING KEY-----
1129MIICXgIBAAKBgQDScVtBC45ayNsa16NylbPXnc6XOJkzhtWMn2Niu43DmfZHLq5h
1130AB9+Gpok4icKaZxV7ayImCWzIf1pGHq8qKhsFshRddRTUAF3np5sDOW3QuhsuXHu
1131lkQzLfQuoiL0TrOYvdi90bOliWQVGdGurAS1ZhsYF/fOc7bnRLnoIJYfZQIDAQAB
1132AoGBAMst7OgpKyFV6c3JwyI/jWqxDySL3caU+RuTTBaodKAUx2ZEmNJIlx9eudLA
1133kucHvoxsM/eRxlxkhdFxdBcwU6J+zqooTnhu/FE3jhrT1lPrbhfGhyKnUrB0KKMM
1134VY3IQZyiehpxaeXAwoAou6TbWoTpl9t8ImAqAMY8hlULCUqlAkEA+9+Ry5FSYK/m
1135542LujIcCaIGoG1/Te6Sxr3hsPagKC2rH20rDLqXwEedSFOpSS0vpzlPAzy/6Rbb
1136PHTJUhNdwwJBANXkA+TkMdbJI5do9/mn//U0LfrCR9NkcoYohxfKz8JuhgRQxzF2
11376jpo3q7CdTuuRixLWVfeJzcrAyNrVcBq87cCQFkTCtOMNC7fZnCTPUv+9q1tcJyB
1138vNjJu3yvoEZeIeuzouX9TJE21/33FaeDdsXbRhQEj23cqR38qFHsF1qAYNMCQQDP
1139QXLEiJoClkR2orAmqjPLVhR3t2oB3INcnEjLNSq8LHyQEfXyaFfu4U9l5+fRPL2i
1140jiC0k/9L5dHUsF0XZothAkEA23ddgRs+Id/HxtojqqUT27B8MT/IGNrYsp4DvS/c
1141qgkeluku4GjxRlDMBuXk94xOBEinUs+p/hwP1Alll80Tpg==
1142-----END RSA TESTING KEY-----`))
1143
1144func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
1145