mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 09:37:02 +00:00
policy: Implement recipient domain policy for #51
- INBUCKET_SMTP_DEFAULTACCEPT - INBUCKET_SMTP_ACCEPTDOMAINS - INBUCKET_SMTP_REJECTDOMAINS
This commit is contained in:
@@ -15,6 +15,9 @@ variables it supports:
|
|||||||
INBUCKET_SMTP_MAXRECIPIENTS 200 Maximum RCPT TO per message
|
INBUCKET_SMTP_MAXRECIPIENTS 200 Maximum RCPT TO per message
|
||||||
INBUCKET_SMTP_MAXMESSAGEBYTES 10240000 Maximum message size
|
INBUCKET_SMTP_MAXMESSAGEBYTES 10240000 Maximum message size
|
||||||
INBUCKET_SMTP_STOREMESSAGES true Store incoming mail?
|
INBUCKET_SMTP_STOREMESSAGES true Store incoming mail?
|
||||||
|
INBUCKET_SMTP_DEFAULTACCEPT true Accept all mail by default?
|
||||||
|
INBUCKET_SMTP_ACCEPTDOMAINS Domains to accept mail for
|
||||||
|
INBUCKET_SMTP_REJECTDOMAINS Domains to reject mail for
|
||||||
INBUCKET_SMTP_TIMEOUT 300s Idle network timeout
|
INBUCKET_SMTP_TIMEOUT 300s Idle network timeout
|
||||||
INBUCKET_POP3_ADDR 0.0.0.0:1100 POP3 server IP4 host:port
|
INBUCKET_POP3_ADDR 0.0.0.0:1100 POP3 server IP4 host:port
|
||||||
INBUCKET_POP3_DOMAIN inbucket HELLO domain
|
INBUCKET_POP3_DOMAIN inbucket HELLO domain
|
||||||
@@ -116,6 +119,39 @@ solar system.
|
|||||||
- Default: `true`
|
- Default: `true`
|
||||||
- Values: `true` or `false`
|
- Values: `true` or `false`
|
||||||
|
|
||||||
|
### Default Recipient Accept Policy
|
||||||
|
|
||||||
|
`INBUCKET_SMTP_DEFAULTACCEPT`
|
||||||
|
|
||||||
|
If true, Inbucket will accept mail to any domain unless present in the reject
|
||||||
|
domains list. If false, recipients will be rejected unless their domain is
|
||||||
|
present in the accept domains list.
|
||||||
|
|
||||||
|
- Default: `true`
|
||||||
|
- Values: `true` or `false`
|
||||||
|
|
||||||
|
### Accepted Recipient Domain List
|
||||||
|
|
||||||
|
`INBUCKET_SMTP_ACCEPTDOMAINS`
|
||||||
|
|
||||||
|
List of domains to accept mail for when `INBUCKET_SMTP_DEFAULTACCEPT` is false;
|
||||||
|
has no effect when true.
|
||||||
|
|
||||||
|
- Default: None
|
||||||
|
- Values: Comma separated list of domains
|
||||||
|
- Example: `localhost,mysite.org`
|
||||||
|
|
||||||
|
### Rejected Recipient Domain List
|
||||||
|
|
||||||
|
`INBUCKET_SMTP_REJECTDOMAINS`
|
||||||
|
|
||||||
|
List of domains to reject mail for when `INBUCKET_SMTP_DEFAULTACCEPT` is true;
|
||||||
|
has no effect when false.
|
||||||
|
|
||||||
|
- Default: None
|
||||||
|
- Values: Comma separated list of domains
|
||||||
|
- Example: `reject.com,gmail.com`
|
||||||
|
|
||||||
### Network Idle Timeout
|
### Network Idle Timeout
|
||||||
|
|
||||||
`INBUCKET_SMTP_TIMEOUT`
|
`INBUCKET_SMTP_TIMEOUT`
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/jhillyerd/inbucket/pkg/stringutil"
|
||||||
"github.com/kelseyhightower/envconfig"
|
"github.com/kelseyhightower/envconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,6 +46,9 @@ type SMTP struct {
|
|||||||
MaxRecipients int `required:"true" default:"200" desc:"Maximum RCPT TO per message"`
|
MaxRecipients int `required:"true" default:"200" desc:"Maximum RCPT TO per message"`
|
||||||
MaxMessageBytes int `required:"true" default:"10240000" desc:"Maximum message size"`
|
MaxMessageBytes int `required:"true" default:"10240000" desc:"Maximum message size"`
|
||||||
StoreMessages bool `required:"true" default:"true" desc:"Store incoming mail?"`
|
StoreMessages bool `required:"true" default:"true" desc:"Store incoming mail?"`
|
||||||
|
DefaultAccept bool `required:"true" default:"true" desc:"Accept all mail by default?"`
|
||||||
|
AcceptDomains []string `desc:"Domains to accept mail for"`
|
||||||
|
RejectDomains []string `desc:"Domains to reject mail for"`
|
||||||
Timeout time.Duration `required:"true" default:"300s" desc:"Idle network timeout"`
|
Timeout time.Duration `required:"true" default:"300s" desc:"Idle network timeout"`
|
||||||
Debug bool `ignored:"true"`
|
Debug bool `ignored:"true"`
|
||||||
}
|
}
|
||||||
@@ -83,6 +87,8 @@ func Process() (*Root, error) {
|
|||||||
c := &Root{}
|
c := &Root{}
|
||||||
err := envconfig.Process(prefix, c)
|
err := envconfig.Process(prefix, c)
|
||||||
c.SMTP.DomainNoStore = strings.ToLower(c.SMTP.DomainNoStore)
|
c.SMTP.DomainNoStore = strings.ToLower(c.SMTP.DomainNoStore)
|
||||||
|
stringutil.SliceToLower(c.SMTP.AcceptDomains)
|
||||||
|
stringutil.SliceToLower(c.SMTP.RejectDomains)
|
||||||
return c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jhillyerd/inbucket/pkg/config"
|
"github.com/jhillyerd/inbucket/pkg/config"
|
||||||
|
"github.com/jhillyerd/inbucket/pkg/stringutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Addressing handles email address policy.
|
// Addressing handles email address policy.
|
||||||
@@ -29,14 +30,26 @@ func (a *Addressing) NewRecipient(address string) (*Recipient, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Recipient{
|
return &Recipient{
|
||||||
Address: *ar,
|
Address: *ar,
|
||||||
apolicy: a,
|
addrPolicy: a,
|
||||||
LocalPart: local,
|
LocalPart: local,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
Mailbox: mailbox,
|
Mailbox: mailbox,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShouldAcceptDomain indicates if Inbucket accepts mail destined for the specified domain.
|
||||||
|
func (a *Addressing) ShouldAcceptDomain(domain string) bool {
|
||||||
|
domain = strings.ToLower(domain)
|
||||||
|
if a.Config.DefaultAccept && !stringutil.SliceContains(a.Config.RejectDomains, domain) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !a.Config.DefaultAccept && stringutil.SliceContains(a.Config.AcceptDomains, domain) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// ShouldStoreDomain indicates if Inbucket stores email destined for the specified domain.
|
// ShouldStoreDomain indicates if Inbucket stores email destined for the specified domain.
|
||||||
func (a *Addressing) ShouldStoreDomain(domain string) bool {
|
func (a *Addressing) ShouldStoreDomain(domain string) bool {
|
||||||
if a.Config.StoreMessages {
|
if a.Config.StoreMessages {
|
||||||
|
|||||||
@@ -8,6 +8,59 @@ import (
|
|||||||
"github.com/jhillyerd/inbucket/pkg/policy"
|
"github.com/jhillyerd/inbucket/pkg/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestShouldAcceptDomain(t *testing.T) {
|
||||||
|
// Test with default accept.
|
||||||
|
ap := &policy.Addressing{
|
||||||
|
Config: config.SMTP{
|
||||||
|
DefaultAccept: true,
|
||||||
|
RejectDomains: []string{"a.deny.com", "deny.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
domain string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{domain: "bar.com", want: true},
|
||||||
|
{domain: "DENY.com", want: false},
|
||||||
|
{domain: "a.deny.com", want: false},
|
||||||
|
{domain: "b.deny.com", want: true},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.domain, func(t *testing.T) {
|
||||||
|
got := ap.ShouldAcceptDomain(tc.domain)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Test with default reject.
|
||||||
|
ap = &policy.Addressing{
|
||||||
|
Config: config.SMTP{
|
||||||
|
DefaultAccept: false,
|
||||||
|
AcceptDomains: []string{"a.allow.com", "allow.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testCases = []struct {
|
||||||
|
domain string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{domain: "bar.com", want: false},
|
||||||
|
{domain: "ALLOW.com", want: true},
|
||||||
|
{domain: "a.allow.com", want: true},
|
||||||
|
{domain: "b.allow.com", want: false},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.domain, func(t *testing.T) {
|
||||||
|
got := ap.ShouldAcceptDomain(tc.domain)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("Got %v for %q, want: %v", got, tc.domain, tc.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestShouldStoreDomain(t *testing.T) {
|
func TestShouldStoreDomain(t *testing.T) {
|
||||||
// Test with storage enabled.
|
// Test with storage enabled.
|
||||||
ap := &policy.Addressing{
|
ap := &policy.Addressing{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import "net/mail"
|
|||||||
// Recipient represents a potential email recipient, allows policies for it to be queried.
|
// Recipient represents a potential email recipient, allows policies for it to be queried.
|
||||||
type Recipient struct {
|
type Recipient struct {
|
||||||
mail.Address
|
mail.Address
|
||||||
apolicy *Addressing
|
addrPolicy *Addressing
|
||||||
// LocalPart is the part of the address before @, including +extension.
|
// LocalPart is the part of the address before @, including +extension.
|
||||||
LocalPart string
|
LocalPart string
|
||||||
// Domain is the part of the address after @.
|
// Domain is the part of the address after @.
|
||||||
@@ -16,10 +16,10 @@ type Recipient struct {
|
|||||||
|
|
||||||
// ShouldAccept returns true if Inbucket should accept mail for this recipient.
|
// ShouldAccept returns true if Inbucket should accept mail for this recipient.
|
||||||
func (r *Recipient) ShouldAccept() bool {
|
func (r *Recipient) ShouldAccept() bool {
|
||||||
return true
|
return r.addrPolicy.ShouldAcceptDomain(r.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldStore returns true if Inbucket should store mail for this recipient.
|
// ShouldStore returns true if Inbucket should store mail for this recipient.
|
||||||
func (r *Recipient) ShouldStore() bool {
|
func (r *Recipient) ShouldStore() bool {
|
||||||
return r.apolicy.ShouldStoreDomain(r.Domain)
|
return r.addrPolicy.ShouldStoreDomain(r.Domain)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,6 +329,11 @@ func (s *Session) mailHandler(cmd string, arg string) {
|
|||||||
s.logger.Warn().Msgf("Bad address as RCPT arg: %q, %s", addr, err)
|
s.logger.Warn().Msgf("Bad address as RCPT arg: %q, %s", addr, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !recip.ShouldAccept() {
|
||||||
|
s.logger.Warn().Str("addr", addr).Msg("Rejecting recipient")
|
||||||
|
s.send("550 Relay not permitted")
|
||||||
|
return
|
||||||
|
}
|
||||||
if len(s.recipients) >= s.config.MaxRecipients {
|
if len(s.recipients) >= s.config.MaxRecipients {
|
||||||
s.logger.Warn().Msgf("Maximum limit of %v recipients reached",
|
s.logger.Warn().Msgf("Maximum limit of %v recipients reached",
|
||||||
s.config.MaxRecipients)
|
s.config.MaxRecipients)
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ func TestMailState(t *testing.T) {
|
|||||||
{"RCPT TO:<u1@gmail.com>", 250},
|
{"RCPT TO:<u1@gmail.com>", 250},
|
||||||
{"RCPT TO: <u2@gmail.com>", 250},
|
{"RCPT TO: <u2@gmail.com>", 250},
|
||||||
{"RCPT TO:u3@gmail.com", 250},
|
{"RCPT TO:u3@gmail.com", 250},
|
||||||
|
{"RCPT TO:u3@deny.com", 550},
|
||||||
{"RCPT TO: u4@gmail.com", 250},
|
{"RCPT TO: u4@gmail.com", 250},
|
||||||
{"RSET", 250},
|
{"RSET", 250},
|
||||||
{"MAIL FROM:<john@gmail.com>", 250},
|
{"MAIL FROM:<john@gmail.com>", 250},
|
||||||
@@ -366,9 +367,11 @@ func setupSMTPServer(ds storage.Store) (s *Server, buf *bytes.Buffer, teardown f
|
|||||||
Domain: "inbucket.local",
|
Domain: "inbucket.local",
|
||||||
DomainNoStore: "bitbucket.local",
|
DomainNoStore: "bitbucket.local",
|
||||||
MaxRecipients: 5,
|
MaxRecipients: 5,
|
||||||
Timeout: 5,
|
|
||||||
MaxMessageBytes: 5000,
|
MaxMessageBytes: 5000,
|
||||||
StoreMessages: true,
|
StoreMessages: true,
|
||||||
|
DefaultAccept: true,
|
||||||
|
RejectDomains: []string{"deny.com"},
|
||||||
|
Timeout: 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture log output
|
// Capture log output
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HashMailboxName accepts a mailbox name and hashes it. filestore uses this as
|
// HashMailboxName accepts a mailbox name and hashes it. filestore uses this as
|
||||||
@@ -28,3 +29,20 @@ func StringAddressList(addrs []*mail.Address) []string {
|
|||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SliceContains returns true if s is present in slice.
|
||||||
|
func SliceContains(slice []string, s string) bool {
|
||||||
|
for _, v := range slice {
|
||||||
|
if s == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SliceToLower lowercases the contents of slice of strings.
|
||||||
|
func SliceToLower(slice []string) {
|
||||||
|
for i, s := range slice {
|
||||||
|
slice[i] = strings.ToLower(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user