Skip to content

Commit f920d0f

Browse files
committed
Add support for postfix XFORWARD command.
1 parent ab24fe7 commit f920d0f

File tree

7 files changed

+159
-1
lines changed

7 files changed

+159
-1
lines changed

backend.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ type Session interface {
6363
//
6464
// r must be consumed before Data returns.
6565
Data(r io.Reader) error
66+
67+
// XForward is called for any attribute name/value pair.
68+
// See https://www.postfix.org/XFORWARD_README.html
69+
XForward(attrName, attrValue string) error
6670
}
6771

6872
// LMTPSession is an add-on interface for Session. It can be implemented by

cmd/smtp-debug-server/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ func (s *session) Data(r io.Reader) error {
3939
return nil
4040
}
4141

42+
func (s *session) XForward(attrName, attrValue string) error {
43+
return nil
44+
}
45+
4246
func (s *session) Reset() {}
4347

4448
func (s *session) Logout() error {

conn.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ func (c *Conn) handle(cmd string, arg string) {
137137
c.handleBdat(arg)
138138
case "DATA":
139139
c.handleData(arg)
140+
case "XFORWARD":
141+
c.handleXForward(arg)
140142
case "QUIT":
141143
c.writeResponse(221, EnhancedCode{2, 0, 0}, "Bye")
142144
c.Close()
@@ -308,6 +310,9 @@ func (c *Conn) handleGreet(enhanced bool, arg string) {
308310
caps = append(caps, fmt.Sprintf("MT-PRIORITY %s", c.server.MtPriorityProfile))
309311
}
310312
}
313+
if c.server.EnableXFORWARD {
314+
caps = append(caps, "XFORWARD NAME ADDR PROTO HELO")
315+
}
311316

312317
args := []string{"Hello " + domain}
313318
args = append(args, caps...)
@@ -990,6 +995,42 @@ func (c *Conn) handleData(arg string) {
990995
c.writeResponse(code, enhancedCode, msg)
991996
}
992997

998+
func (c *Conn) handleXForward(arg string) {
999+
/*
1000+
Handling is according to https://www.postfix.org/XFORWARD_README.html
1001+
xforward-command = XFORWARD 1*( SP attribute-name"="attribute-value )
1002+
attribute-name = ( NAME | ADDR | PORT | PROTO | HELO | IDENT | SOURCE )
1003+
attribute-value = xtext
1004+
Attribute values are xtext encoded as per RFC 1891. https://datatracker.ietf.org/doc/html/rfc1891
1005+
*/
1006+
1007+
if c.bdatPipe != nil {
1008+
c.writeResponse(502, EnhancedCode{5, 5, 1}, "XFORWARD not allowed during message transfer")
1009+
return
1010+
}
1011+
1012+
attributes := strings.Split(arg, " ")
1013+
for _, attribute := range attributes {
1014+
if !strings.Contains(attribute, "=") {
1015+
c.writeResponse(501, EnhancedCode{5, 5, 1}, "Bad XFORWARD command.")
1016+
return
1017+
}
1018+
nameValue := strings.SplitN(attribute, "=", 2)
1019+
attrName := strings.ToUpper(nameValue[0]) // attr names are case insensitive, normalise
1020+
attrValue, err := decodeXtext(nameValue[1])
1021+
if err != nil {
1022+
c.writeResponse(501, EnhancedCode{5, 5, 1}, "Bad XFORWARD value.")
1023+
return
1024+
}
1025+
err = c.Session().XForward(attrName, attrValue)
1026+
if err != nil {
1027+
c.writeResponse(501, EnhancedCode{5, 0, 0}, "Error: transaction failed: "+err.Error())
1028+
return
1029+
}
1030+
}
1031+
c.writeResponse(250, EnhancedCode{2, 0, 0}, "OK")
1032+
}
1033+
9931034
func (c *Conn) handleBdat(arg string) {
9941035
args := strings.Fields(arg)
9951036
if len(args) == 0 {

example_server_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ func (s *Session) Data(r io.Reader) error {
6868
return nil
6969
}
7070

71+
func (s *Session) XForward(attrName, attrValue string) error {
72+
log.Printf("XFORWARD %s=%s\n", attrName, attrValue)
73+
return nil
74+
}
75+
7176
func (s *Session) Reset() {}
7277

7378
func (s *Session) Logout() error {
@@ -99,6 +104,7 @@ func ExampleServer() {
99104
s.MaxMessageBytes = 1024 * 1024
100105
s.MaxRecipients = 50
101106
s.AllowInsecureAuth = true
107+
s.EnableXFORWARD = true
102108

103109
log.Println("Starting server at", s.Addr)
104110
if err := s.ListenAndServe(); err != nil {

parse.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ func parseCmd(line string) (cmd string, arg string, err error) {
2222
switch {
2323
case strings.HasPrefix(strings.ToUpper(line), "STARTTLS"):
2424
return "STARTTLS", "", nil
25+
case strings.HasPrefix(strings.ToUpper(line), "XFORWARD"):
26+
return "XFORWARD", strings.TrimSpace(line[8:]), nil
2527
case l == 0:
2628
return "", "", nil
2729
case l < 4:

server.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ type Server struct {
7373
// Advertise MT-PRIORITY (RFC 6710) capability.
7474
// Should only be used if backend supports it.
7575
EnableMTPRIORITY bool
76+
77+
// Advertise XFORWARD NAME ADDR PROTO HELO
78+
// (https://www.postfix.org/XFORWARD_README.html) capability.
79+
// Should only be used if backend supports it.
80+
EnableXFORWARD bool
81+
7682
// The priority profile mapping as defined
7783
// in RFC 6710 section 10.2.
7884
//

server_test.go

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type message struct {
2525
RcptOpts []*smtp.RcptOptions
2626
Data []byte
2727
Opts *smtp.MailOptions
28+
XForward map[string]string
2829
}
2930

3031
type backend struct {
@@ -98,7 +99,9 @@ func (s *session) Auth(mech string) (sasl.Server, error) {
9899
}
99100

100101
func (s *session) Reset() {
101-
s.msg = &message{}
102+
s.msg = &message{
103+
XForward: make(map[string]string),
104+
}
102105
}
103106

104107
func (s *session) Logout() error {
@@ -157,6 +160,11 @@ func (s *session) Data(r io.Reader) error {
157160
return nil
158161
}
159162

163+
func (s *session) XForward(attrName, attrValue string) error {
164+
s.msg.XForward[attrName] = attrValue
165+
return nil
166+
}
167+
160168
func (s *session) LMTPData(r io.Reader, collector smtp.StatusCollector) error {
161169
if err := s.Data(r); err != nil {
162170
return err
@@ -1712,3 +1720,90 @@ func TestServerMTPRIORITY(t *testing.T) {
17121720
t.Fatal("Incorrect MtPriority parameter value:", fmt.Sprintf("expected %d, got %d", expectedPriority, *priority))
17131721
}
17141722
}
1723+
1724+
func TestServerXFORWARD(t *testing.T) {
1725+
be, s, c, scanner, caps := testServerEhlo(t,
1726+
func(s *smtp.Server) {
1727+
s.EnableXFORWARD = true
1728+
})
1729+
defer s.Close()
1730+
defer c.Close()
1731+
1732+
if _, ok := caps["XFORWARD NAME ADDR PROTO HELO"]; !ok {
1733+
t.Fatal("Missing capability: XFORWARD")
1734+
}
1735+
1736+
io.WriteString(c, "MAIL FROM:<[email protected]>\r\n")
1737+
scanner.Scan()
1738+
if !strings.HasPrefix(scanner.Text(), "250 ") {
1739+
t.Fatal("Invalid MAIL response:", scanner.Text())
1740+
}
1741+
1742+
io.WriteString(c, "RCPT TO:<[email protected]>\r\n")
1743+
scanner.Scan()
1744+
if !strings.HasPrefix(scanner.Text(), "250 ") {
1745+
t.Fatal("Invalid RCPT response:", scanner.Text())
1746+
}
1747+
1748+
// Atrribute names are case insensitive, check we normalised them
1749+
io.WriteString(c, "XFORWARD NAME=spike.porcupine.org ADDR=168.100.189.2 proto=ESMTP\r\n")
1750+
scanner.Scan()
1751+
if !strings.HasPrefix(scanner.Text(), "250 ") {
1752+
t.Fatal("Invalid XFORWARD response:", scanner.Text())
1753+
}
1754+
1755+
io.WriteString(c, "DATA\r\n")
1756+
scanner.Scan()
1757+
if !strings.HasPrefix(scanner.Text(), "354 ") {
1758+
t.Fatal("Invalid DATA response:", scanner.Text())
1759+
}
1760+
1761+
io.WriteString(c, "From: [email protected]\r\n")
1762+
io.WriteString(c, "\r\n")
1763+
io.WriteString(c, "Hey\r <3\r\n")
1764+
io.WriteString(c, "..this dot is fine\r\n")
1765+
io.WriteString(c, ".\r\n")
1766+
scanner.Scan()
1767+
if !strings.HasPrefix(scanner.Text(), "250 ") {
1768+
t.Fatal("Invalid DATA response:", scanner.Text())
1769+
}
1770+
1771+
if len(be.messages) != 0 || len(be.anonmsgs) != 1 {
1772+
t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs)
1773+
}
1774+
1775+
msg := be.anonmsgs[0]
1776+
1777+
if len(msg.XForward) != 3 {
1778+
t.Fatal("Invalid XFORWARD data:", msg.XForward)
1779+
}
1780+
1781+
if got, ok := msg.XForward["NAME"]; !ok {
1782+
t.Fatal("Missing XFORWARD attribute NAME")
1783+
} else if got != "spike.porcupine.org" {
1784+
t.Fatal("Invalid XFORWARD attribute value for NAME", got)
1785+
}
1786+
1787+
if got, ok := msg.XForward["ADDR"]; !ok {
1788+
t.Fatal("Missing XFORWARD attribute ADDR")
1789+
} else if got != "168.100.189.2" {
1790+
t.Fatal("Invalid XFORWARD attribute value for ADDR", got)
1791+
}
1792+
1793+
if got, ok := msg.XForward["PROTO"]; !ok {
1794+
t.Fatal("Missing (normalised) XFORWARD attribute PROTO")
1795+
} else if got != "ESMTP" {
1796+
t.Fatal("Invalid (normalised) XFORWARD attribute value for PROTO", got)
1797+
}
1798+
1799+
// Sanity that the rest of the connection continued fine
1800+
if msg.From != "[email protected]" {
1801+
t.Fatal("Invalid mail sender:", msg.From)
1802+
}
1803+
if len(msg.To) != 1 || msg.To[0] != "[email protected]" {
1804+
t.Fatal("Invalid mail recipients:", msg.To)
1805+
}
1806+
if string(msg.Data) != "From: [email protected]\r\n\r\nHey\r <3\r\n.this dot is fine\r\n" {
1807+
t.Fatal("Invalid mail data:", string(msg.Data))
1808+
}
1809+
}

0 commit comments

Comments
 (0)