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:
2
Makefile
2
Makefile
@@ -36,4 +36,4 @@ lint:
|
|||||||
@go vet $(PKGS)
|
@go vet $(PKGS)
|
||||||
|
|
||||||
reflex:
|
reflex:
|
||||||
reflex -r '\.go$$' -- sh -c 'echo; date; echo; go test ./...'
|
reflex -r '\.go$$' -- sh -c 'echo; date; echo; go test ./... && echo ALL PASS'
|
||||||
|
|||||||
@@ -3,9 +3,48 @@ package policy
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/mail"
|
||||||
"strings"
|
"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")
|
// ParseMailboxName takes a localPart string (ex: "user+ext" without "@domain")
|
||||||
// and returns just the mailbox name (ex: "user"). Returns an error if
|
// and returns just the mailbox name (ex: "user"). Returns an error if
|
||||||
// localPart contains invalid characters; it won't accept any that must be
|
// 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
|
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.
|
// 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
|
// An error is returned if the local or domain parts fail validation following the guidelines
|
||||||
// in RFC3696.
|
// in RFC3696.
|
||||||
@@ -202,3 +193,52 @@ LOOP:
|
|||||||
}
|
}
|
||||||
return buf.String(), domain, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,62 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jhillyerd/inbucket/pkg/config"
|
||||||
"github.com/jhillyerd/inbucket/pkg/policy"
|
"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) {
|
func TestParseMailboxName(t *testing.T) {
|
||||||
var validTable = []struct {
|
var validTable = []struct {
|
||||||
input string
|
input string
|
||||||
|
|||||||
25
pkg/policy/recipient.go
Normal file
25
pkg/policy/recipient.go
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user