mirror of
https://github.com/jhillyerd/inbucket.git
synced 2026-10-29 05:32:31 +00:00
test(policy): cover origin parsing and accept/store policy dispatch (#624)
This commit is contained in:
@@ -484,6 +484,254 @@ func TestValidateLocal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseOrigin verifies Origin values returned by Addressing.ParseOrigin.
|
||||||
|
// This function parses a MAIL FROM path, not a From header. See rfc5321#section-4.1.2
|
||||||
|
func TestParseOrigin(t *testing.T) {
|
||||||
|
localPolicy := policy.Addressing{Config: &config.Root{MailboxNaming: config.LocalNaming}}
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
input string
|
||||||
|
address string
|
||||||
|
localPart string
|
||||||
|
domain string
|
||||||
|
}{
|
||||||
|
"null sender": {input: "", address: "", localPart: "", domain: ""},
|
||||||
|
"common": {input: "user@example.com", address: "user@example.com", localPart: "user", domain: "example.com"},
|
||||||
|
"with label": {input: "user+mailbox@example.com", address: "user+mailbox@example.com", localPart: "user+mailbox", domain: "example.com"},
|
||||||
|
"case preserved": {input: "User@Example.COM", address: "User@Example.COM", localPart: "User", domain: "Example.COM"},
|
||||||
|
"special chars": {input: "a!#$%&'*@example.com", address: "a!#$%&'*@example.com", localPart: "a!#$%&'*", domain: "example.com"},
|
||||||
|
"ipv4": {input: "user@[127.0.0.1]", address: "user@[127.0.0.1]", localPart: "user", domain: "[127.0.0.1]"},
|
||||||
|
"route domain": {input: "@route.com:user@example.com", address: "@route.com:user@example.com", localPart: "user", domain: "example.com"},
|
||||||
|
"quoted local part": {input: `"first last"@example.com`, address: `"first last"@example.com`, localPart: "first last", domain: "example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
o, err := localPolicy.ParseOrigin(tc.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse of %q failed: %v", tc.input, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := o.Address.Address, tc.address; got != want {
|
||||||
|
t.Errorf("Got Address: %q, want: %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := o.LocalPart, tc.localPart; got != want {
|
||||||
|
t.Errorf("Got LocalPart: %q, want: %q", got, want)
|
||||||
|
}
|
||||||
|
if got, want := o.Domain, tc.domain; got != want {
|
||||||
|
t.Errorf("Got Domain: %q, want: %q", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseOriginInvalid verifies Addressing.ParseOrigin rejects malformed MAIL FROM paths.
|
||||||
|
func TestParseOriginInvalid(t *testing.T) {
|
||||||
|
localPolicy := policy.Addressing{Config: &config.Root{MailboxNaming: config.LocalNaming}}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
input, msg string
|
||||||
|
}{
|
||||||
|
{"first last@example.com", "Space not permitted"},
|
||||||
|
{"user@bad..domain.com", "Invalid domain not permitted"},
|
||||||
|
{"user@-bad.com", "Domain cannot start with hyphen"},
|
||||||
|
{"user", "Bare local part without domain"},
|
||||||
|
{"@route.comuser@example.com", "Unterminated forward-path route"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.input, func(t *testing.T) {
|
||||||
|
if o, err := localPolicy.ParseOrigin(tc.input); err == nil {
|
||||||
|
t.Errorf("Didn't get an error while parsing origin %q: %s (got %#v)", tc.input, tc.msg, o)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestShouldAcceptOriginDomain verifies origin-domain rejection patterns, including
|
||||||
|
// wildcards and case handling.
|
||||||
|
func TestShouldAcceptOriginDomain(t *testing.T) {
|
||||||
|
// Patterns are matched against the lowercased input domain; config.Process() is
|
||||||
|
// responsible for lowercasing the pattern list itself.
|
||||||
|
ap := &policy.Addressing{
|
||||||
|
Config: &config.Root{
|
||||||
|
SMTP: config.SMTP{
|
||||||
|
RejectOriginDomains: []string{"*.deny.com", "deny.com", "evil?om"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
domain string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{domain: "foo.com", want: true},
|
||||||
|
{domain: "deny.com", want: false},
|
||||||
|
{domain: "DENY.com", want: false},
|
||||||
|
// Wildcard subdomain matches, at any depth, but not the bare domain.
|
||||||
|
{domain: "a.deny.com", want: false},
|
||||||
|
{domain: "b.a.deny.com", want: false},
|
||||||
|
{domain: "deny.com.org", want: true},
|
||||||
|
// Single-character wildcards match exactly one character.
|
||||||
|
{domain: "evilcom", want: false},
|
||||||
|
{domain: "evilaom", want: false},
|
||||||
|
{domain: "evilaacom", want: true},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.domain, func(t *testing.T) {
|
||||||
|
got := ap.ShouldAcceptOriginDomain(tc.domain)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard-only pattern: `*.deny.com` matches subdomains at any depth, but not the
|
||||||
|
// bare domain - operators must list that separately.
|
||||||
|
wildcardOnly := &policy.Addressing{
|
||||||
|
Config: &config.Root{
|
||||||
|
SMTP: config.SMTP{RejectOriginDomains: []string{"*.deny.com"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
t.Run("wildcard only", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
domain string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{domain: "deny.com", want: true},
|
||||||
|
{domain: "a.deny.com", want: false},
|
||||||
|
{domain: "b.a.deny.com", want: false},
|
||||||
|
} {
|
||||||
|
if got := wildcardOnly.ShouldAcceptOriginDomain(tc.domain); got != tc.want {
|
||||||
|
t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// With no RejectOriginDomains configured, every origin is accepted.
|
||||||
|
noRestrictions := &policy.Addressing{
|
||||||
|
Config: &config.Root{
|
||||||
|
SMTP: config.SMTP{RejectDomains: []string{"deny.com"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !noRestrictions.ShouldAcceptOriginDomain("deny.com") {
|
||||||
|
t.Errorf("Got false for %q with no RejectOriginDomains configured, want: true", "deny.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
// An un-lowercased pattern does not match a lowercased domain: it is up to
|
||||||
|
// config.Process() to normalize pattern lists.
|
||||||
|
upperPattern := &policy.Addressing{
|
||||||
|
Config: &config.Root{
|
||||||
|
SMTP: config.SMTP{RejectOriginDomains: []string{"DENY.com"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !upperPattern.ShouldAcceptOriginDomain("deny.com") {
|
||||||
|
t.Errorf("Got false for %q against un-lowercased pattern, want: true", "deny.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOriginShouldAccept drives ParseOrigin and ShouldAccept together, as the
|
||||||
|
// SMTP server does when handling a MAIL FROM argument.
|
||||||
|
func TestOriginShouldAccept(t *testing.T) {
|
||||||
|
ap := &policy.Addressing{
|
||||||
|
Config: &config.Root{
|
||||||
|
SMTP: config.SMTP{
|
||||||
|
RejectOriginDomains: []string{"*.spam.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
input string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
"null sender": {input: "", want: true},
|
||||||
|
"accepted": {input: "user@example.com", want: true},
|
||||||
|
"rejected": {input: "user@mail.spam.com", want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
o, err := ap.ParseOrigin(tc.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse of %q failed: %v", tc.input, err)
|
||||||
|
}
|
||||||
|
if got := o.ShouldAccept(); got != tc.want {
|
||||||
|
t.Errorf("Got ShouldAccept %v for %q, want: %v", got, tc.input, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRecipientShouldAcceptStore drives NewRecipient with ShouldAccept and
|
||||||
|
// ShouldStore together, as the SMTP server does when handling a RCPT TO argument.
|
||||||
|
func TestRecipientShouldAcceptStore(t *testing.T) {
|
||||||
|
ap := &policy.Addressing{
|
||||||
|
Config: &config.Root{
|
||||||
|
MailboxNaming: config.FullNaming,
|
||||||
|
SMTP: config.SMTP{
|
||||||
|
DefaultAccept: true,
|
||||||
|
RejectDomains: []string{"reject.com"},
|
||||||
|
DefaultStore: false,
|
||||||
|
StoreDomains: []string{"keep.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]struct {
|
||||||
|
input string
|
||||||
|
wantAccept bool
|
||||||
|
wantStore bool
|
||||||
|
}{
|
||||||
|
"accepted and kept": {input: "user@keep.com", wantAccept: true, wantStore: true},
|
||||||
|
"accepted, discarded": {input: "user@example.com", wantAccept: true, wantStore: false},
|
||||||
|
"rejected": {input: "user@REJECT.com", wantAccept: false, wantStore: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
r, err := ap.NewRecipient(tc.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Parse of %q failed: %v", tc.input, err)
|
||||||
|
}
|
||||||
|
if got := r.ShouldAccept(); got != tc.wantAccept {
|
||||||
|
t.Errorf("Got ShouldAccept %v for %q, want: %v", got, tc.input, tc.wantAccept)
|
||||||
|
}
|
||||||
|
if got := r.ShouldStore(); got != tc.wantStore {
|
||||||
|
t.Errorf("Got ShouldStore %v for %q, want: %v", got, tc.input, tc.wantStore)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewRecipientInvalid verifies error paths of Addressing.NewRecipient.
|
||||||
|
func TestNewRecipientInvalid(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
policy policy.Addressing
|
||||||
|
input string
|
||||||
|
msg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid address",
|
||||||
|
policy: policy.Addressing{Config: &config.Root{MailboxNaming: config.LocalNaming}},
|
||||||
|
input: "user@bad..domain.com",
|
||||||
|
msg: "Invalid domain should fail ParseEmailAddress",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown mailbox naming",
|
||||||
|
policy: policy.Addressing{Config: &config.Root{}},
|
||||||
|
input: "user@example.com",
|
||||||
|
msg: "Unknown MailboxNaming should fail ExtractMailbox",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if r, err := tc.policy.NewRecipient(tc.input); err == nil {
|
||||||
|
t.Errorf("Didn't get an error while parsing recipient %q: %s (got %#v)", tc.input, tc.msg, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestRecipientAddress verifies the Recipient.Address values returned by Addressing.NewRecipient.
|
// TestRecipientAddress verifies the Recipient.Address values returned by Addressing.NewRecipient.
|
||||||
// This function parses a RCPT TO path, not a To header. See rfc5321#section-4.1.2
|
// This function parses a RCPT TO path, not a To header. See rfc5321#section-4.1.2
|
||||||
func TestRecipientAddress(t *testing.T) {
|
func TestRecipientAddress(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user