Skip to content

Commit

Permalink
server: implement DSN extension (RFC 3461, RFC 6533)
Browse files Browse the repository at this point in the history
  • Loading branch information
ikedas authored and emersion committed Nov 3, 2023
1 parent 42a0048 commit 7ac0d7e
Show file tree
Hide file tree
Showing 4 changed files with 466 additions and 9 deletions.
222 changes: 218 additions & 4 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,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))
} else {
Expand Down Expand Up @@ -353,6 +356,31 @@ func (c *Conn) handleMail(arg string) {
return
}
opts.Body = BodyType(value)
case "RET":
if !c.server.EnableDSN {
c.writeResponse(504, EnhancedCode{5, 5, 4}, "RET is not implemented")
return
}
value = strings.ToUpper(value)
switch DSNReturn(value) {
case DSNReturnFull, DSNReturnHeaders:
// This space is intentionally left blank
default:
c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unknown RET value")
return
}
opts.Return = DSNReturn(value)
case "ENVID":
if !c.server.EnableDSN {
c.writeResponse(504, EnhancedCode{5, 5, 4}, "ENVID is not implemented")
return
}
value, err := decodeXtext(value)
if err != nil || value == "" || !isPrintableASCII(value) {
c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed ENVID parameter value")
return
}
opts.EnvelopeID = value
case "AUTH":
value, err := decodeXtext(value)
if err != nil || value == "" {
Expand Down Expand Up @@ -421,6 +449,122 @@ func decodeXtext(val string) (string, error) {
return decoded, nil
}

// This regexp matches 'EmbeddedUnicodeChar' token defined in
// https://datatracker.ietf.org/doc/html/rfc6533.html#section-3
// however it is intentionally relaxed by requiring only '\x{HEX}' to be
// present. It also matches disallowed characters in QCHAR and QUCHAR defined
// in above.
// So it allows us to detect malformed values and report them appropriately.
var eUOrDCharRe = regexp.MustCompile(`\\x[{][0-9A-F]+[}]|[[:cntrl:] \\+=]`)

// Decodes the utf-8-addr-xtext or the utf-8-addr-unitext form.
func decodeUTF8AddrXtext(val string) (string, error) {
var replaceErr error
decoded := eUOrDCharRe.ReplaceAllStringFunc(val, func(match string) string {
if len(match) == 1 {
replaceErr = errors.New("disallowed character:" + match)
return ""
}

hexpoint := match[3 : len(match)-1]
char, err := strconv.ParseUint(hexpoint, 16, 21)
if err != nil {
replaceErr = err
return ""
}
switch len(hexpoint) {
case 2:
switch {
// all xtext-specials
case 0x01 <= char && char <= 0x09 ||
0x11 <= char && char <= 0x19 ||
char == 0x10 || char == 0x20 ||
char == 0x2B || char == 0x3D || char == 0x7F:
// 2-digit forms
case char == 0x5C || 0x80 <= char && char <= 0xFF:
// This space is intentionally left blank
default:
replaceErr = errors.New("illegal hexpoint:" + hexpoint)
return ""
}
// 3-digit forms
case 3:
switch {
case 0x100 <= char && char <= 0xFFF:
// This space is intentionally left blank
default:
replaceErr = errors.New("illegal hexpoint:" + hexpoint)
return ""
}
// 4-digit forms excluding surrogate
case 4:
switch {
case 0x1000 <= char && char <= 0xD7FF:
case 0xE000 <= char && char <= 0xFFFF:
// This space is intentionally left blank
default:
replaceErr = errors.New("illegal hexpoint:" + hexpoint)
return ""
}
// 5-digit forms
case 5:
switch {
case 0x1_0000 <= char && char <= 0xF_FFFF:
// This space is intentionally left blank
default:
replaceErr = errors.New("illegal hexpoint:" + hexpoint)
return ""
}
// 6-digit forms
case 6:
switch {
case 0x10_0000 <= char && char <= 0x10_FFFF:
// This space is intentionally left blank
default:
replaceErr = errors.New("illegal hexpoint:" + hexpoint)
return ""
}
// the other invalid forms
default:
replaceErr = errors.New("illegal hexpoint:" + hexpoint)
return ""
}

return string(rune(char))
})
if replaceErr != nil {
return "", replaceErr
}

return decoded, nil
}

func decodeTypedAddress(val string) (DSNAddressType, string, error) {
tv := strings.SplitN(val, ";", 2)
if len(tv) != 2 || tv[0] == "" || tv[1] == "" {
return "", "", errors.New("bad address")
}
aType, aAddr := strings.ToUpper(tv[0]), tv[1]

var err error
switch DSNAddressType(aType) {
case DSNAddressTypeRFC822:
aAddr, err = decodeXtext(aAddr)
if err == nil && !isPrintableASCII(aAddr) {
err = errors.New("illegal address:" + aAddr)
}
case DSNAddressTypeUTF8:
aAddr, err = decodeUTF8AddrXtext(aAddr)
default:
err = errors.New("unknown address type:" + aType)
}
if err != nil {
return "", "", err
}

return DSNAddressType(aType), aAddr, nil
}

func encodeXtext(raw string) string {
var out strings.Builder
out.Grow(len(raw))
Expand All @@ -438,6 +582,15 @@ func encodeXtext(raw string) string {
return out.String()
}

func isPrintableASCII(val string) bool {
for _, ch := range val {
if ch < ' ' || '~' < ch {
return false
}
}
return true
}

// MAIL state -> waiting for RCPTs followed by DATA
func (c *Conn) handleRcpt(arg string) {
if !c.fromReceived {
Expand All @@ -461,17 +614,54 @@ func (c *Conn) handleRcpt(arg string) {
c.writeResponse(501, EnhancedCode{5, 5, 2}, "Was expecting RCPT arg syntax of TO:<address>")
return
}
if len(strings.Fields(p.s)) > 0 {
c.writeResponse(501, EnhancedCode{5, 5, 2}, "RCPT parameters are not supported")
return
}

if c.server.MaxRecipients > 0 && len(c.recipients) >= c.server.MaxRecipients {
c.writeResponse(452, EnhancedCode{4, 5, 3}, fmt.Sprintf("Maximum limit of %v recipients reached", c.server.MaxRecipients))
return
}

args, err := parseArgs(p.s)
if err != nil {
c.writeResponse(501, EnhancedCode{5, 5, 4}, "Unable to parse RCPT ESMTP parameters")
return
}

opts := &RcptOptions{}

for key, value := range args {
switch key {
case "NOTIFY":
if !c.server.EnableDSN {
c.writeResponse(504, EnhancedCode{5, 5, 4}, "NOTIFY is not implemented")
return
}
notify := []DSNNotify{}
for _, val := range strings.Split(value, ",") {
notify = append(notify, DSNNotify(strings.ToUpper(val)))
}
if err := checkNotifySet(notify); err != nil {
c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed NOTIFY parameter value")
return
}
opts.Notify = notify
case "ORCPT":
if !c.server.EnableDSN {
c.writeResponse(504, EnhancedCode{5, 5, 4}, "ORCPT is not implemented")
return
}
aType, aAddr, err := decodeTypedAddress(value)
if err != nil || aAddr == "" {
c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed ORCPT parameter value")
return
}
opts.OriginalRecipientType = aType
opts.OriginalRecipient = aAddr
default:
c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown RCPT TO argument")
return
}
}

if err := c.Session().Rcpt(recipient, opts); err != nil {
if smtpErr, ok := err.(*SMTPError); ok {
c.writeResponse(smtpErr.Code, smtpErr.EnhancedCode, smtpErr.Message)
Expand All @@ -484,6 +674,30 @@ func (c *Conn) handleRcpt(arg string) {
c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("I'll make sure <%v> gets this", recipient))
}

func checkNotifySet(values []DSNNotify) error {
if len(values) == 0 {
return errors.New("Malformed NOTIFY parameter value")
}

seen := map[DSNNotify]struct{}{}
for _, val := range values {
switch val {
case DSNNotifyNever, DSNNotifyDelayed, DSNNotifyFailure, DSNNotifySuccess:
if _, ok := seen[val]; ok {
return errors.New("Malformed NOTIFY parameter value")
}
default:
return errors.New("Malformed NOTIFY parameter value")
}
seen[val] = struct{}{}
}
if _, ok := seen[DSNNotifyNever]; ok && len(seen) > 1 {
return errors.New("Malformed NOTIFY parameter value")
}

return nil
}

func (c *Conn) handleAuth(arg string) {
if c.helo == "" {
c.writeResponse(502, EnhancedCode{5, 5, 1}, "Please introduce yourself first.")
Expand Down
4 changes: 4 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,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
Expand Down
Loading

0 comments on commit 7ac0d7e

Please sign in to comment.