Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ type Session interface {
//
// r must be consumed before Data returns.
Data(r io.Reader) error

// XForward is called for any attribute name/value pair.
// See https://www.postfix.org/XFORWARD_README.html
XForward(attrName, attrValue string) error
}

// LMTPSession is an add-on interface for Session. It can be implemented by
Expand Down
4 changes: 4 additions & 0 deletions cmd/smtp-debug-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func (s *session) Data(r io.Reader) error {
return nil
}

func (s *session) XForward(attrName, attrValue string) error {
return nil
}

func (s *session) Reset() {}

func (s *session) Logout() error {
Expand Down
41 changes: 41 additions & 0 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ func (c *Conn) handle(cmd string, arg string) {
c.handleBdat(arg)
case "DATA":
c.handleData(arg)
case "XFORWARD":
c.handleXForward(arg)
case "QUIT":
c.writeResponse(221, EnhancedCode{2, 0, 0}, "Bye")
c.Close()
Expand Down Expand Up @@ -308,6 +310,9 @@ func (c *Conn) handleGreet(enhanced bool, arg string) {
caps = append(caps, fmt.Sprintf("MT-PRIORITY %s", c.server.MtPriorityProfile))
}
}
if c.server.EnableXFORWARD {
caps = append(caps, "XFORWARD NAME ADDR PROTO HELO")
}

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

func (c *Conn) handleXForward(arg string) {
/*
Handling is according to https://www.postfix.org/XFORWARD_README.html
xforward-command = XFORWARD 1*( SP attribute-name"="attribute-value )
attribute-name = ( NAME | ADDR | PORT | PROTO | HELO | IDENT | SOURCE )
attribute-value = xtext
Attribute values are xtext encoded as per RFC 1891. https://datatracker.ietf.org/doc/html/rfc1891
*/

if c.bdatPipe != nil {
c.writeResponse(502, EnhancedCode{5, 5, 1}, "XFORWARD not allowed during message transfer")
return
}

attributes := strings.Split(arg, " ")
for _, attribute := range attributes {
if !strings.Contains(attribute, "=") {
c.writeResponse(501, EnhancedCode{5, 5, 1}, "Bad XFORWARD command.")
return
}
nameValue := strings.SplitN(attribute, "=", 2)
attrName := strings.ToUpper(nameValue[0]) // attr names are case insensitive, normalise
attrValue, err := decodeXtext(nameValue[1])
if err != nil {
c.writeResponse(501, EnhancedCode{5, 5, 1}, "Bad XFORWARD value.")
return
}
err = c.Session().XForward(attrName, attrValue)
if err != nil {
c.writeResponse(501, EnhancedCode{5, 0, 0}, "Error: transaction failed: "+err.Error())
return
}
}
c.writeResponse(250, EnhancedCode{2, 0, 0}, "OK")
}

func (c *Conn) handleBdat(arg string) {
args := strings.Fields(arg)
if len(args) == 0 {
Expand Down
6 changes: 6 additions & 0 deletions example_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ func (s *Session) Data(r io.Reader) error {
return nil
}

func (s *Session) XForward(attrName, attrValue string) error {
log.Printf("XFORWARD %s=%s\n", attrName, attrValue)
return nil
}

func (s *Session) Reset() {}

func (s *Session) Logout() error {
Expand Down Expand Up @@ -99,6 +104,7 @@ func ExampleServer() {
s.MaxMessageBytes = 1024 * 1024
s.MaxRecipients = 50
s.AllowInsecureAuth = true
s.EnableXFORWARD = true

log.Println("Starting server at", s.Addr)
if err := s.ListenAndServe(); err != nil {
Expand Down
2 changes: 2 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ func parseCmd(line string) (cmd string, arg string, err error) {
switch {
case strings.HasPrefix(strings.ToUpper(line), "STARTTLS"):
return "STARTTLS", "", nil
case strings.HasPrefix(strings.ToUpper(line), "XFORWARD"):
return "XFORWARD", strings.TrimSpace(line[8:]), nil
case l == 0:
return "", "", nil
case l < 4:
Expand Down
6 changes: 6 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ type Server struct {
// Advertise MT-PRIORITY (RFC 6710) capability.
// Should only be used if backend supports it.
EnableMTPRIORITY bool

// Advertise XFORWARD NAME ADDR PROTO HELO
// (https://www.postfix.org/XFORWARD_README.html) capability.
// Should only be used if backend supports it.
EnableXFORWARD bool

// The priority profile mapping as defined
// in RFC 6710 section 10.2.
//
Expand Down
97 changes: 96 additions & 1 deletion server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type message struct {
RcptOpts []*smtp.RcptOptions
Data []byte
Opts *smtp.MailOptions
XForward map[string]string
}

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

func (s *session) Reset() {
s.msg = &message{}
s.msg = &message{
XForward: make(map[string]string),
}
}

func (s *session) Logout() error {
Expand Down Expand Up @@ -157,6 +160,11 @@ func (s *session) Data(r io.Reader) error {
return nil
}

func (s *session) XForward(attrName, attrValue string) error {
s.msg.XForward[attrName] = attrValue
return nil
}

func (s *session) LMTPData(r io.Reader, collector smtp.StatusCollector) error {
if err := s.Data(r); err != nil {
return err
Expand Down Expand Up @@ -1712,3 +1720,90 @@ func TestServerMTPRIORITY(t *testing.T) {
t.Fatal("Incorrect MtPriority parameter value:", fmt.Sprintf("expected %d, got %d", expectedPriority, *priority))
}
}

func TestServerXFORWARD(t *testing.T) {
be, s, c, scanner, caps := testServerEhlo(t,
func(s *smtp.Server) {
s.EnableXFORWARD = true
})
defer s.Close()
defer c.Close()

if _, ok := caps["XFORWARD NAME ADDR PROTO HELO"]; !ok {
t.Fatal("Missing capability: XFORWARD")
}

io.WriteString(c, "MAIL FROM:<[email protected]>\r\n")
scanner.Scan()
if !strings.HasPrefix(scanner.Text(), "250 ") {
t.Fatal("Invalid MAIL response:", scanner.Text())
}

io.WriteString(c, "RCPT TO:<[email protected]>\r\n")
scanner.Scan()
if !strings.HasPrefix(scanner.Text(), "250 ") {
t.Fatal("Invalid RCPT response:", scanner.Text())
}

// Atrribute names are case insensitive, check we normalised them
io.WriteString(c, "XFORWARD NAME=spike.porcupine.org ADDR=168.100.189.2 proto=ESMTP\r\n")
scanner.Scan()
if !strings.HasPrefix(scanner.Text(), "250 ") {
t.Fatal("Invalid XFORWARD response:", scanner.Text())
}

io.WriteString(c, "DATA\r\n")
scanner.Scan()
if !strings.HasPrefix(scanner.Text(), "354 ") {
t.Fatal("Invalid DATA response:", scanner.Text())
}

io.WriteString(c, "From: [email protected]\r\n")
io.WriteString(c, "\r\n")
io.WriteString(c, "Hey\r <3\r\n")
io.WriteString(c, "..this dot is fine\r\n")
io.WriteString(c, ".\r\n")
scanner.Scan()
if !strings.HasPrefix(scanner.Text(), "250 ") {
t.Fatal("Invalid DATA response:", scanner.Text())
}

if len(be.messages) != 0 || len(be.anonmsgs) != 1 {
t.Fatal("Invalid number of sent messages:", be.messages, be.anonmsgs)
}

msg := be.anonmsgs[0]

if len(msg.XForward) != 3 {
t.Fatal("Invalid XFORWARD data:", msg.XForward)
}

if got, ok := msg.XForward["NAME"]; !ok {
t.Fatal("Missing XFORWARD attribute NAME")
} else if got != "spike.porcupine.org" {
t.Fatal("Invalid XFORWARD attribute value for NAME", got)
}

if got, ok := msg.XForward["ADDR"]; !ok {
t.Fatal("Missing XFORWARD attribute ADDR")
} else if got != "168.100.189.2" {
t.Fatal("Invalid XFORWARD attribute value for ADDR", got)
}

if got, ok := msg.XForward["PROTO"]; !ok {
t.Fatal("Missing (normalised) XFORWARD attribute PROTO")
} else if got != "ESMTP" {
t.Fatal("Invalid (normalised) XFORWARD attribute value for PROTO", got)
}

// Sanity that the rest of the connection continued fine
if msg.From != "[email protected]" {
t.Fatal("Invalid mail sender:", msg.From)
}
if len(msg.To) != 1 || msg.To[0] != "[email protected]" {
t.Fatal("Invalid mail recipients:", msg.To)
}
if string(msg.Data) != "From: [email protected]\r\n\r\nHey\r <3\r\n.this dot is fine\r\n" {
t.Fatal("Invalid mail data:", string(msg.Data))
}
}