1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 17:47:03 +00:00

policy: Impl Addressing{} and Recipient{} for #84

This commit is contained in:
James Hillyerd
2018-03-17 11:15:17 -07:00
parent d132efd6fa
commit 469a778d81
4 changed files with 167 additions and 49 deletions

View File

@@ -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'

View File

@@ -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
}

View File

@@ -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

25
pkg/policy/recipient.go Normal file
View File

@@ -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)
}