This commit is contained in:
Mechiel Lukkien
2023-01-30 14:27:06 +01:00
commit cb229cb6cf
1256 changed files with 491723 additions and 0 deletions

316
smtp/address.go Normal file
View File

@ -0,0 +1,316 @@
package smtp
import (
"errors"
"fmt"
"strings"
"github.com/mjl-/mox/dns"
)
var ErrBadAddress = errors.New("invalid email address")
// Localpart is a decoded local part of an email address, before the "@".
// For quoted strings, values do not hold the double quote or escaping backslashes.
// An empty string can be a valid localpart.
type Localpart string
// String returns a packed representation of an address, with proper escaping/quoting, for use in SMTP.
func (lp Localpart) String() string {
// See ../rfc/5321:2322 ../rfc/6531:414
// First we try as dot-string. If not possible we make a quoted-string.
dotstr := true
t := strings.Split(string(lp), ".")
for _, e := range t {
for _, c := range e {
if c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c > 0x7f {
continue
}
switch c {
case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~':
continue
}
dotstr = false
break
}
dotstr = dotstr && len(e) > 0
}
dotstr = dotstr && len(t) > 0
if dotstr {
return string(lp)
}
// Make quoted-string.
r := `"`
for _, b := range lp {
if b == '"' || b == '\\' {
r += "\\" + string(b)
} else {
r += string(b)
}
}
r += `"`
return r
}
// DSNString returns the localpart as string for use in a DSN.
// utf8 indicates if the remote MTA supports utf8 messaging. If not, the 7bit DSN
// encoding for "utf-8-addr-xtext" from RFC 6533 is used.
func (lp Localpart) DSNString(utf8 bool) string {
if utf8 {
return lp.String()
}
// ../rfc/6533:259
r := ""
for _, c := range lp {
if c > 0x20 && c < 0x7f && c != '\\' && c != '+' && c != '=' {
r += string(c)
} else {
r += fmt.Sprintf(`\x{%x}`, c)
}
}
return r
}
// IsInternational returns if this is an internationalized local part, i.e. has
// non-ASCII characters.
func (lp Localpart) IsInternational() bool {
for _, c := range lp {
if c > 0x7f {
return true
}
}
return false
}
// Address is a parsed email address.
type Address struct {
Localpart Localpart
Domain dns.Domain // todo: shouldn't we accept an ip address here too? and merge this type into smtp.Path.
}
// NewAddress returns an address.
func NewAddress(localpart Localpart, domain dns.Domain) Address {
return Address{localpart, domain}
}
func (a Address) IsZero() bool {
return a == Address{}
}
// Pack returns the address in string form. If smtputf8 is true, the domain is
// formatted with non-ASCII characters. If localpart has non-ASCII characters,
// they are returned regardless of smtputf8.
func (a Address) Pack(smtputf8 bool) string {
return a.Localpart.String() + "@" + a.Domain.XName(smtputf8)
}
// String returns the address in string form with non-ASCII characters.
func (a Address) String() string {
return a.Localpart.String() + "@" + a.Domain.Name()
}
// ParseAddress parses an email address. UTF-8 is allowed.
// Returns ErrBadAddress for invalid addresses.
func ParseAddress(s string) (address Address, err error) {
lp, rem, err := parseLocalPart(s)
if err != nil {
return Address{}, fmt.Errorf("%w: %s", ErrBadAddress, err)
}
if !strings.HasPrefix(rem, "@") {
return Address{}, fmt.Errorf("%w: expected @", ErrBadAddress)
}
rem = rem[1:]
d, err := dns.ParseDomain(rem)
if err != nil {
return Address{}, fmt.Errorf("%w: %s", ErrBadAddress, err)
}
return Address{lp, d}, err
}
var ErrBadLocalpart = errors.New("invalid localpart")
// ParseLocalpart parses the local part.
// UTF-8 is allowed.
// Returns ErrBadAddress for invalid addresses.
func ParseLocalpart(s string) (localpart Localpart, err error) {
lp, rem, err := parseLocalPart(s)
if err != nil {
return "", err
}
if rem != "" {
return "", fmt.Errorf("%w: remaining after localpart: %q", ErrBadLocalpart, rem)
}
return lp, nil
}
func parseLocalPart(s string) (localpart Localpart, remain string, err error) {
p := &parser{s, 0}
defer func() {
x := recover()
if x == nil {
return
}
e, ok := x.(error)
if !ok {
panic(x)
}
err = fmt.Errorf("%w: %s", ErrBadLocalpart, e)
}()
lp := p.xlocalpart()
return lp, p.remainder(), nil
}
type parser struct {
s string
o int
}
func (p *parser) xerrorf(format string, args ...any) {
panic(fmt.Errorf(format, args...))
}
func (p *parser) hasPrefix(s string) bool {
return strings.HasPrefix(p.s[p.o:], s)
}
func (p *parser) take(s string) bool {
if p.hasPrefix(s) {
p.o += len(s)
return true
}
return false
}
func (p *parser) xtake(s string) {
if !p.take(s) {
p.xerrorf("expected %q", s)
}
}
func (p *parser) empty() bool {
return p.o == len(p.s)
}
func (p *parser) xtaken(n int) string {
r := p.s[p.o : p.o+n]
p.o += n
return r
}
func (p *parser) remainder() string {
r := p.s[p.o:]
p.o = len(p.s)
return r
}
// todo: reduce duplication between implementations: ../smtp/address.go:/xlocalpart ../dkim/parser.go:/xlocalpart ../smtpserver/parse.go:/xlocalpart
func (p *parser) xlocalpart() Localpart {
// ../rfc/5321:2316
var s string
if p.hasPrefix(`"`) {
s = p.xquotedString()
} else {
s = p.xatom()
for p.take(".") {
s += "." + p.xatom()
}
}
// todo: have a strict parser that only allows the actual max of 64 bytes. some services have large localparts because of generated (bounce) addresses.
if len(s) > 128 {
// ../rfc/5321:3486
p.xerrorf("localpart longer than 64 octets")
}
return Localpart(s)
}
func (p *parser) xquotedString() string {
p.xtake(`"`)
var s string
var esc bool
for {
c := p.xchar()
if esc {
if c >= ' ' && c < 0x7f {
s += string(c)
esc = false
continue
}
p.xerrorf("invalid localpart, bad escaped char %c", c)
}
if c == '\\' {
esc = true
continue
}
if c == '"' {
return s
}
// todo: should we be accepting utf8 for quoted strings?
if c >= ' ' && c < 0x7f && c != '\\' && c != '"' || c > 0x7f {
s += string(c)
continue
}
p.xerrorf("invalid localpart, invalid character %c", c)
}
}
func (p *parser) xchar() rune {
// We are careful to track invalid utf-8 properly.
if p.empty() {
p.xerrorf("need another character")
}
var r rune
var o int
for i, c := range p.s[p.o:] {
if i > 0 {
o = i
break
}
r = c
}
if o == 0 {
p.o = len(p.s)
} else {
p.o += o
}
return r
}
func (p *parser) takefn1(what string, fn func(c rune, i int) bool) string {
if p.empty() {
p.xerrorf("need at least one char for %s", what)
}
for i, c := range p.s[p.o:] {
if !fn(c, i) {
if i == 0 {
p.xerrorf("expected at least one char for %s, got char %c", what, c)
}
return p.xtaken(i)
}
}
return p.remainder()
}
func (p *parser) xatom() string {
return p.takefn1("atom", func(c rune, i int) bool {
switch c {
case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~':
return true
}
return isalphadigit(c) || c > 0x7f
})
}
func isalpha(c rune) bool {
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
}
func isdigit(c rune) bool {
return c >= '0' && c <= '9'
}
func isalphadigit(c rune) bool {
return isalpha(c) || isdigit(c)
}

