Skip to content

Commit

Permalink
Implement DSN extension parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
foxcpp committed Jan 22, 2021
1 parent 5639f4b commit aff67d8
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 23 deletions.
50 changes: 49 additions & 1 deletion backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
//
Expand Down
78 changes: 61 additions & 17 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
91 changes: 86 additions & 5 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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:<address>")
if len(arg) < 4 || strings.ToUpper(arg[0:3]) != "TO:" {
c.WriteResponse(501, EnhancedCode{5, 5, 1}, "Was expecting TO arg syntax of TO:<address>")
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:<address>")
return
}
}
recipient := toArgs[0]
if recipient == "" {
c.WriteResponse(501, EnhancedCode{5, 5, 2}, "Was expecting MAIL arg syntax of FROM:<address>")
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)
Expand Down

0 comments on commit aff67d8

Please sign in to comment.