Skip to content

Commit 54dd31d

Browse files
authored
Implement DELIVERBY (RFC 2852)
1 parent 495c409 commit 54dd31d

File tree

7 files changed

+200
-6
lines changed

7 files changed

+200
-6
lines changed

client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,16 @@ func (c *Client) Rcpt(to string, opts *RcptOptions) error {
534534
if _, ok := c.ext["RRVS"]; ok && opts != nil && !opts.RequireRecipientValidSince.IsZero() {
535535
sb.WriteString(fmt.Sprintf(" RRVS=%s", opts.RequireRecipientValidSince.Format(time.RFC3339)))
536536
}
537+
if _, ok := c.ext["DELIVERBY"]; ok && opts != nil && opts.DeliverBy != nil {
538+
if opts.DeliverBy.Mode == DeliverByReturn && opts.DeliverBy.Time < 1 {
539+
return errors.New("smtp: DELIVERBY mode must be greater than zero with return mode")
540+
}
541+
arg := fmt.Sprintf(" BY=%d;%s", int(opts.DeliverBy.Time.Seconds()), opts.DeliverBy.Mode)
542+
if opts.DeliverBy.Trace {
543+
arg += "T"
544+
}
545+
sb.WriteString(arg)
546+
}
537547
if _, _, err := c.cmd(25, "%s", sb.String()); err != nil {
538548
return err
539549
}

client_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,3 +1079,39 @@ func TestClientRRVS(t *testing.T) {
10791079
t.Errorf("wrote %q; want %q", actualcmds, client)
10801080
}
10811081
}
1082+
1083+
var deliverByServer = `220 hello world
1084+
250 ok
1085+
`
1086+
1087+
var deliverByClient = `RCPT TO:<root@nsa.gov> BY=100;RT
1088+
`
1089+
1090+
func TestClientDELIVERBY(t *testing.T) {
1091+
server := strings.Join(strings.Split(deliverByServer, "\n"), "\r\n")
1092+
client := strings.Join(strings.Split(deliverByClient, "\n"), "\r\n")
1093+
1094+
var wrote bytes.Buffer
1095+
var fake faker
1096+
fake.ReadWriter = struct {
1097+
io.Reader
1098+
io.Writer
1099+
}{
1100+
strings.NewReader(server),
1101+
&wrote,
1102+
}
1103+
c := NewClient(fake)
1104+
c.didHello = true
1105+
c.ext = map[string]string{"DELIVERBY": ""}
1106+
c.Rcpt("root@nsa.gov", &RcptOptions{
1107+
DeliverBy: &DeliverByOptions{
1108+
Time: 100 * time.Second,
1109+
Mode: DeliverByReturn,
1110+
Trace: true,
1111+
},
1112+
})
1113+
c.Close()
1114+
if actualcmds := wrote.String(); client != actualcmds {
1115+
t.Errorf("wrote %q; want %q", actualcmds, client)
1116+
}
1117+
}