92
smtp/address_test.go Normal file
View File

@ -0,0 +1,92 @@
package smtp
import (
"errors"
"testing"
)
func TestParseLocalpart(t *testing.T) {
good := func(s string) {
t.Helper()
_, err := ParseLocalpart(s)
if err != nil {
t.Fatalf("unexpected error for localpart %q: %v", s, err)
}
}
bad := func(s string) {
t.Helper()
_, err := ParseLocalpart(s)
if err == nil {
t.Fatalf("did not see expected error for localpart %q", s)
}
if !errors.Is(err, ErrBadLocalpart) {
t.Fatalf("expected ErrBadLocalpart, got %v", err)
}
}
good("user")
good("a")
good("a.b.c")
good(`""`)
good(`"ok"`)
good(`"a.bc"`)
bad("")
bad(`"`) // missing ending dquot
bad("\x00") // control not allowed
bad("\"\\") // ending with backslash
bad("\"\x01") // control not allowed in dquote
bad(`""leftover`) // leftover data after close dquote
}
func TestParseAddress(t *testing.T) {
good := func(s string) {
t.Helper()
_, err := ParseAddress(s)
if err != nil {
t.Fatalf("unexpected error for localpart %q: %v", s, err)
}
}
bad := func(s string) {
t.Helper()
_, err := ParseAddress(s)
if err == nil {
t.Fatalf("did not see expected error for localpart %q", s)
}
if !errors.Is(err, ErrBadAddress) {
t.Fatalf("expected ErrBadAddress, got %v", err)
}
}
good("user@example.com")
bad("user@@example.com")
bad("user") // missing @domain
bad("@example.com") // missing localpart
bad(`"@example.com`) // missing ending dquot or domain
bad("\x00@example.com") // control not allowed
bad("\"\\@example.com") // missing @domain
bad("\"\x01@example.com") // control not allowed in dquote
bad(`""leftover@example.com`) // leftover data after close dquot
}
func TestPackLocalpart(t *testing.T) {
var l = []struct {
input, expect string
}{
{``, `""`}, // No atom.
{`a.`, `"a."`}, // Empty atom not allowed.
{`a.b`, `a.b`}, // Fine.
{"azAZ09!#$%&'*+-/=?^_`{|}~", "azAZ09!#$%&'*+-/=?^_`{|}~"}, // All ascii that are fine as atom.
{` `, `" "`},
{"\x01", "\"\x01\""}, // todo: should probably return an error for control characters.
{"<>", `"<>"`},
}
for _, e := range l {
r := Localpart(e.input).String()
if r != e.expect {
t.Fatalf("PackLocalpart for %q, expect %q, got %q", e.input, e.expect, r)
}
}
}

