Skip to content

Commit

Permalink
issue #199 - IP addresses are normalized, Address.String() returns an…
Browse files Browse the repository at this point in the history
… empty string of address is empty, or just "postmaster" it it's a postmaster address
  • Loading branch information
flashmob committed Dec 8, 2019
1 parent a32e269 commit c08de3d
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 58 deletions.
1 change: 1 addition & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ func (c *client) parsePath(in []byte, p pathParser) (mail.Address, error) {
ADL: c.parser.ADL,
PathParams: c.parser.PathParams,
NullPath: c.parser.NullPath,
Quoted: c.parser.LocalPartQuoted,
}
}
return address, err
Expand Down
75 changes: 64 additions & 11 deletions mail/envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"mime"
"net"
"net/mail"
"net/textproto"
"strings"
Expand Down Expand Up @@ -41,10 +42,36 @@ type Address struct {
PathParams [][]string
// NullPath is true if <> was received
NullPath bool
// Quoted indicates if the local-part needs quotes
Quoted bool
// IP stores the IP Address, if the Host is an IP
IP net.IP
}

func (ep *Address) String() string {
return fmt.Sprintf("%s@%s", ep.User, ep.Host)
var local string
if ep.IsEmpty() {
return ""
}
if ep.Quoted {
var sb bytes.Buffer
sb.WriteByte('"')
for i := 0; i < len(ep.User); i++ {
if ep.User[i] == '\\' || ep.User[i] == '"' {
// escape
sb.WriteByte('\\')
}
sb.WriteByte(ep.User[i])
}
sb.WriteByte('"')
local = sb.String()
} else {
local = ep.User
}
if ep.Host != "" {
return fmt.Sprintf("%s@%s", local, ep.Host)
}
return local
}

