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