16
smtp/addrlit.go Normal file
View File

@ -0,0 +1,16 @@
package smtp
import (
"net"
)
// AddressLiteral returns an IPv4 or IPv6 address literal for use in SMTP.
func AddressLiteral(ip net.IP) string {
// ../rfc/5321:2309
s := "["
if ip.To4() == nil {
s += "IPv6:"
}
s += ip.String() + "]"
return s
}

145
smtp/codes.go Normal file
View File

@ -0,0 +1,145 @@
package smtp
// ../rfc/5321:2863
// Reply codes.
var (
C211SystemStatus = 211
C214Help = 214
C220ServiceReady = 220
C221Closing = 221
C235AuthSuccess = 235 // ../rfc/4954:573
C250Completed = 250
C251UserNotLocalWillForward = 251
C252WithoutVrfy = 252
C334ContinueAuth = 334 // ../rfc/4954:187
C354Continue = 354
C421ServiceUnavail = 421
C432PasswdTransitionNeeded = 432 // ../rfc/4954:578
C454TempAuthFail = 454 // ../rfc/4954:586
C450MailboxUnavail = 450
C451LocalErr = 451
C452StorageFull = 452 // Also for "too many recipients", ../rfc/5321:3576
C455BadParams = 455
C500BadSyntax = 500
C501BadParamSyntax = 501
C502CmdNotImpl = 502
C503BadCmdSeq = 503
C504ParamNotImpl = 504
C521HostNoMail = 521 // ../rfc/7504:179
C530SecurityRequired = 530 // ../rfc/3207:148 ../rfc/4954:623
C538EncReqForAuth = 538 // ../rfc/4954:630
C534AuthMechWeak = 534 // ../rfc/4954:593
C535AuthBadCreds = 535 // ../rfc/4954:600
C550MailboxUnavail = 550
C551UserNotLocal = 551
C552MailboxFull = 552
C553BadMailbox = 553
C554TransactionFailed = 554
C555UnrecognizedAddrParams = 555
C556DomainNoMail = 556 // ../rfc/7504:207
)
// Short enhanced reply codes, without leading number and first dot.
//
// See https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml
var (
// 0.x - Other or Undefined Status.
// ../rfc/3463:287
SeOther00 = "0.0"
// 1.x - Address.
// ../rfc/3463:295
SeAddr1Other0 = "1.0"
SeAddr1UnknownDestMailbox1 = "1.1"
SeAddr1UnknownSystem2 = "1.2"
SeAddr1MailboxSyntax3 = "1.3"
SeAddr1MailboxAmbiguous4 = "1.4"
SeAddr1DestValid5 = "1.5" // For success responses.
SeAddr1DestMailboxMoved6 = "1.6"
SeAddr1SenderSyntax7 = "1.7"
SeAddr1BadSenderSystemAddress8 = "1.8"
SeAddr1NullMX = "1.10" // ../rfc/7505:237
// 2.x - Mailbox.
// ../rfc/3463:361
SeMailbox2Other0 = "2.0"
SeMailbox2Disabled1 = "2.1"
SeMailbox2Full2 = "2.2"
SeMailbox2MsgLimitExceeded3 = "2.3"
SeMailbox2MailListExpansion4 = "2.4"
// 3.x - Mail system.
// ../rfc/3463:405
SeSys3Other0 = "3.0"
SeSys3StorageFull1 = "3.1"
SeSys3NotAccepting2 = "3.2"
SeSys3NotSupported3 = "3.3"
SeSys3MsgLimitExceeded4 = "3.4"
SeSys3Misconfigured5 = "3.5"
// 4.x - Network and routing.
// ../rfc/3463:455
SeNet4Other0 = "4.0"
SeNet4NoAnswer1 = "4.1"
SeNet4BadConn2 = "4.2"
SeNet4Name3 = "4.3"
SeNet4Routing4 = "4.4"
SeNet4Congestion5 = "4.5"
SeNet4Loop6 = "4.6"
SeNet4DeliveryExpired7 = "4.7"
// 5.x - Mail delivery protocol.
// ../rfc/3463:527
SeProto5Other0 = "5.0"
SeProto5BadCmdOrSeq1 = "5.1"
SeProto5Syntax2 = "5.2"
SeProto5TooManyRcpts3 = "5.3"
SeProto5BadParams4 = "5.4"
SeProto5ProtocolMismatch5 = "5.5"
SeProto5AuthExchangeTooLong = "5.6" // ../rfc/4954:650
// 6.x - Message content/media.
// ../rfc/3463:579
SeMsg6Other0 = "6.0"
SeMsg6MediaUnsupported1 = "6.1"
SeMsg6ConversionProhibited2 = "6.2"
SeMsg6ConversoinUnsupported3 = "6.3"
SeMsg6ConversionWithLoss4 = "6.4"
SeMsg6ConversionFailed5 = "6.5"
SeMsg6NonASCIIAddrNotPermitted7 = "6.7" // ../rfc/6531:735
SeMsg6UTF8ReplyRequired8 = "6.8" // ../rfc/6531:746
SeMsg6UTF8CannotTransfer9 = "6.9" // ../rfc/6531:758
// 7.x - Security/policy.
// ../rfc/3463:628
SePol7Other0 = "7.0"
SePol7DeliveryUnauth1 = "7.1"
SePol7ExpnProhibited2 = "7.2"
SePol7ConversionImpossible3 = "7.3"
SePol7Unsupported4 = "7.4"
SePol7CryptoFailure5 = "7.5"
SePol7CryptoUnsupported6 = "7.6"
SePol7MsgIntegrity7 = "7.7"
SePol7AuthBadCreds8 = "7.8" // ../rfc/4954:600
SePol7AuthWeakMech9 = "7.9" // ../rfc/4954:593
SePol7EncNeeded10 = "7.10" // ../rfc/5248:359
SePol7EncReqForAuth11 = "7.11" // ../rfc/4954:630
SePol7PasswdTransitionReq12 = "7.12" // ../rfc/4954:578
SePol7AccountDisabled13 = "7.13" // ../rfc/5248:399
SePol7TrustReq14 = "7.14" // ../rfc/5248:418
SePol7NoDKIMPass20 = "7.20" // ../rfc/7372:137
SePol7NoDKIMAccept21 = "7.21" // ../rfc/7372:148
SePol7NoDKIMAuthorMatch22 = "7.22" // ../rfc/7372:175
SePol7SPFResultFail23 = "7.23" // ../rfc/7372:192
SePol7SPFError24 = "7.24" // ../rfc/7372:204
SePol7RevDNSFail25 = "7.25" // ../rfc/7372:233
SePol7MultiAuthFails26 = "7.26" // ../rfc/7372:246
SePol7SenderHasNullMX27 = "7.27" // ../rfc/7505:246
SePol7ARCFail = "7.29" // ../rfc/8617:1438
SePol7MissingReqTLS = "7.30" // ../rfc/8689:448
)

