From 469a778d81923cc221cc6dc3fd4af65fe97ba978 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Sat, 17 Mar 2018 11:15:17 -0700 Subject: [PATCH] policy: Impl Addressing{} and Recipient{} for #84 --- Makefile | 2 +- pkg/policy/address.go | 136 ++++++++++++++++++++++++------------- pkg/policy/address_test.go | 53 +++++++++++++++ pkg/policy/recipient.go | 25 +++++++ 4 files changed, 167 insertions(+), 49 deletions(-) create mode 100644 pkg/policy/recipient.go diff --git a/Makefile b/Makefile index eacf4ba..ff275c0 100644 --- a/Makefile +++ b/Makefile @@ -36,4 +36,4 @@ lint: @go vet $(PKGS) reflex: - reflex -r '\.go$$' -- sh -c 'echo; date; echo; go test ./...' + reflex -r '\.go$$' -- sh -c 'echo; date; echo; go test ./... && echo ALL PASS' diff --git a/pkg/policy/address.go b/pkg/policy/address.go index 7cc85f0..34ed8f8 100644 --- a/pkg/policy/address.go +++ b/pkg/policy/address.go @@ -3,9 +3,48 @@ package policy import ( "bytes" "fmt" + "net/mail" "strings" + + "github.com/jhillyerd/inbucket/pkg/config" ) +// Addressing handles email address policy. +type Addressing struct { + Config config.SMTPConfig +} + +// NewRecipient parses an address into a Recipient. +func (a *Addressing) NewRecipient(address string) (*Recipient, error) { + local, domain, err := ParseEmailAddress(address) + if err != nil { + return nil, err + } + mailbox, err := ParseMailboxName(local) + if err != nil { + return nil, err + } + ar, err := mail.ParseAddress(address) + if err != nil { + return nil, err + } + return &Recipient{ + Address: *ar, + apolicy: a, + LocalPart: local, + Domain: domain, + Mailbox: mailbox, + }, nil +} + +// ShouldStoreDomain indicates if Inbucket stores email destined for the specified domain. +func (a *Addressing) ShouldStoreDomain(domain string) bool { + if a.Config.StoreMessages { + return strings.ToLower(domain) != strings.ToLower(a.Config.DomainNoStore) + } + return false +} + // ParseMailboxName takes a localPart string (ex: "user+ext" without "@domain") // and returns just the mailbox name (ex: "user"). Returns an error if // localPart contains invalid characters; it won't accept any that must be @@ -35,54 +74,6 @@ func ParseMailboxName(localPart string) (result string, err error) { return result, nil } -// ValidateDomainPart returns true if the domain part complies to RFC3696, RFC1035 -func ValidateDomainPart(domain string) bool { - if len(domain) == 0 { - return false - } - if len(domain) > 255 { - return false - } - if domain[len(domain)-1] != '.' { - domain += "." - } - prev := '.' - labelLen := 0 - hasAlphaNum := false - for _, c := range domain { - switch { - case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || - ('0' <= c && c <= '9') || c == '_': - // Must contain some of these to be a valid label. - hasAlphaNum = true - labelLen++ - case c == '-': - if prev == '.' { - // Cannot lead with hyphen. - return false - } - case c == '.': - if prev == '.' || prev == '-' { - // Cannot end with hyphen or double-dot. - return false - } - if labelLen > 63 { - return false - } - if !hasAlphaNum { - return false - } - labelLen = 0 - hasAlphaNum = false - default: - // Unknown character. - return false - } - prev = c - } - return true -} - // ParseEmailAddress unescapes an email address, and splits the local part from the domain part. // An error is returned if the local or domain parts fail validation following the guidelines // in RFC3696. @@ -202,3 +193,52 @@ LOOP: } return buf.String(), domain, nil } + +// ValidateDomainPart returns true if the domain part complies to RFC3696, RFC1035. Used by +// ParseEmailAddress(). +func ValidateDomainPart(domain string) bool { + if len(domain) == 0 { + return false + } + if len(domain) > 255 { + return false + } + if domain[len(domain)-1] != '.' { + domain += "." + } + prev := '.' + labelLen := 0 + hasAlphaNum := false + for _, c := range domain { + switch { + case ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || + ('0' <= c && c <= '9') || c == '_': + // Must contain some of these to be a valid label. + hasAlphaNum = true + labelLen++ + case c == '-': + if prev == '.' { + // Cannot lead with hyphen. + return false + } + case c == '.': + if prev == '.' || prev == '-' { + // Cannot end with hyphen or double-dot. + return false + } + if labelLen > 63 { + return false + } + if !hasAlphaNum { + return false + } + labelLen = 0 + hasAlphaNum = false + default: + // Unknown character. + return false + } + prev = c + } + return true +} diff --git a/pkg/policy/address_test.go b/pkg/policy/address_test.go index 149e9b3..2ab2c60 100644 --- a/pkg/policy/address_test.go +++ b/pkg/policy/address_test.go @@ -4,9 +4,62 @@ import ( "strings" "testing" + "github.com/jhillyerd/inbucket/pkg/config" "github.com/jhillyerd/inbucket/pkg/policy" ) +func TestShouldStoreDomain(t *testing.T) { + // Test with storage enabled. + ap := &policy.Addressing{ + Config: config.SMTPConfig{ + DomainNoStore: "Foo.Com", + StoreMessages: true, + }, + } + testCases := []struct { + domain string + want bool + }{ + {domain: "bar.com", want: true}, + {domain: "foo.com", want: false}, + {domain: "FOO.com", want: false}, + {domain: "bar.foo.com", want: true}, + } + for _, tc := range testCases { + t.Run(tc.domain, func(t *testing.T) { + got := ap.ShouldStoreDomain(tc.domain) + if got != tc.want { + t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want) + } + + }) + } + // Test with storage disabled. + ap = &policy.Addressing{ + Config: config.SMTPConfig{ + StoreMessages: false, + }, + } + testCases = []struct { + domain string + want bool + }{ + {domain: "bar.com", want: false}, + {domain: "foo.com", want: false}, + {domain: "FOO.com", want: false}, + {domain: "bar.foo.com", want: false}, + } + for _, tc := range testCases { + t.Run(tc.domain, func(t *testing.T) { + got := ap.ShouldStoreDomain(tc.domain) + if got != tc.want { + t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want) + } + + }) + } +} + func TestParseMailboxName(t *testing.T) { var validTable = []struct { input string diff --git a/pkg/policy/recipient.go b/pkg/policy/recipient.go new file mode 100644 index 0000000..36fd94b --- /dev/null +++ b/pkg/policy/recipient.go @@ -0,0 +1,25 @@ +package policy + +import "net/mail" + +// Recipient represents a potential email recipient, allows policies for it to be queried. +type Recipient struct { + mail.Address + apolicy *Addressing + // LocalPart is the part of the address before @, including +extension. + LocalPart string + // Domain is the part of the address after @. + Domain string + // Mailbox is the canonical mailbox name for this recipient. + Mailbox string +} + +// ShouldAccept returns true if Inbucket should accept mail for this recipient. +func (r *Recipient) ShouldAccept() bool { + return true +} + +// ShouldStore returns true if Inbucket should store mail for this recipient. +func (r *Recipient) ShouldStore() bool { + return r.apolicy.ShouldStoreDomain(r.Domain) +}