github.com/emersion/go-smtp@v0.20.2/client.go (about) 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 5 package smtp 6 7 import ( 8 "crypto/tls" 9 "encoding/base64" 10 "errors" 11 "fmt" 12 "io" 13 "net" 14 "net/textproto" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/emersion/go-sasl" 20 ) 21 22 // A Client represents a client connection to an SMTP server. 23 type Client struct { 24 // keep a reference to the connection so it can be used to create a TLS 25 // connection later 26 conn net.Conn 27 text *textproto.Conn 28 serverName string 29 lmtp bool 30 // map of supported extensions 31 ext map[string]string 32 // supported auth mechanisms 33 auth []string 34 localName string // the name to use in HELO/EHLO/LHLO 35 didHello bool // whether we've said HELO/EHLO/LHLO 36 helloError error // the error from the hello 37 rcpts []string // recipients accumulated for the current session 38 39 // Time to wait for command responses (this includes 3xx reply to DATA). 40 CommandTimeout time.Duration 41 // Time to wait for responses after final dot. 42 SubmissionTimeout time.Duration 43 44 // Logger for all network activity. 45 DebugWriter io.Writer 46 } 47 48 // 30 seconds was chosen as it's the same duration as http.DefaultTransport's 49 // timeout. 50 const defaultTimeout = 30 * time.Second 51 52 var defaultDialer = net.Dialer{Timeout: defaultTimeout} 53 54 // Dial returns a new Client connected to an SMTP server at addr. The addr must 55 // include a port, as in "mail.example.com:smtp". 56 // 57 // This function returns a plaintext connection. To enable TLS, use StartTLS. 58 func Dial(addr string) (*Client, error) { 59 conn, err := defaultDialer.Dial("tcp", addr) 60 if err != nil { 61 return nil, err 62 } 63 client := NewClient(conn) 64 client.serverName, _, _ = net.SplitHostPort(addr) 65 return client, nil 66 } 67 68 // DialTLS returns a new Client connected to an SMTP server via TLS at addr. 69 // The addr must include a port, as in "mail.example.com:smtps". 70 // 71 // A nil tlsConfig is equivalent to a zero tls.Config. 72 func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) { 73 tlsDialer := tls.Dialer{ 74 NetDialer: &defaultDialer, 75 Config: tlsConfig, 76 } 77 conn, err := tlsDialer.Dial("tcp", addr) 78 if err != nil { 79 return nil, err 80 } 81 client := NewClient(conn) 82 client.serverName, _, _ = net.SplitHostPort(addr) 83 return client, nil 84 } 85 86 // NewClient returns a new Client using an existing connection and host as a 87 // server name to be used when authenticating. 88 func NewClient(conn net.Conn) *Client { 89 c := &Client{ 90 localName: "localhost", 91 // As recommended by RFC 5321. For DATA command reply (3xx one) RFC 92 // recommends a slightly shorter timeout but we do not bother 93 // differentiating these. 94 CommandTimeout: 5 * time.Minute, 95 // 10 minutes + 2 minute buffer in case the server is doing transparent 96 // forwarding and also follows recommended timeouts. 97 SubmissionTimeout: 12 * time.Minute, 98 } 99 100 c.setConn(conn) 101 102 return c 103 } 104 105 // NewClientLMTP returns a new LMTP Client (as defined in RFC 2033) using an 106 // existing connection and host as a server name to be used when authenticating. 107 func NewClientLMTP(conn net.Conn) *Client { 108 c := NewClient(conn) 109 c.lmtp = true 110 return c 111 } 112 113 // setConn sets the underlying network connection for the client. 114 func (c *Client) setConn(conn net.Conn) { 115 c.conn = conn 116 117 var r io.Reader = conn 118 var w io.Writer = conn 119 120 r = &lineLimitReader{ 121 R: conn, 122 // Doubled maximum line length per RFC 5321 (Section 4.5.3.1.6) 123 LineLimit: 2000, 124 } 125 126 r = io.TeeReader(r, clientDebugWriter{c}) 127 w = io.MultiWriter(w, clientDebugWriter{c}) 128 129 rwc := struct { 130 io.Reader 131 io.Writer 132 io.Closer 133 }{ 134 Reader: r, 135 Writer: w, 136 Closer: conn, 137 } 138 c.text = textproto.NewConn(rwc) 139 } 140 141 // Close closes the connection. 142 func (c *Client) Close() error { 143 return c.text.Close() 144 } 145 146 func (c *Client) greet() error { 147 // Initial greeting timeout. RFC 5321 recommends 5 minutes. 148 c.conn.SetDeadline(time.Now().Add(c.CommandTimeout)) 149 defer c.conn.SetDeadline(time.Time{}) 150 151 _, _, err := c.text.ReadResponse(220) 152 if err != nil { 153 c.text.Close() 154 if protoErr, ok := err.(*textproto.Error); ok { 155 return toSMTPErr(protoErr) 156 } 157 return err 158 } 159 160 return nil 161 } 162 163 // hello runs a hello exchange if needed. 164 func (c *Client) hello() error { 165 if !c.didHello { 166 c.didHello = true 167 if err := c.greet(); err != nil { 168 c.helloError = err 169 } else if err := c.ehlo(); err != nil { 170 c.helloError = c.helo() 171 } 172 } 173 return c.helloError 174 } 175 176 // Hello sends a HELO or EHLO to the server as the given host name. 177 // Calling this method is only necessary if the client needs control 178 // over the host name used. The client will introduce itself as "localhost" 179 // automatically otherwise. If Hello is called, it must be called before 180 // any of the other methods. 181 // 182 // If server returns an error, it will be of type *SMTPError. 183 func (c *Client) Hello(localName string) error { 184 if err := validateLine(localName); err != nil { 185 return err 186 } 187 if c.didHello { 188 return errors.New("smtp: Hello called after other methods") 189 } 190 c.localName = localName 191 return c.hello() 192 } 193 194 // cmd is a convenience function that sends a command and returns the response 195 // textproto.Error returned by c.text.ReadResponse is converted into SMTPError. 196 func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) { 197 c.conn.SetDeadline(time.Now().Add(c.CommandTimeout)) 198 defer c.conn.SetDeadline(time.Time{}) 199 200 id, err := c.text.Cmd(format, args...) 201 if err != nil { 202 return 0, "", err 203 } 204 c.text.StartResponse(id) 205 defer c.text.EndResponse(id) 206 code, msg, err := c.text.ReadResponse(expectCode) 207 if err != nil { 208 if protoErr, ok := err.(*textproto.Error); ok { 209 smtpErr := toSMTPErr(protoErr) 210 return code, smtpErr.Message, smtpErr 211 } 212 return code, msg, err 213 } 214 return code, msg, nil 215 } 216 217 // helo sends the HELO greeting to the server. It should be used only when the 218 // server does not support ehlo. 219 func (c *Client) helo() error { 220 c.ext = nil 221 _, _, err := c.cmd(250, "HELO %s", c.localName) 222 return err 223 } 224 225 // ehlo sends the EHLO (extended hello) greeting to the server. It 226 // should be the preferred greeting for servers that support it. 227 func (c *Client) ehlo() error { 228 cmd := "EHLO" 229 if c.lmtp { 230 cmd = "LHLO" 231 } 232 233 _, msg, err := c.cmd(250, "%s %s", cmd, c.localName) 234 if err != nil { 235 return err 236 } 237 ext := make(map[string]string) 238 extList := strings.Split(msg, "\n") 239 if len(extList) > 1 { 240 extList = extList[1:] 241 for _, line := range extList { 242 args := strings.SplitN(line, " ", 2) 243 if len(args) > 1 { 244 ext[args[0]] = args[1] 245 } else { 246 ext[args[0]] = "" 247 } 248 } 249 } 250 if mechs, ok := ext["AUTH"]; ok { 251 c.auth = strings.Split(mechs, " ") 252 } 253 c.ext = ext 254 return err 255 } 256 257 // StartTLS sends the STARTTLS command and encrypts all further communication. 258 // Only servers that advertise the STARTTLS extension support this function. 259 // 260 // A nil config is equivalent to a zero tls.Config. 261 // 262 // If server returns an error, it will be of type *SMTPError. 263 func (c *Client) StartTLS(config *tls.Config) error { 264 if err := c.hello(); err != nil { 265 return err 266 } 267 _, _, err := c.cmd(220, "STARTTLS") 268 if err != nil { 269 return err 270 } 271 if config == nil { 272 config = &tls.Config{} 273 } 274 if config.ServerName == "" && c.serverName != "" { 275 // Make a copy to avoid polluting argument 276 config = config.Clone() 277 config.ServerName = c.serverName 278 } 279 if testHookStartTLS != nil { 280 testHookStartTLS(config) 281 } 282 c.setConn(tls.Client(c.conn, config)) 283 return c.ehlo() 284 } 285 286 // TLSConnectionState returns the client's TLS connection state. 287 // The return values are their zero values if StartTLS did 288 // not succeed. 289 func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) { 290 tc, ok := c.conn.(*tls.Conn) 291 if !ok { 292 return 293 } 294 return tc.ConnectionState(), true 295 } 296 297 // Verify checks the validity of an email address on the server. 298 // If Verify returns nil, the address is valid. A non-nil return 299 // does not necessarily indicate an invalid address. Many servers 300 // will not verify addresses for security reasons. 301 // 302 // If server returns an error, it will be of type *SMTPError. 303 func (c *Client) Verify(addr string) error { 304 if err := validateLine(addr); err != nil { 305 return err 306 } 307 if err := c.hello(); err != nil { 308 return err 309 } 310 _, _, err := c.cmd(250, "VRFY %s", addr) 311 return err 312 } 313 314 // Auth authenticates a client using the provided authentication mechanism. 315 // Only servers that advertise the AUTH extension support this function. 316 // 317 // If server returns an error, it will be of type *SMTPError. 318 func (c *Client) Auth(a sasl.Client) error { 319 if err := c.hello(); err != nil { 320 return err 321 } 322 encoding := base64.StdEncoding 323 mech, resp, err := a.Start() 324 if err != nil { 325 return err 326 } 327 resp64 := make([]byte, encoding.EncodedLen(len(resp))) 328 encoding.Encode(resp64, resp) 329 code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64))) 330 for err == nil { 331 var msg []byte 332 switch code { 333 case 334: 334 msg, err = encoding.DecodeString(msg64) 335 case 235: 336 // the last message isn't base64 because it isn't a challenge 337 msg = []byte(msg64) 338 default: 339 err = toSMTPErr(&textproto.Error{Code: code, Msg: msg64}) 340 } 341 if err == nil { 342 if code == 334 { 343 resp, err = a.Next(msg) 344 } else { 345 resp = nil 346 } 347 } 348 if err != nil { 349 // abort the AUTH 350 c.cmd(501, "*") 351 break 352 } 353 if resp == nil { 354 break 355 } 356 resp64 = make([]byte, encoding.EncodedLen(len(resp))) 357 encoding.Encode(resp64, resp) 358 code, msg64, err = c.cmd(0, string(resp64)) 359 } 360 return err 361 } 362 363 // Mail issues a MAIL command to the server using the provided email address. 364 // If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME 365 // parameter. 366 // This initiates a mail transaction and is followed by one or more Rcpt calls. 367 // 368 // If opts is not nil, MAIL arguments provided in the structure will be added 369 // to the command. Handling of unsupported options depends on the extension. 370 // 371 // If server returns an error, it will be of type *SMTPError. 372 func (c *Client) Mail(from string, opts *MailOptions) error { 373 if err := validateLine(from); err != nil { 374 return err 375 } 376 if err := c.hello(); err != nil { 377 return err 378 } 379 380 var sb strings.Builder 381 // A high enough power of 2 than 510+14+26+11+9+9+39+500 382 sb.Grow(2048) 383 fmt.Fprintf(&sb, "MAIL FROM:<%s>", from) 384 if _, ok := c.ext["8BITMIME"]; ok { 385 sb.WriteString(" BODY=8BITMIME") 386 } 387 if _, ok := c.ext["SIZE"]; ok && opts != nil && opts.Size != 0 { 388 fmt.Fprintf(&sb, " SIZE=%v", opts.Size) 389 } 390 if opts != nil && opts.RequireTLS { 391 if _, ok := c.ext["REQUIRETLS"]; ok { 392 sb.WriteString(" REQUIRETLS") 393 } else { 394 return errors.New("smtp: server does not support REQUIRETLS") 395 } 396 } 397 if opts != nil && opts.UTF8 { 398 if _, ok := c.ext["SMTPUTF8"]; ok { 399 sb.WriteString(" SMTPUTF8") 400 } else { 401 return errors.New("smtp: server does not support SMTPUTF8") 402 } 403 } 404 if _, ok := c.ext["DSN"]; ok && opts != nil { 405 switch opts.Return { 406 case DSNReturnFull, DSNReturnHeaders: 407 fmt.Fprintf(&sb, " RET=%s", string(opts.Return)) 408 case "": 409 // This space is intentionally left blank 410 default: 411 return errors.New("smtp: Unknown RET parameter value") 412 } 413 if opts.EnvelopeID != "" { 414 if !isPrintableASCII(opts.EnvelopeID) { 415 return errors.New("smtp: Malformed ENVID parameter value") 416 } 417 fmt.Fprintf(&sb, " ENVID=%s", encodeXtext(opts.EnvelopeID)) 418 } 419 } 420 if opts != nil && opts.Auth != nil { 421 if _, ok := c.ext["AUTH"]; ok { 422 fmt.Fprintf(&sb, " AUTH=%s", encodeXtext(*opts.Auth)) 423 } 424 // We can safely discard parameter if server does not support AUTH. 425 } 426 _, _, err := c.cmd(250, "%s", sb.String()) 427 return err 428 } 429 430 // Rcpt issues a RCPT command to the server using the provided email address. 431 // A call to Rcpt must be preceded by a call to Mail and may be followed by 432 // a Data call or another Rcpt call. 433 // 434 // If opts is not nil, RCPT arguments provided in the structure will be added 435 // to the command. Handling of unsupported options depends on the extension. 436 // 437 // If server returns an error, it will be of type *SMTPError. 438 func (c *Client) Rcpt(to string, opts *RcptOptions) error { 439 if err := validateLine(to); err != nil { 440 return err 441 } 442 443 var sb strings.Builder 444 // A high enough power of 2 than 510+29+501 445 sb.Grow(2048) 446 fmt.Fprintf(&sb, "RCPT TO:<%s>", to) 447 if _, ok := c.ext["DSN"]; ok && opts != nil { 448 if opts.Notify != nil && len(opts.Notify) != 0 { 449 sb.WriteString(" NOTIFY=") 450 if err := checkNotifySet(opts.Notify); err != nil { 451 return errors.New("smtp: Malformed NOTIFY parameter value") 452 } 453 for i, v := range opts.Notify { 454 if i != 0 { 455 sb.WriteString(",") 456 } 457 sb.WriteString(string(v)) 458 } 459 } 460 if opts.OriginalRecipient != "" { 461 var enc string 462 switch opts.OriginalRecipientType { 463 case DSNAddressTypeRFC822: 464 if !isPrintableASCII(opts.OriginalRecipient) { 465 return errors.New("smtp: Illegal address") 466 } 467 enc = encodeXtext(opts.OriginalRecipient) 468 case DSNAddressTypeUTF8: 469 if _, ok := c.ext["SMTPUTF8"]; ok { 470 enc = encodeUTF8AddrUnitext(opts.OriginalRecipient) 471 } else { 472 enc = encodeUTF8AddrXtext(opts.OriginalRecipient) 473 } 474 default: 475 return errors.New("smtp: Unknown address type") 476 } 477 fmt.Fprintf(&sb, " ORCPT=%s;%s", string(opts.OriginalRecipientType), enc) 478 } 479 } 480 if _, _, err := c.cmd(25, "%s", sb.String()); err != nil { 481 return err 482 } 483 c.rcpts = append(c.rcpts, to) 484 return nil 485 } 486 487 type dataCloser struct { 488 c *Client 489 io.WriteCloser 490 statusCb func(rcpt string, status *SMTPError) 491 closed bool 492 } 493 494 func (d *dataCloser) Close() error { 495 if d.closed { 496 return fmt.Errorf("smtp: data writer closed twice") 497 } 498 499 if err := d.WriteCloser.Close(); err != nil { 500 return err 501 } 502 503 d.c.conn.SetDeadline(time.Now().Add(d.c.SubmissionTimeout)) 504 defer d.c.conn.SetDeadline(time.Time{}) 505 506 expectedResponses := len(d.c.rcpts) 507 if d.c.lmtp { 508 for expectedResponses > 0 { 509 rcpt := d.c.rcpts[len(d.c.rcpts)-expectedResponses] 510 if _, _, err := d.c.text.ReadResponse(250); err != nil { 511 if protoErr, ok := err.(*textproto.Error); ok { 512 if d.statusCb != nil { 513 d.statusCb(rcpt, toSMTPErr(protoErr)) 514 } 515 } else { 516 return err 517 } 518 } else if d.statusCb != nil { 519 d.statusCb(rcpt, nil) 520 } 521 expectedResponses-- 522 } 523 } else { 524 _, _, err := d.c.text.ReadResponse(250) 525 if err != nil { 526 if protoErr, ok := err.(*textproto.Error); ok { 527 return toSMTPErr(protoErr) 528 } 529 return err 530 } 531 } 532 533 d.closed = true 534 return nil 535 } 536 537 // Data issues a DATA command to the server and returns a writer that 538 // can be used to write the mail headers and body. The caller should 539 // close the writer before calling any more methods on c. A call to 540 // Data must be preceded by one or more calls to Rcpt. 541 // 542 // If server returns an error, it will be of type *SMTPError. 543 func (c *Client) Data() (io.WriteCloser, error) { 544 _, _, err := c.cmd(354, "DATA") 545 if err != nil { 546 return nil, err 547 } 548 return &dataCloser{c: c, WriteCloser: c.text.DotWriter()}, nil 549 } 550 551 // LMTPData is the LMTP-specific version of the Data method. It accepts a callback 552 // that will be called for each status response received from the server. 553 // 554 // Status callback will receive a SMTPError argument for each negative server 555 // reply and nil for each positive reply. I/O errors will not be reported using 556 // callback and instead will be returned by the Close method of io.WriteCloser. 557 // Callback will be called for each successfull Rcpt call done before in the 558 // same order. 559 func (c *Client) LMTPData(statusCb func(rcpt string, status *SMTPError)) (io.WriteCloser, error) { 560 if !c.lmtp { 561 return nil, errors.New("smtp: not a LMTP client") 562 } 563 564 _, _, err := c.cmd(354, "DATA") 565 if err != nil { 566 return nil, err 567 } 568 return &dataCloser{c: c, WriteCloser: c.text.DotWriter(), statusCb: statusCb}, nil 569 } 570 571 // SendMail will use an existing connection to send an email from 572 // address from, to addresses to, with message r. 573 // 574 // This function does not start TLS, nor does it perform authentication. Use 575 // StartTLS and Auth before-hand if desirable. 576 // 577 // The addresses in the to parameter are the SMTP RCPT addresses. 578 // 579 // The r parameter should be an RFC 822-style email with headers 580 // first, a blank line, and then the message body. The lines of r 581 // should be CRLF terminated. The r headers should usually include 582 // fields such as "From", "To", "Subject", and "Cc". Sending "Bcc" 583 // messages is accomplished by including an email address in the to 584 // parameter but not including it in the r headers. 585 func (c *Client) SendMail(from string, to []string, r io.Reader) error { 586 var err error 587 588 if err = c.Mail(from, nil); err != nil { 589 return err 590 } 591 for _, addr := range to { 592 if err = c.Rcpt(addr, nil); err != nil { 593 return err 594 } 595 } 596 w, err := c.Data() 597 if err != nil { 598 return err 599 } 600 _, err = io.Copy(w, r) 601 if err != nil { 602 return err 603 } 604 return w.Close() 605 } 606 607 var testHookStartTLS func(*tls.Config) // nil, except for tests 608 609 // SendMail connects to the server at addr, switches to TLS, authenticates with 610 // the optional SASL client, and then sends an email from address from, to 611 // addresses to, with message r. The addr must include a port, as in 612 // "mail.example.com:smtp". 613 // 614 // The addresses in the to parameter are the SMTP RCPT addresses. 615 // 616 // The r parameter should be an RFC 822-style email with headers 617 // first, a blank line, and then the message body. The lines of r 618 // should be CRLF terminated. The r headers should usually include 619 // fields such as "From", "To", "Subject", and "Cc". Sending "Bcc" 620 // messages is accomplished by including an email address in the to 621 // parameter but not including it in the r headers. 622 // 623 // SendMail is intended to be used for very simple use-cases. If you want to 624 // customize SendMail's behavior, use a Client instead. 625 // 626 // The SendMail function and the go-smtp package are low-level 627 // mechanisms and provide no support for DKIM signing (see go-msgauth), MIME 628 // attachments (see the mime/multipart package or the go-message package), or 629 // other mail functionality. 630 func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader) error { 631 if err := validateLine(from); err != nil { 632 return err 633 } 634 for _, recp := range to { 635 if err := validateLine(recp); err != nil { 636 return err 637 } 638 } 639 640 c, err := Dial(addr) 641 if err != nil { 642 return err 643 } 644 defer c.Close() 645 646 if err = c.hello(); err != nil { 647 return err 648 } 649 if ok, _ := c.Extension("STARTTLS"); !ok { 650 return errors.New("smtp: server doesn't support STARTTLS") 651 } 652 if err = c.StartTLS(nil); err != nil { 653 return err 654 } 655 if a != nil { 656 if ok, _ := c.Extension("AUTH"); !ok { 657 return errors.New("smtp: server doesn't support AUTH") 658 } 659 if err = c.Auth(a); err != nil { 660 return err 661 } 662 } 663 if err := c.SendMail(from, to, r); err != nil { 664 return err 665 } 666 return c.Quit() 667 } 668 669 // SendMailTLS works like SendMail, but with implicit TLS. 670 func SendMailTLS(addr string, a sasl.Client, from string, to []string, r io.Reader) error { 671 if err := validateLine(from); err != nil { 672 return err 673 } 674 for _, recp := range to { 675 if err := validateLine(recp); err != nil { 676 return err 677 } 678 } 679 680 c, err := DialTLS(addr, nil) 681 if err != nil { 682 return err 683 } 684 defer c.Close() 685 686 if err = c.hello(); err != nil { 687 return err 688 } 689 if a != nil { 690 if ok, _ := c.Extension("AUTH"); !ok { 691 return errors.New("smtp: server doesn't support AUTH") 692 } 693 if err = c.Auth(a); err != nil { 694 return err 695 } 696 } 697 if err := c.SendMail(from, to, r); err != nil { 698 return err 699 } 700 return c.Quit() 701 } 702 703 // Extension reports whether an extension is support by the server. 704 // The extension name is case-insensitive. If the extension is supported, 705 // Extension also returns a string that contains any parameters the 706 // server specifies for the extension. 707 func (c *Client) Extension(ext string) (bool, string) { 708 if err := c.hello(); err != nil { 709 return false, "" 710 } 711 if c.ext == nil { 712 return false, "" 713 } 714 ext = strings.ToUpper(ext) 715 param, ok := c.ext[ext] 716 return ok, param 717 } 718 719 // Reset sends the RSET command to the server, aborting the current mail 720 // transaction. 721 func (c *Client) Reset() error { 722 if err := c.hello(); err != nil { 723 return err 724 } 725 if _, _, err := c.cmd(250, "RSET"); err != nil { 726 return err 727 } 728 c.rcpts = nil 729 return nil 730 } 731 732 // Noop sends the NOOP command to the server. It does nothing but check 733 // that the connection to the server is okay. 734 func (c *Client) Noop() error { 735 if err := c.hello(); err != nil { 736 return err 737 } 738 _, _, err := c.cmd(250, "NOOP") 739 return err 740 } 741 742 // Quit sends the QUIT command and closes the connection to the server. 743 // 744 // If Quit fails the connection is not closed, Close should be used 745 // in this case. 746 func (c *Client) Quit() error { 747 if err := c.hello(); err != nil { 748 return err 749 } 750 _, _, err := c.cmd(221, "QUIT") 751 if err != nil { 752 return err 753 } 754 return c.Close() 755 } 756 757 func parseEnhancedCode(s string) (EnhancedCode, error) { 758 parts := strings.Split(s, ".") 759 if len(parts) != 3 { 760 return EnhancedCode{}, fmt.Errorf("wrong amount of enhanced code parts") 761 } 762 763 code := EnhancedCode{} 764 for i, part := range parts { 765 num, err := strconv.Atoi(part) 766 if err != nil { 767 return code, err 768 } 769 code[i] = num 770 } 771 return code, nil 772 } 773 774 // toSMTPErr converts textproto.Error into SMTPError, parsing 775 // enhanced status code if it is present. 776 func toSMTPErr(protoErr *textproto.Error) *SMTPError { 777 smtpErr := &SMTPError{ 778 Code: protoErr.Code, 779 Message: protoErr.Msg, 780 } 781 782 parts := strings.SplitN(protoErr.Msg, " ", 2) 783 if len(parts) != 2 { 784 return smtpErr 785 } 786 787 enchCode, err := parseEnhancedCode(parts[0]) 788 if err != nil { 789 return smtpErr 790 } 791 792 msg := parts[1] 793 794 // Per RFC 2034, enhanced code should be prepended to each line. 795 msg = strings.ReplaceAll(msg, "\n"+parts[0]+" ", "\n") 796 797 smtpErr.EnhancedCode = enchCode 798 smtpErr.Message = msg 799 return smtpErr 800 } 801 802 type clientDebugWriter struct { 803 c *Client 804 } 805 806 func (cdw clientDebugWriter) Write(b []byte) (int, error) { 807 if cdw.c.DebugWriter == nil { 808 return len(b), nil 809 } 810 return cdw.c.DebugWriter.Write(b) 811 } 812 813 // validateLine checks to see if a line has CR or LF. 814 func validateLine(line string) error { 815 if strings.ContainsAny(line, "\n\r") { 816 return errors.New("smtp: a line must not contain CR or LF") 817 } 818 return nil 819 }