138
smtp/data.go Normal file
View File

@ -0,0 +1,138 @@
package smtp
import (
"bufio"
"bytes"
"errors"
"io"
)
var errMissingCRLF = errors.New("missing crlf at end of message")
// DataWrite reads data (a mail message) from r, and writes it to smtp
// connection w with dot stuffing, as required by the SMTP data command.
func DataWrite(w io.Writer, r io.Reader) error {
// ../rfc/5321:2003
var prevlast, last byte = '\r', '\n' // Start on a new line, so we insert a dot if the first byte is a dot.
// todo: at least for smtp submission we should probably set a max line length, eg 1000 octects including crlf. ../rfc/5321:3512
// todo: at least for smtp submission or a pedantic mode, we should refuse messages with bare \r or bare \n.
buf := make([]byte, 8*1024)
for {
nr, err := r.Read(buf)
if nr > 0 {
// Process buf by writing a line at a time, and checking if the next character
// after the line starts with a dot. Insert an extra dot if so.
p := buf[:nr]
for len(p) > 0 {
if p[0] == '.' && prevlast == '\r' && last == '\n' {
if _, err := w.Write([]byte{'.'}); err != nil {
return err
}
}
// Look for the next newline, or end of buffer.
n := 0
for n < len(p) {
c := p[n]
n++
if c == '\n' {
break
}
}
if _, err := w.Write(p[:n]); err != nil {
return err
}
// Keep track of the last two bytes we've written.
if n == 1 {
prevlast, last = last, p[0]
} else {
prevlast, last = p[n-2], p[n-1]
}
p = p[n:]
}
}
if err == io.EOF {
break
} else if err != nil {
return err
}
}
if prevlast != '\r' || last != '\n' {
return errMissingCRLF
}
if _, err := w.Write(dotcrlf); err != nil {
return err
}
return nil
}
var dotcrlf = []byte(".\r\n")
// DataReader is an io.Reader that reads data from an SMTP DATA command, doing dot
// unstuffing and returning io.EOF when a bare dot is received. Use NewDataReader.
type DataReader struct {
// ../rfc/5321:2003
r *bufio.Reader
plast, last byte
buf []byte // From previous read.
err error // Read error, for after r.buf is exhausted.
}
// NewDataReader returns an initialized DataReader.
func NewDataReader(r *bufio.Reader) *DataReader {
return &DataReader{
r: r,
// Set up initial state to accept a message that is only "." and CRLF.
plast: '\r',
last: '\n',
}
}
// Read implements io.Reader.
func (r *DataReader) Read(p []byte) (int, error) {
wrote := 0
for len(p) > 0 {
// Read until newline as long as it fits in the buffer.
if len(r.buf) == 0 {
if r.err != nil {
break
}
// todo: set a max length, eg 1000 octets including crlf excluding potential leading dot. ../rfc/5321:3512
r.buf, r.err = r.r.ReadSlice('\n')
if r.err == bufio.ErrBufferFull {
r.err = nil
} else if r.err == io.EOF {
// Mark EOF as bad for now. If we see the ending dotcrlf below, err becomes regular
// io.EOF again.
r.err = io.ErrUnexpectedEOF
}
}
if len(r.buf) > 0 {
// We require crlf. A bare LF is not a line ending. ../rfc/5321:2032
// todo: we could return an error for a bare \n.
if r.plast == '\r' && r.last == '\n' {
if bytes.Equal(r.buf, dotcrlf) {
r.buf = nil
r.err = io.EOF
break
} else if r.buf[0] == '.' {
r.buf = r.buf[1:]
}
}
n := len(r.buf)
if n > len(p) {
n = len(p)
}
copy(p, r.buf[:n])
if n == 1 {
r.plast, r.last = r.last, r.buf[0]
} else if n > 1 {
r.plast, r.last = r.buf[n-2], r.buf[n-1]
}
p = p[n:]
r.buf = r.buf[n:]
wrote += n
}
}
return wrote, r.err
}

