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