-
Notifications
You must be signed in to change notification settings - Fork 229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add client and server support for SMTP DSN extension #135
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -400,12 +411,48 @@ 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 | ||
} | ||
if _, _, err := c.cmd(25, "RCPT TO:<%s>", to); err != nil { | ||
cmdStr := "RCPT TO:<%s>" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it safe to just append content to We should probably set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It applies to MAIL FROM too which uses similar code. |
||
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) | ||
|
@@ -546,7 +593,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 | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -121,7 +121,7 @@ func TestBasic(t *testing.T) { | |
t.Fatalf("AUTH failed: %s", err) | ||
} | ||
|
||
if err := c.Rcpt("[email protected]>\r\nDATA\r\nInjected message body\r\n.\r\nQUIT\r\n"); err == nil { | ||
if err := c.Rcpt("[email protected]>\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("[email protected]>\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("[email protected]", nil); err != nil { | ||
t.Fatalf("MAIL failed: %s", err) | ||
} | ||
if err := c.Rcpt("[email protected]"); err != nil { | ||
if err := c.Rcpt("[email protected]", nil); err != nil { | ||
t.Fatalf("RCPT failed: %s", err) | ||
} | ||
msg := `From: [email protected] | ||
|
@@ -922,7 +922,7 @@ func TestLMTP(t *testing.T) { | |
if err := c.Mail("[email protected]", nil); err != nil { | ||
t.Fatalf("MAIL failed: %s", err) | ||
} | ||
if err := c.Rcpt("[email protected]"); err != nil { | ||
if err := c.Rcpt("[email protected]", nil); err != nil { | ||
t.Fatalf("RCPT failed: %s", err) | ||
} | ||
msg := `From: [email protected] | ||
|
@@ -1007,10 +1007,10 @@ func TestLMTPData(t *testing.T) { | |
if err := c.Mail("[email protected]", nil); err != nil { | ||
t.Fatalf("MAIL failed: %s", err) | ||
} | ||
if err := c.Rcpt("[email protected]"); err != nil { | ||
if err := c.Rcpt("[email protected]", nil); err != nil { | ||
t.Fatalf("RCPT failed: %s", err) | ||
} | ||
if err := c.Rcpt("[email protected]"); err != nil { | ||
if err := c.Rcpt("[email protected]", nil); err != nil { | ||
t.Fatalf("RCPT failed: %s", err) | ||
} | ||
msg := `From: [email protected] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,20 +508,68 @@ 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, "<>") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we keep the TODO about this trim being not quite what we want? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also don't like the existing TODO, and proposed to use |
||
|
||
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 | ||
} | ||
|
||
if err := c.Session().Rcpt(recipient); err != nil { | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to error out on unrecognized parameter? |
||
case "ORCPT": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a |
||
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) | ||
return | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If #145 is accepted,
opts
should be switched to a pointer as well.