91
smtp/data_test.go Normal file
View File

@ -0,0 +1,91 @@
package smtp
import (
"bufio"
"errors"
"io"
"strings"
"testing"
)
func TestDataWrite(t *testing.T) {
if err := DataWrite(io.Discard, strings.NewReader("bad")); err == nil || !errors.Is(err, errMissingCRLF) {
t.Fatalf("got err %v, expected errMissingCRLF", err)
}
if err := DataWrite(io.Discard, strings.NewReader(".")); err == nil || !errors.Is(err, errMissingCRLF) {
t.Fatalf("got err %v, expected errMissingCRLF", err)
}
check := func(msg, want string) {
t.Helper()
w := &strings.Builder{}
if err := DataWrite(w, strings.NewReader(msg)); err != nil {
t.Fatalf("writing smtp data: %s", err)
}
got := w.String()
if got != want {
t.Fatalf("got %q, expected %q, for msg %q", got, want, msg)
}
}
check("", ".\r\n")
check(".\r\n", "..\r\n.\r\n")
check("header: abc\r\n\r\nmessage\r\n", "header: abc\r\n\r\nmessage\r\n.\r\n")
}
func TestDataReader(t *testing.T) {
// Copy with a 1 byte buffer for reading.
smallCopy := func(d io.Writer, r io.Reader) (int, error) {
var wrote int
buf := make([]byte, 1)
for {
n, err := r.Read(buf)
if n > 0 {
nn, err := d.Write(buf)
if nn > 0 {
wrote += nn
}
if err != nil {
return wrote, err
}
}
if err == io.EOF {
break
} else if err != nil {
return wrote, err
}
}
return wrote, nil
}
check := func(data, want string) {
t.Helper()
s := &strings.Builder{}
dr := NewDataReader(bufio.NewReader(strings.NewReader(data)))
if _, err := io.Copy(s, dr); err != nil {
t.Fatalf("got err %v", err)
} else if got := s.String(); got != want {
t.Fatalf("got %q, expected %q, for %q", got, want, data)
}
s = &strings.Builder{}
dr = NewDataReader(bufio.NewReader(strings.NewReader(data)))
if _, err := smallCopy(s, dr); err != nil {
t.Fatalf("got err %v", err)
} else if got := s.String(); got != want {
t.Fatalf("got %q, expected %q, for %q", got, want, data)
}
}
check("test\r\n.\r\n", "test\r\n")
check(".\r\n", "")
check(".test\r\n.\r\n", "test\r\n") // Unnecessary dot, but valid in SMTP.
check("..test\r\n.\r\n", ".test\r\n")
s := &strings.Builder{}
dr := NewDataReader(bufio.NewReader(strings.NewReader("no end")))
if _, err := io.Copy(s, dr); err != io.ErrUnexpectedEOF {
t.Fatalf("got err %v, expected io.ErrUnexpectedEOF", err)
}
}