conn.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,13 @@ func (c *Conn) handleGreet(enhanced bool, arg string) {
294294
if c.server.EnableRRVS {
295295
caps = append(caps, "RRVS")
296296
}
297+
if c.server.EnableDELIVERBY {
298+
if c.server.MinimumDeliverByTime == 0 {
299+
caps = append(caps, "DELIVERBY")
300+
} else {
301+
caps = append(caps, fmt.Sprintf("DELIVERBY %d", int(c.server.MinimumDeliverByTime.Seconds())))
302+
}
303+
}
297304

298305
args := []string{"Hello " + domain}
299306
args = append(args, caps...)
@@ -731,6 +738,23 @@ func (c *Conn) handleRcpt(arg string) {
731738
return
732739
}
733740
opts.RequireRecipientValidSince = rrvsTime
741+
case "BY":
742+
if !c.server.EnableDELIVERBY {
743+
c.writeResponse(504, EnhancedCode{5, 5, 4}, "DELIVERBY is not implemented")
744+
return
745+
}
746+
deliverBy := parseDeliverByArgument(value)
747+
if deliverBy == nil {
748+
c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed BY parameter value")
749+
return
750+
}
751+
if c.server.MinimumDeliverByTime != 0 &&
752+
deliverBy.Mode == DeliverByReturn &&
753+
deliverBy.Time < c.server.MinimumDeliverByTime {
754+
c.writeResponse(501, EnhancedCode{5, 5, 4}, "BY parameter is below server minimum")
755+
return
756+
}
757+
opts.DeliverBy = deliverBy
734758
default:
735759
c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown RCPT TO argument")
736760
return

parse.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package smtp
22

33
import (
44
"fmt"
5+
"strconv"
56
"strings"
7+
"time"
68
)
79

810
// cutPrefixFold is a version of strings.CutPrefix which is case-insensitive.
@@ -74,6 +76,29 @@ func parseHelloArgument(arg string) (string, error) {
7476
return domain, nil
7577
}
7678

79+
// Parses the BY argument defined in RFC2852 section 4.
80+
// Returns pointer to options or nil if invalid.
81+
func parseDeliverByArgument(arg string) *DeliverByOptions {
82+
secondsStr, modeStr, ok := strings.Cut(arg, ";")
83+
if !ok {
84+
return nil
85+
}
86+
modeStr, traceValue := strings.CutSuffix(modeStr, "T")
87+
if modeStr != string(DeliverByNotify) && modeStr != string(DeliverByReturn) {
88+
return nil
89+
}
90+
modeValue := DeliverByMode(modeStr)
91+
secondsValue, err := strconv.Atoi(secondsStr)
92+
if err != nil || (modeValue == DeliverByReturn && secondsValue < 1) {
93+
return nil
94+
}
95+
return &DeliverByOptions{
96+
Time: time.Duration(secondsValue) * time.Second,
97+
Mode: modeValue,
98+
Trace: traceValue,
99+
}
100+
}
101+
77102
// parser parses command arguments defined in RFC 5321 section 4.1.2.
78103
type parser struct {
79104
s string

server.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ type Server struct {
6161
// Should be used only if backend supports it.
6262
EnableRRVS bool
6363

64+
// Advertise DELIVERBY (RFC 2852) capability.
65+
// Should be used only if backend supports it.
66+
EnableDELIVERBY bool
67+
// The minimum time, with seconds precision, that a client
68+
// may specify in the BY argument with return mode.
69+
// A zero value indicates no set minimum.
70+
// Only use if DELIVERBY is enabled.
71+
MinimumDeliverByTime time.Duration
72+
6473
// The server backend.
6574
Backend Backend
6675

server_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,3 +1580,73 @@ func TestServerRRVS(t *testing.T) {
15801580
t.Fatal("Invalid RRVS parameter value:", fmt.Sprintf("%#v", opts[1].RequireRecipientValidSince))
15811581
}
15821582
}
1583+
1584+
func TestServerDELIVERBY(t *testing.T) {
1585+
be, s, c, scanner, caps := testServerEhlo(t,
1586+
func(s *smtp.Server) {
1587+
s.EnableDELIVERBY = true
1588+
s.MinimumDeliverByTime = 50 * time.Second
1589+
})
1590+
defer s.Close()
1591+
defer c.Close()
1592+
1593+
if _, ok := caps["DELIVERBY 50"]; !ok {
1594+
t.Fatal("Missing capability: DELIVERBY")
1595+
}
1596+
1597+
io.WriteString(c, "MAIL FROM:<root@nsa.gov>\r\n")
1598+
scanner.Scan()
1599+
1600+
malformedMsgs := []string{
1601+
"RCPT TO:<root@gchq.gov.uk> BY=",
1602+
"RCPT TO:<root@gchq.gov.uk> BY=1234",
1603+
"RCPT TO:<root@gchq.gov.uk> BY=123;RT;",
1604+
"RCPT TO:<root@gchq.gov.uk> BY=0;R",
1605+
"RCPT TO:<root@gchq.gov.uk> BY=49;RT",
1606+
}
1607+
1608+
for _, msg := range malformedMsgs {
1609+
io.WriteString(c, msg+"\r\n")
1610+
scanner.Scan()
1611+
if !strings.HasPrefix(scanner.Text(), "501 5.5.4") {
1612+
t.Fatal("Unexpected res on malformed BY parameter value:", scanner.Text())
1613+
}
1614+
}
1615+
1616+
io.WriteString(c, "RCPT TO:<root@gchq.gov.uk> BY=100;NT\r\n")
1617+
scanner.Scan()
1618+
1619+
if !strings.HasPrefix(scanner.Text(), "250 ") {
1620+
t.Fatal("Invalid BY parameter value:", scanner.Text())
1621+
}
1622+
1623+
// complete the transaction
1624+
io.WriteString(c, "DATA\r\n")
1625+
scanner.Scan()
1626+
io.WriteString(c, "Hey <3\r\n")
1627+
io.WriteString(c, ".\r\n")
1628+
scanner.Scan()
1629+
1630+
opts := be.anonmsgs[0].RcptOpts
1631+
if opts == nil || len(opts) != 1 {
1632+
t.Fatal("Invalid number of recipients:", opts)
1633+
}
1634+
1635+
deliverByOpts := opts[0].DeliverBy
1636+
1637+
if deliverByOpts == nil {
1638+
t.Fatal("Deliver by options is nil:", opts)
1639+
}
1640+
1641+
expectedDeliverByOpts := smtp.DeliverByOptions{
1642+
Time: 100 * time.Second,
1643+
Mode: smtp.DeliverByNotify,
1644+
Trace: true,
1645+
}
1646+
1647+
if deliverByOpts.Time != expectedDeliverByOpts.Time ||
1648+
deliverByOpts.Mode != expectedDeliverByOpts.Mode ||
1649+
deliverByOpts.Trace != expectedDeliverByOpts.Trace {
1650+
t.Fatal("Incorrect BY parameter value:", fmt.Sprintf("expected %#v, got %#v", expectedDeliverByOpts, deliverByOpts))
1651+
}
1652+
}

smtp.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@
33
// It also implements the following extensions:
44
//
55
// - 8BITMIME (RFC 1652)
6-
// - AUTH (RFC 2554)
7-
// - STARTTLS (RFC 3207)
86
// - ENHANCEDSTATUSCODES (RFC 2034)
9-
// - SMTPUTF8 (RFC 6531)
10-
// - REQUIRETLS (RFC 8689)
7+
// - AUTH (RFC 2554)
8+
// - DELIVERBY (RFC 2852)
119
// - CHUNKING (RFC 3030)
1210
// - BINARYMIME (RFC 3030)
11+
// - STARTTLS (RFC 3207)
1312
// - DSN (RFC 3461, RFC 6533)
13+
// - SMTPUTF8 (RFC 6531)
14+
// - RRVS (RFC 7293)
15+
// - REQUIRETLS (RFC 8689)
1416
//
1517
// LMTP (RFC 2033) is also supported.
1618
//
1719
// Additional extensions may be handled by other packages.
1820
package smtp
1921

20-
import "time"
22+
import (
23+
"time"
24+
)
2125

2226
type BodyType string
2327

@@ -84,6 +88,19 @@ const (
8488
DSNAddressTypeUTF8 DSNAddressType = "UTF-8"
8589
)
8690

91+
type DeliverByMode string
92+
93+
const (
94+
DeliverByNotify DeliverByMode = "N"
95+
DeliverByReturn DeliverByMode = "R"
96+
)
97+
98+
type DeliverByOptions struct {
99+
Time time.Duration
100+
Mode DeliverByMode
101+
Trace bool
102+
}
103+
87104
// RcptOptions contains parameters for the RCPT command.
88105
type RcptOptions struct {
89106
// Value of NOTIFY= argument, NEVER or a combination of either of
@@ -95,6 +112,9 @@ type RcptOptions struct {
95112
OriginalRecipient string
96113

97114
// Time value of the RRVS= argument
98-
// Left as the zero time if unset.
115+
// or the zero time if unset.
99116
RequireRecipientValidSince time.Time
117+
118+
// Value of BY= argument or nil if unset.
119+
DeliverBy *DeliverByOptions
100120
}

0 commit comments

Comments
 (0)