@@ -12,6 +12,7 @@ import (
12
12
"io"
13
13
"net"
14
14
"net/textproto"
15
+ "sort"
15
16
"strconv"
16
17
"strings"
17
18
"time"
@@ -540,85 +541,152 @@ func (c *Client) Rcpt(to string, opts *RcptOptions) error {
540
541
return nil
541
542
}
542
543
543
- type dataCloser struct {
544
- c * Client
545
- io.WriteCloser
546
- statusCb func (rcpt string , status * SMTPError )
547
- closed bool
544
+ // DataCommand is a pending DATA command. DataCommand is an io.WriteCloser.
545
+ // See Client.Data.
546
+ type DataCommand struct {
547
+ client * Client
548
+ wc io.WriteCloser
549
+
550
+ closeErr error
551
+ }
552
+
553
+ var _ io.WriteCloser = (* DataCommand )(nil )
554
+
555
+ // Write implements io.Writer.
556
+ func (cmd * DataCommand ) Write (b []byte ) (int , error ) {
557
+ return cmd .wc .Write (b )
558
+ }
559
+
560
+ // Close implements io.Closer.
561
+ func (cmd * DataCommand ) Close () error {
562
+ var err error
563
+ if cmd .client .lmtp {
564
+ _ , err = cmd .CloseWithLMTPResponse ()
565
+ } else {
566
+ _ , err = cmd .CloseWithResponse ()
567
+ }
568
+ return err
548
569
}
549
570
550
- func (d * dataCloser ) Close () error {
551
- if d .closed {
552
- return fmt .Errorf ("smtp: data writer closed twice" )
571
+ // CloseWithResponse is equivalent to Close, but also returns the server
572
+ // response. It cannot be called when the LMTP protocol is used.
573
+ //
574
+ // If server returns an error, it will be of type *SMTPError.
575
+ func (cmd * DataCommand ) CloseWithResponse () (* DataResponse , error ) {
576
+ if cmd .client .lmtp {
577
+ return nil , errors .New ("smtp: CloseWithResponse used with an LMTP client" )
553
578
}
554
579
555
- if err := d . WriteCloser . Close (); err != nil {
556
- return err
580
+ if err := cmd . close (); err != nil {
581
+ return nil , err
557
582
}
558
583
559
- d . c .conn .SetDeadline (time .Now ().Add (d . c .SubmissionTimeout ))
560
- defer d . c .conn .SetDeadline (time.Time {})
584
+ cmd . client .conn .SetDeadline (time .Now ().Add (cmd . client .SubmissionTimeout ))
585
+ defer cmd . client .conn .SetDeadline (time.Time {})
561
586
562
- expectedResponses := len (d .c .rcpts )
563
- if d .c .lmtp {
564
- for expectedResponses > 0 {
565
- rcpt := d .c .rcpts [len (d .c .rcpts )- expectedResponses ]
566
- if _ , _ , err := d .c .readResponse (250 ); err != nil {
567
- if smtpErr , ok := err .(* SMTPError ); ok {
568
- if d .statusCb != nil {
569
- d .statusCb (rcpt , smtpErr )
570
- }
571
- } else {
572
- return err
587
+ _ , msg , err := cmd .client .readResponse (250 )
588
+ if err != nil {
589
+ cmd .closeErr = err
590
+ return nil , err
591
+ }
592
+
593
+ return & DataResponse {StatusText : msg }, nil
594
+ }
595
+
596
+ // CloseWithLMTPResponse is equivalent to Close, but also returns per-recipient
597
+ // server responses. It can only be called when the LMTP protocol is used.
598
+ //
599
+ // If server returns an error, it will be of type LMTPDataError.
600
+ func (cmd * DataCommand ) CloseWithLMTPResponse () (map [string ]* DataResponse , error ) {
601
+ if ! cmd .client .lmtp {
602
+ return nil , errors .New ("smtp: CloseWithLMTPResponse used without an LMTP client" )
603
+ }
604
+
605
+ if err := cmd .close (); err != nil {
606
+ return nil , err
607
+ }
608
+
609
+ cmd .client .conn .SetDeadline (time .Now ().Add (cmd .client .SubmissionTimeout ))
610
+ defer cmd .client .conn .SetDeadline (time.Time {})
611
+
612
+ resp := make (map [string ]* DataResponse , len (cmd .client .rcpts ))
613
+ lmtpErr := make (LMTPDataError , len (cmd .client .rcpts ))
614
+ for i := 0 ; i < len (cmd .client .rcpts ); i ++ {
615
+ rcpt := cmd .client .rcpts [i ]
616
+ _ , msg , err := cmd .client .readResponse (250 )
617
+ if err != nil {
618
+ if smtpErr , ok := err .(* SMTPError ); ok {
619
+ lmtpErr [rcpt ] = smtpErr
620
+ } else {
621
+ if len (lmtpErr ) > 0 {
622
+ return resp , errors .Join (err , lmtpErr )
573
623
}
574
- } else if d .statusCb != nil {
575
- d .statusCb (rcpt , nil )
624
+ return resp , err
576
625
}
577
- expectedResponses --
578
- }
579
- } else {
580
- _ , _ , err := d .c .readResponse (250 )
581
- if err != nil {
582
- return err
626
+ } else {
627
+ resp [rcpt ] = & DataResponse {StatusText : msg }
583
628
}
584
629
}
585
630
586
- d .closed = true
587
- return nil
631
+ if len (lmtpErr ) > 0 {
632
+ return resp , lmtpErr
633
+ }
634
+ return resp , nil
588
635
}
589
636
590
- // Data issues a DATA command to the server and returns a writer that
591
- // can be used to write the mail headers and body. The caller should
592
- // close the writer before calling any more methods on c. A call to
593
- // Data must be preceded by one or more calls to Rcpt.
594
- //
595
- // If server returns an error, it will be of type *SMTPError.
596
- func (c * Client ) Data () (io.WriteCloser , error ) {
597
- _ , _ , err := c .cmd (354 , "DATA" )
598
- if err != nil {
599
- return nil , err
637
+ func (cmd * DataCommand ) close () error {
638
+ if cmd .closeErr != nil {
639
+ return cmd .closeErr
600
640
}
601
- return & dataCloser {c : c , WriteCloser : c .text .DotWriter ()}, nil
641
+
642
+ if err := cmd .wc .Close (); err != nil {
643
+ cmd .closeErr = err
644
+ return err
645
+ }
646
+
647
+ cmd .closeErr = errors .New ("smtp: data writer closed twice" )
648
+ return nil
602
649
}
603
650
604
- // LMTPData is the LMTP-specific version of the Data method. It accepts a callback
605
- // that will be called for each status response received from the server.
606
- //
607
- // Status callback will receive a SMTPError argument for each negative server
608
- // reply and nil for each positive reply. I/O errors will not be reported using
609
- // callback and instead will be returned by the Close method of io.WriteCloser.
610
- // Callback will be called for each successfull Rcpt call done before in the
611
- // same order.
612
- func (c * Client ) LMTPData (statusCb func (rcpt string , status * SMTPError )) (io.WriteCloser , error ) {
613
- if ! c .lmtp {
614
- return nil , errors .New ("smtp: not a LMTP client" )
651
+ // DataResponse is the response returned by a DATA command. See
652
+ // DataCommand.CloseWithResponse.
653
+ type DataResponse struct {
654
+ // StatusText is the status text returned by the server. It may contain
655
+ // tracking information.
656
+ StatusText string
657
+ }
658
+
659
+ // LMTPDataError is a collection of errors returned by an LMTP server for a
660
+ // DATA command. It holds per-recipient errors.
661
+ type LMTPDataError map [string ]* SMTPError
662
+
663
+ // Error implements error.
664
+ func (lmtpErr LMTPDataError ) Error () string {
665
+ return errors .Join (lmtpErr .Unwrap ()... ).Error ()
666
+ }
667
+
668
+ // Unwrap returns all per-recipient errors returned by the server.
669
+ func (lmtpErr LMTPDataError ) Unwrap () []error {
670
+ l := make ([]error , 0 , len (lmtpErr ))
671
+ for rcpt , smtpErr := range lmtpErr {
672
+ l = append (l , fmt .Errorf ("<%v>: %w" , rcpt , smtpErr ))
615
673
}
674
+ sort .Slice (l , func (i , j int ) bool {
675
+ return l [i ].Error () < l [j ].Error ()
676
+ })
677
+ return l
678
+ }
616
679
680
+ // Data issues a DATA command to the server and returns a writer that
681
+ // can be used to write the mail headers and body. The caller should
682
+ // close the writer before calling any more methods on c. A call to
683
+ // Data must be preceded by one or more calls to Rcpt.
684
+ func (c * Client ) Data () (* DataCommand , error ) {
617
685
_ , _ , err := c .cmd (354 , "DATA" )
618
686
if err != nil {
619
687
return nil , err
620
688
}
621
- return & dataCloser { c : c , WriteCloser : c .text .DotWriter (), statusCb : statusCb }, nil
689
+ return & DataCommand { client : c , wc : c .text .DotWriter ()}, nil
622
690
}
623
691
624
692
// SendMail will use an existing connection to send an email from
0 commit comments