2
smtp/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package smtp provides SMTP definitions and functions shared between smtpserver and smtpclient.
package smtp

17
smtp/ehlo.go Normal file
View File

@ -0,0 +1,17 @@
package smtp
import (
"net"
"github.com/mjl-/mox/dns"
)
// Ehlo is the remote identification of an incoming SMTP connection.
type Ehlo struct {
Name dns.IPDomain // Name from EHLO/HELO line. Can be an IP or host name.
ConnIP net.IP // Address of connection.
}
func (e Ehlo) IsZero() bool {
return e.Name.IsZero() && e.ConnIP == nil
}

67
smtp/path.go Normal file
View File

@ -0,0 +1,67 @@
package smtp
import (
"strings"
"github.com/mjl-/mox/dns"
)
// Path is an SMTP forward/reverse path, as used in MAIL FROM and RCPT TO
// commands.
type Path struct {
Localpart Localpart
IPDomain dns.IPDomain
}
func (p Path) IsZero() bool {
return p.Localpart == "" && p.IPDomain.IsZero()
}
// String returns a string representation with ASCII-only domain name.
func (p Path) String() string {
return p.XString(false)
}
// XString is like String, but returns unicode UTF-8 domain names if utf8 is
// true.
func (p Path) XString(utf8 bool) string {
if p.Localpart == "" && p.IPDomain.IsZero() {
return ""
}
return p.Localpart.String() + "@" + p.IPDomain.XString(utf8)
}
// ASCIIExtra returns an ascii-only path if utf8 is true and the ipdomain is a
// unicode domain. Otherwise returns an empty string.
//
// For use in comments in message headers added during SMTP.
func (p Path) ASCIIExtra(utf8 bool) string {
if utf8 && p.IPDomain.Domain.Unicode != "" {
return p.XString(false)
}
return ""
}
// DSNString returns a string representation as used with DSN with/without
// UTF-8 support.
//
// If utf8 is false, the domain is represented as US-ASCII (IDNA), and the
// localpart is encoded with in 7bit according to RFC 6533.
func (p Path) DSNString(utf8 bool) string {
if utf8 {
return p.XString(utf8)
}
return p.Localpart.DSNString(utf8) + "@" + p.IPDomain.XString(utf8)
}
func (p Path) Equal(o Path) bool {
if p.Localpart != o.Localpart {
return false
}
d0 := p.IPDomain
d1 := o.IPDomain
if len(d0.IP) > 0 || len(d1.IP) > 0 {
return d0.IP.Equal(d1.IP)
}
return strings.EqualFold(d0.Domain.ASCII, d1.Domain.ASCII)
}