diff --git a/backend.go b/backend.go index febc839..c016396 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,45 @@ 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" +) + +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..86dc102 100644 --- a/conn.go +++ b/conn.go @@ -396,6 +396,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 +485,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 +505,73 @@ 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), ",") + seenDelay, seenSuccess := false, false + for _, f := range notifyFlags { + 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: + if seenDelay { + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "NOTIFY=DELAY cannot be specified multiple times") + return + } + seenDelay = true + case NotifySuccess: + if seenSuccess { + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "NOTIFY=SUCCESS cannot be specified multiple times") + return + } + seenSuccess = true + default: + c.WriteResponse(501, EnhancedCode{5, 5, 4}, "Unknown NOTIFY parameter") + return + } + 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)