func (ep *Address) IsEmpty() bool {
Expand All @@ -55,20 +82,46 @@ var ap = mail.AddressParser{}

// NewAddress takes a string of an RFC 5322 address of the
// form "Gogh Fir <[email protected]>" or "[email protected]".
func NewAddress(str string) (Address, error) {
a, err := ap.Parse(str)
// TODO its not parsing ip addresses properly
func NewAddress(str string) (*Address, error) {
var isQuoted, isIP bool
var pos int
var a *mail.Address
var err error
address := new(Address)
if pos = strings.LastIndex(str, "@"); pos > 0 && str[pos-1] == '"' {
isQuoted = true
}
if pos > 0 && pos+1 < len(str) && str[pos+1] == '[' {
isIP = true
}

a, err = ap.Parse(str)

if err != nil {
return Address{}, err
return nil, err
}
pos := strings.Index(a.Address, "@")
pos = strings.LastIndex(a.Address, "@")

if pos > 0 {
return Address{
User: a.Address[0:pos],
Host: a.Address[pos+1:],
},
nil
address.User = a.Address[0:pos]
address.Host = a.Address[pos+1:]
if isQuoted {
address.Quoted = true
}
if isIP {
// check if the ip address is valid
if v := net.ParseIP(address.Host); v == nil {
return nil, errors.New("invalid ip")
} else {
address.IP = v
// this will normalize ipv6 addresses
address.Host = v.String()
}
}
return address, nil
}
return Address{}, errors.New("invalid address")
return nil, errors.New("invalid address")
}

// Envelope of Email represents a single SMTP message.
Expand Down
30 changes: 29 additions & 1 deletion mail/envelope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestMimeHeaderDecode(t *testing.T) {
*/

str := MimeHeaderDecode("=?utf-8?B?55So5oi34oCcRXBpZGVtaW9sb2d5IGluIG51cnNpbmcgYW5kIGg=?= =?utf-8?B?ZWFsdGggY2FyZSBlQm9vayByZWFkL2F1ZGlvIGlkOm8=?= =?utf-8?B?cTNqZWVr4oCd5Zyo572R56uZ4oCcU1BZ5Lit5paH5a6Y5pa5572R56uZ4oCd?= =?utf-8?B?55qE5biQ5Y+36K+m5oOF?=")
if i := strings.Index(str, "用户“Epidemiology in nursing and h ealth care eBook read/audio id:o q3jeek”在网站“SPY中文官方网站” 的帐号详情"); i != 0 {
if i := strings.Index(str, "用户“Epidemiology in nursing and health care eBook read/audio id:oq3jeek”在网站“SPY中文官方网站” 的帐号详情"); i != 0 {
t.Error("expecting 用户“Epidemiology in nursing and h ealth care eBook read/audio id:o q3jeek”在网站“SPY中文官方网站” 的帐号详情, got:", str)
}
str = MimeHeaderDecode("=?ISO-8859-1?Q?Andr=E9?= Pirard <[email protected]>")
Expand All @@ -39,6 +39,34 @@ func TestNewAddress(t *testing.T) {
t.Error("there should be no error:", addr.Host, err)
}
}

func TestQuotedAddress(t *testing.T) {

str := `<" yo-- man wazz'''up? surprise \surprise, this is [email protected] "@example.com>`
//str = `<"post\master">`
addr, err := NewAddress(str)
if err != nil {
t.Error("there should be no error:", err)
}

str = addr.String()
// in this case, string should remove the unnecessary escape
if strings.Contains(str, "\\surprise") {
t.Error("there should be no \\surprise:", err)
}

}

func TestAddressWithIP(t *testing.T) {
str := `<" yo-- man wazz'''up? surprise \surprise, this is [email protected] "@[64.233.160.71]>`
addr, err := NewAddress(str)
if err != nil {
t.Error("there should be no error:", err)
} else if addr.IP == nil {
t.Error("expecting the address host to be an IP")
}
}

func TestEnvelope(t *testing.T) {
e := NewEnvelope("127.0.0.1", 22)

Expand Down
57 changes: 36 additions & 21 deletions mail/rfc5321/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"net"
"strconv"
"strings"
)

const (
Expand All @@ -21,17 +22,21 @@ const (
LimitRecipients = 100
)

var atExpected = errors.New("@ expected as part of mailbox")

// Parse Email Addresses according to https://tools.ietf.org/html/rfc5321
type Parser struct {
accept bytes.Buffer
buf []byte
PathParams [][]string
ADL []string
LocalPart string
Domain string
pos int
NullPath bool
ch byte
accept bytes.Buffer
buf []byte
PathParams [][]string
ADL []string
LocalPart string
LocalPartQuoted bool // is the local part quoted?
Domain string // can be an ip-address, enclosed in square brackets if it is
IP net.IP
pos int
NullPath bool
ch byte
}

func NewParser(buf []byte) *Parser {
Expand All @@ -51,6 +56,8 @@ func (s *Parser) Reset() {
s.LocalPart = ""
s.Domain = ""
s.accept.Reset()
s.LocalPartQuoted = false
s.IP = nil
}
}

Expand Down Expand Up @@ -93,14 +100,15 @@ func (s *Parser) forwardPath() (err error) {
if s.peek() == ' ' {
s.next() // tolerate a space at the front
}
if i := bytes.Index(bytes.ToLower(s.buf[s.pos+1:]), []byte(postmasterPath)); i == 0 {
s.LocalPart = postmasterLocalPart
return nil
}
if err = s.path(); err != nil {
if err = s.path(); err != nil && err != atExpected {
return err
}
return nil
// special case for forwardPath only - can just be addressed to postmaster
if i := strings.Index(strings.ToLower(s.LocalPart), postmasterLocalPart); i == 0 {
s.LocalPart = postmasterLocalPart
return nil // atExpected will be ignored, postmaster doesn't need @
}
return err // it may return atExpected
}

//MailFrom accepts the following syntax: Reverse-path [SP Mail-parameters] CRLF
Expand All @@ -123,8 +131,7 @@ func (s *Parser) MailFrom(input []byte) (err error) {
return nil
}

const postmasterPath = "<postmaster>"
const postmasterLocalPart = "Postmaster"
const postmasterLocalPart = "postmaster"

//RcptTo accepts the following syntax: ( "<Postmaster@" Domain ">" / "<Postmaster>" /
// Forward-path ) [SP Rcpt-parameters] CRLF
Expand Down Expand Up @@ -337,7 +344,7 @@ func (s *Parser) mailbox() error {
return err
}
if s.ch != '@' {
return errors.New("@ expected as part of mailbox")
return atExpected
}
if p := s.peek(); p == '[' {
return s.addressLiteral()
Expand Down Expand Up @@ -384,6 +391,11 @@ func (s *Parser) ipv4AddressLiteral() error {
}
s.accept.WriteByte(s.ch)
}
ip := net.ParseIP(s.accept.String())
if ip == nil {
return errors.New("invalid ip")
}
s.IP = ip
return nil
}

Expand Down Expand Up @@ -433,7 +445,8 @@ func (s *Parser) ipv6AddressLiteral() error {
c != ':' && c != '.' {
ipstr := ip.String()
if v := net.ParseIP(ipstr); v != nil {
s.accept.WriteString(ipstr)
s.accept.WriteString(v.String())
s.IP = v
return nil
}
return errors.New("invalid ipv6")
Expand All @@ -453,6 +466,7 @@ func (s *Parser) localPart() error {
}()
p := s.peek()
if p == '"' {
s.LocalPartQuoted = true
return s.quotedString()
} else {
return s.dotString()
Expand Down Expand Up @@ -486,7 +500,7 @@ func (s *Parser) QcontentSMTP() error {
case 0:
if ch == '\\' {
state = 1
s.accept.WriteByte(ch)
// s.accept.WriteByte(ch)
continue
} else if ch == 32 || ch == 33 ||
(ch >= 35 && ch <= 91) ||
Expand Down Expand Up @@ -567,7 +581,8 @@ atext = ALPHA / DIGIT / ; Any character except controls,

func (s *Parser) isAtext(c byte) bool {
if ('0' <= c && c <= '9') ||
('A' <= c && c <= 'z') ||
('a' <= c && c <= 'z') ||
('A' <= c && c <= 'Z') ||
c == '!' || c == '#' ||
c == '$' || c == '%' ||
c == '&' || c == '\'' ||
Expand Down
Loading

0 comments on commit c08de3d

Please sign in to comment.