From 5639f4bae524995bb72f788cd1a2bf586e96b1b4 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Sun, 17 Jan 2021 19:04:16 +0300 Subject: [PATCH 1/2] server: Introduce smtp.RcptOptions --- backend.go | 4 +++- backendutil/transform.go | 4 ++-- backendutil/transform_test.go | 2 +- client.go | 7 +++++-- client_test.go | 10 +++++----- cmd/smtp-debug-server/main.go | 2 +- conn.go | 2 +- example_test.go | 4 ++-- server_test.go | 12 +++++++----- 9 files changed, 27 insertions(+), 20 deletions(-) diff --git a/backend.go b/backend.go index 6a00873..febc839 100644 --- a/backend.go +++ b/backend.go @@ -58,6 +58,8 @@ type MailOptions struct { Auth *string } +type RcptOptions struct{} + // Session is used by servers to respond to an SMTP client. // // The methods are called when the remote client issues the matching command. @@ -71,7 +73,7 @@ type Session interface { // Set return path for currently processed message. Mail(from string, opts MailOptions) error // Add recipient for currently processed message. - Rcpt(to string) error + Rcpt(to string, opts RcptOptions) error // Set currently processed message contents and send it. Data(r io.Reader) error } diff --git a/backendutil/transform.go b/backendutil/transform.go index a5ff666..525549c 100755 --- a/backendutil/transform.go +++ b/backendutil/transform.go @@ -54,7 +54,7 @@ func (s *transformSession) Mail(from string, opts smtp.MailOptions) error { return s.Session.Mail(from, opts) } -func (s *transformSession) Rcpt(to string) error { +func (s *transformSession) Rcpt(to string, opts smtp.RcptOptions) error { if s.be.TransformRcpt != nil { var err error to, err = s.be.TransformRcpt(to) @@ -62,7 +62,7 @@ func (s *transformSession) Rcpt(to string) error { return err } } - return s.Session.Rcpt(to) + return s.Session.Rcpt(to, opts) } func (s *transformSession) Data(r io.Reader) error { diff --git a/backendutil/transform_test.go b/backendutil/transform_test.go index 3c77638..0cde8a6 100755 --- a/backendutil/transform_test.go +++ b/backendutil/transform_test.go @@ -69,7 +69,7 @@ func (s *session) Mail(from string, opts smtp.MailOptions) error { return nil } -func (s *session) Rcpt(to string) error { +func (s *session) Rcpt(to string, opts smtp.RcptOptions) error { s.msg.To = append(s.msg.To, to) return nil } diff --git a/client.go b/client.go index 228ed89..e882593 100644 --- a/client.go +++ b/client.go @@ -400,8 +400,11 @@ func (c *Client) Mail(from string, opts *MailOptions) error { // A call to Rcpt must be preceded by a call to Mail and may be followed by // a Data call or another Rcpt call. // +// If opts is not nil, RCPT arguments provided in the structure will be added +// to the command. Handling of unsupported options depends on the extension. +// // If server returns an error, it will be of type *SMTPError. -func (c *Client) Rcpt(to string) error { +func (c *Client) Rcpt(to string, opts *RcptOptions) error { if err := validateLine(to); err != nil { return err } @@ -546,7 +549,7 @@ func SendMail(addr string, a sasl.Client, from string, to []string, r io.Reader) return err } for _, addr := range to { - if err = c.Rcpt(addr); err != nil { + if err = c.Rcpt(addr, nil); err != nil { return err } } diff --git a/client_test.go b/client_test.go index cb12b7d..fdfd1bd 100644 --- a/client_test.go +++ b/client_test.go @@ -121,7 +121,7 @@ func TestBasic(t *testing.T) { t.Fatalf("AUTH failed: %s", err) } - if err := c.Rcpt("golang-nuts@googlegroups.com>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"); err == nil { + if err := c.Rcpt("golang-nuts@googlegroups.com>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n", nil); err == nil { t.Fatalf("RCPT should have failed due to a message injection attempt") } if err := c.Mail("user@gmail.com>\r\nDATA\r\nAnother injected message body\r\n.\r\nQUIT\r\n", nil); err == nil { @@ -130,7 +130,7 @@ func TestBasic(t *testing.T) { if err := c.Mail("user@gmail.com", nil); err != nil { t.Fatalf("MAIL failed: %s", err) } - if err := c.Rcpt("golang-nuts@googlegroups.com"); err != nil { + if err := c.Rcpt("golang-nuts@googlegroups.com", nil); err != nil { t.Fatalf("RCPT failed: %s", err) } msg := `From: user@gmail.com @@ -922,7 +922,7 @@ func TestLMTP(t *testing.T) { if err := c.Mail("user@gmail.com", nil); err != nil { t.Fatalf("MAIL failed: %s", err) } - if err := c.Rcpt("golang-nuts@googlegroups.com"); err != nil { + if err := c.Rcpt("golang-nuts@googlegroups.com", nil); err != nil { t.Fatalf("RCPT failed: %s", err) } msg := `From: user@gmail.com @@ -1007,10 +1007,10 @@ func TestLMTPData(t *testing.T) { if err := c.Mail("user@gmail.com", nil); err != nil { t.Fatalf("MAIL failed: %s", err) } - if err := c.Rcpt("golang-nuts@googlegroups.com"); err != nil { + if err := c.Rcpt("golang-nuts@googlegroups.com", nil); err != nil { t.Fatalf("RCPT failed: %s", err) } - if err := c.Rcpt("golang-not-nuts@googlegroups.com"); err != nil { + if err := c.Rcpt("golang-not-nuts@googlegroups.com", nil); err != nil { t.Fatalf("RCPT failed: %s", err) } msg := `From: user@gmail.com diff --git a/cmd/smtp-debug-server/main.go b/cmd/smtp-debug-server/main.go index 28d9843..9d567e4 100644 --- a/cmd/smtp-debug-server/main.go +++ b/cmd/smtp-debug-server/main.go @@ -31,7 +31,7 @@ func (s *session) Mail(from string, opts smtp.MailOptions) error { return nil } -func (s *session) Rcpt(to string) error { +func (s *session) Rcpt(to string, opts smtp.RcptOptions) error { return nil } diff --git a/conn.go b/conn.go index b847057..0c00dfa 100644 --- a/conn.go +++ b/conn.go @@ -491,7 +491,7 @@ func (c *Conn) handleRcpt(arg string) { return } - if err := c.Session().Rcpt(recipient); err != nil { + if err := c.Session().Rcpt(recipient, RcptOptions{}); err != nil { if smtpErr, ok := err.(*SMTPError); ok { c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) return diff --git a/example_test.go b/example_test.go index 607c971..fcb6130 100644 --- a/example_test.go +++ b/example_test.go @@ -28,7 +28,7 @@ func ExampleDial() { if err := c.Mail("sender@example.org", nil); err != nil { log.Fatal(err) } - if err := c.Rcpt("recipient@example.net"); err != nil { + if err := c.Rcpt("recipient@example.net", nil); err != nil { log.Fatal(err) } @@ -113,7 +113,7 @@ func (s *Session) Mail(from string, opts smtp.MailOptions) error { return nil } -func (s *Session) Rcpt(to string) error { +func (s *Session) Rcpt(to string, opts smtp.RcptOptions) error { log.Println("Rcpt to:", to) return nil } diff --git a/server_test.go b/server_test.go index f2bfe9f..101d1ca 100644 --- a/server_test.go +++ b/server_test.go @@ -14,10 +14,11 @@ import ( ) type message struct { - From string - To []string - Data []byte - Opts smtp.MailOptions + From string + To []string + RcptOpts []smtp.RcptOptions + Data []byte + Opts smtp.MailOptions } type backend struct { @@ -101,8 +102,9 @@ func (s *session) Mail(from string, opts smtp.MailOptions) error { return nil } -func (s *session) Rcpt(to string) error { +func (s *session) Rcpt(to string, opts smtp.RcptOptions) error { s.msg.To = append(s.msg.To, to) + s.msg.RcptOpts = append(s.msg.RcptOpts, opts) return nil } From 9a5cd13a655beece9267a4f85d816c5cf7a9c7d8 Mon Sep 17 00:00:00 2001 From: "fox.cpp" Date: Fri, 22 Jan 2021 18:52:26 +0300 Subject: [PATCH 2/2] Implement DSN extension parameters --- backend.go | 51 ++++++++++++++++++++++++++++++- client.go | 78 ++++++++++++++++++++++++++++++++++++----------- conn.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++---- server.go | 4 +++ 4 files changed, 198 insertions(+), 23 deletions(-) diff --git a/backend.go b/backend.go index febc839..24c445e 100644 --- a/backend.go +++ b/backend.go @@ -29,6 +29,18 @@ const ( BodyBinaryMIME BodyType = "BINARYMIME" ) +type DSNReturn string + +const ( + // Attach the full copy of the message to any DSN that indicates + // a failure. Non-failure DSNs always contain the header only. + ReturnFull DSNReturn = "FULL" + + // Attach only header of the message to any DSN that indicates a + // failure. + ReturnHeaders DSNReturn = "HDRS" +) + // MailOptions contains custom arguments that were // passed as an argument to the MAIL command. type MailOptions struct { @@ -56,9 +68,46 @@ type MailOptions struct { // // Defined in RFC 4954. Auth *string + + // Whether the full message or header only should be returned in + // failure DSNs. + // + // Defined in RFC 3461. Ignored if the server does not support DSN + // extension. + Return DSNReturn + + // Envelope ID identifier. Returned in any DSN for the message. + // + // Not in xtext encoding. go-smtp restricts value to printable US-ASCII + // as required by specification. + // + // Defined in RFC 3461. Ignored if the server does not support DSN + // extension. + EnvelopeID string } -type RcptOptions struct{} +type DSNNotify string + +const ( + NotifyNever DSNNotify = "NEVER" + NotifySuccess DSNNotify = "SUCCESS" + NotifyDelayed DSNNotify = "DELAYED" + NotifyFailure DSNNotify = "FAILURE" +) + +type RcptOptions struct { + // When DSN should be generated for this recipient. + // As described in RFC 3461. + Notify []DSNNotify + + // Original message recipient as described in RFC 3461. + // + // Value of OriginalRecipient is preserved as is. No xtext + // encoding/decoding or sanitization is done irregardless of + // OriginalRecipientType. + OriginalRecipient string + OriginalRecipientType string +} // Session is used by servers to respond to an SMTP client. // diff --git a/client.go b/client.go index e882593..2b30547 100644 --- a/client.go +++ b/client.go @@ -372,25 +372,36 @@ func (c *Client) Mail(from string, opts *MailOptions) error { if _, ok := c.ext["SIZE"]; ok && opts != nil && opts.Size != 0 { cmdStr += " SIZE=" + strconv.Itoa(opts.Size) } - if opts != nil && opts.RequireTLS { - if _, ok := c.ext["REQUIRETLS"]; ok { - cmdStr += " REQUIRETLS" - } else { - return errors.New("smtp: server does not support REQUIRETLS") + if opts != nil { + if opts.RequireTLS { + if _, ok := c.ext["REQUIRETLS"]; ok { + cmdStr += " REQUIRETLS" + } else { + return errors.New("smtp: server does not support REQUIRETLS") + } } - } - if opts != nil && opts.UTF8 { - if _, ok := c.ext["SMTPUTF8"]; ok { - cmdStr += " SMTPUTF8" - } else { - return errors.New("smtp: server does not support SMTPUTF8") + if opts.UTF8 { + if _, ok := c.ext["SMTPUTF8"]; ok { + cmdStr += " SMTPUTF8" + } else { + return errors.New("smtp: server does not support SMTPUTF8") + } } - } - if opts != nil && opts.Auth != nil { - if _, ok := c.ext["AUTH"]; ok { - cmdStr += " AUTH=" + encodeXtext(*opts.Auth) + if opts.Auth != nil { + if _, ok := c.ext["AUTH"]; ok { + cmdStr += " AUTH=" + encodeXtext(*opts.Auth) + } + // We can safely discard parameter if server does not support AUTH. + } + if _, dsn := c.ext["DSN"]; dsn && opts.Return != "" { + cmdStr += " RET=" + string(opts.Return) + } + if _, dsn := c.ext["DSN"]; dsn && opts.EnvelopeID != "" { + if !checkPrintableASCII(opts.EnvelopeID) { + return errors.New("smtp: characters outside of printable ASCII are not allowed in EnvelopeID") + } + cmdStr += " ENVID=" + encodeXtext(opts.EnvelopeID) } - // We can safely discard parameter if server does not support AUTH. } _, _, err := c.cmd(250, cmdStr, from) return err @@ -408,7 +419,40 @@ func (c *Client) Rcpt(to string, opts *RcptOptions) error { if err := validateLine(to); err != nil { return err } - if _, _, err := c.cmd(25, "RCPT TO:<%s>", to); err != nil { + cmdStr := "RCPT TO:<%s>" + if opts != nil { + if len(opts.Notify) != 0 { + if _, ok := c.ext["DSN"]; ok { + notifyFlags := make([]string, len(opts.Notify)) + for i := range opts.Notify { + if opts.Notify[i] == NotifyNever && len(opts.Notify) != 1 { + return errors.New("smtp: NotifyNever cannot be combined with other options") + } + notifyFlags[i] = string(opts.Notify[i]) + } + cmdStr += " NOTIFY=" + strings.Join(notifyFlags, ",") + } else { + return errors.New("smtp: server does not support DSN") + } + } + if opts.OriginalRecipientType != "" { + // This is not strictly speaking a requirement of RFC that requires a registered + // address type but we verify it nonetheless to prevent injections. + if !checkPrintableASCII(opts.OriginalRecipientType) { + return errors.New("smtp: characters outside of printable ASCII are not allowed in OriginalRecipientType") + } + if !checkPrintableASCII(opts.OriginalRecipient) { + return errors.New("smtp: characters outside of printable ASCII are not allowed in OriginalRecipient") + } + if _, ok := c.ext["DSN"]; ok { + cmdStr += " ORCPT=" + opts.OriginalRecipientType + ";" + encodeXtext(opts.OriginalRecipient) + } + // ORCPT is unlikely to be critical for message handling so we can + // silently disregard it if server actually does not support DSN + // extension. + } + } + if _, _, err := c.cmd(25, cmdStr, to); err != nil { return err } c.rcpts = append(c.rcpts, to) diff --git a/conn.go b/conn.go index 0c00dfa..bb9f957 100644 --- a/conn.go +++ b/conn.go @@ -273,6 +273,9 @@ func (c *Conn) handleGreet(enhanced bool, arg string) { if c.server.EnableBINARYMIME { caps = append(caps, "BINARYMIME") } + if c.server.EnableDSN { + caps = append(caps, "DSN") + } if c.server.MaxMessageBytes > 0 { caps = append(caps, fmt.Sprintf("SIZE %v", c.server.MaxMessageBytes)) } @@ -396,6 +399,24 @@ func (c *Conn) handleMail(arg string) { } decodedMbox := value[1 : len(value)-1] opts.Auth = &decodedMbox + case "RET": + value := DSNReturn(strings.ToUpper(value)) + if value != ReturnFull && value != ReturnHeaders { + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Unsupported RET value") + return + } + opts.Return = value + case "ENVID": + value, err := decodeXtext(value) + if err != nil { + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Malformed xtext in ENVID") + return + } + if !checkPrintableASCII(value) { + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Only printable ASCII allowed in ENVID") + return + } + opts.EnvelopeID = value default: c.WriteResponse(500, EnhancedCode{5, 5, 4}, "Unknown MAIL FROM argument") return @@ -467,6 +488,15 @@ func encodeXtext(raw string) string { return out.String() } +func checkPrintableASCII(s string) bool { + for _, c := range s { + if c < 32 && c > 127 { + return false + } + } + return true +} + // MAIL state -> waiting for RCPTs followed by DATA func (c *Conn) handleRcpt(arg string) { if !c.fromReceived { @@ -478,19 +508,67 @@ func (c *Conn) handleRcpt(arg string) { return } - if (len(arg) < 4) || (strings.ToUpper(arg[0:3]) != "TO:") { - c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting RCPT arg syntax of TO:
") + if len(arg) < 4 || strings.ToUpper(arg[0:3]) != "TO:" { + c.WriteResponse(501, EnhancedCode{5, 5, 1}, "Was expecting TO arg syntax of TO:
") return } - - // TODO: This trim is probably too forgiving - recipient := strings.Trim(arg[3:], "<> ") + toArgs := strings.Split(strings.Trim(arg[3:], " "), " ") + if c.server.Strict { + if !strings.HasPrefix(toArgs[0], "<") || !strings.HasSuffix(toArgs[0], ">") { + c.WriteResponse(501, EnhancedCode{5, 5, 1}, "Was expecting TO arg syntax of TO:
") + return + } + } + recipient := toArgs[0] + if recipient == "" { + c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:
") + return + } + recipient = strings.Trim(recipient, "<>") if c.server.MaxRecipients > 0 && len(c.recipients) >= c.server.MaxRecipients { c.WriteResponse(552, EnhancedCode{5, 5, 3}, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients)) return } + opts := RcptOptions{} + + if len(toArgs) > 1 { + args, err := parseArgs(toArgs[1:]) + if err != nil { + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse TO ESMTP parameters") + return + } + + for key, value := range args { + switch key { + case "ORCPT": + case "NOTIFY": + notifyFlags := strings.Split(strings.ToUpper(value), ",") + seenFlags := make(map[string]struct{}) + for _, f := range notifyFlags { + if _, ok := seenFlags[f]; ok { + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "NOTIFY parameters cannot be specified multiple times") + return + } + switch DSNNotify(f) { + case NotifyNever: + if len(notifyFlags) != 1 { + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "NOTIFY=NEVER cannot be combined with other options") + return + } + case NotifyDelayed, NotifySuccess, NotifyFailure: + default: + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Unknown NOTIFY parameter") + return + } + seenFlags[f] = struct{}{} + opts.Notify = append(opts.Notify, DSNNotify(f)) + } + } + } + } + if err := c.Session().Rcpt(recipient, RcptOptions{}); err != nil { if smtpErr, ok := err.(*SMTPError); ok { c.WriteResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message) diff --git a/server.go b/server.go index a7c37c9..77d1e26 100644 --- a/server.go +++ b/server.go @@ -57,6 +57,10 @@ type Server struct { // Should be used only if backend supports it. EnableBINARYMIME bool + // Advertise DSN (RFC 3461) capability. + // Should be used only if backend supports it. + EnableDSN bool + // If set, the AUTH command will not be advertised and authentication // attempts will be rejected. This setting overrides AllowInsecureAuth. AuthDisabled bool