mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 09:37:02 +00:00
policy: Tie mailbox extraction to Addressing for #33
This commit is contained in:
@@ -35,8 +35,9 @@ type Manager interface {
|
||||
|
||||
// StoreManager is a message Manager backed by the storage.Store.
|
||||
type StoreManager struct {
|
||||
Store storage.Store
|
||||
Hub *msghub.Hub
|
||||
AddrPolicy *policy.Addressing
|
||||
Store storage.Store
|
||||
Hub *msghub.Hub
|
||||
}
|
||||
|
||||
// Deliver submits a new message to the store.
|
||||
@@ -154,7 +155,7 @@ func (s *StoreManager) SourceReader(mailbox, id string) (io.ReadCloser, error) {
|
||||
|
||||
// MailboxForAddress parses an email address to return the canonical mailbox name.
|
||||
func (s *StoreManager) MailboxForAddress(mailbox string) (string, error) {
|
||||
return policy.ParseMailboxName(mailbox)
|
||||
return s.AddrPolicy.ExtractMailbox(mailbox)
|
||||
}
|
||||
|
||||
// makeMetadata populates Metadata from a storage.Message.
|
||||
|
||||
@@ -15,13 +15,18 @@ type Addressing struct {
|
||||
Config *config.Root
|
||||
}
|
||||
|
||||
// ExtractMailbox extracts the mailbox name from a partial email address.
|
||||
func (a *Addressing) ExtractMailbox(address string) (string, error) {
|
||||
return parseMailboxName(address)
|
||||
}
|
||||
|
||||
// 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)
|
||||
mailbox, err := a.ExtractMailbox(local)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -66,35 +71,6 @@ func (a *Addressing) ShouldStoreDomain(domain string) bool {
|
||||
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
|
||||
// quoted according to RFC3696.
|
||||
func ParseMailboxName(localPart string) (result string, err error) {
|
||||
if localPart == "" {
|
||||
return "", fmt.Errorf("Mailbox name cannot be empty")
|
||||
}
|
||||
result = strings.ToLower(localPart)
|
||||
invalid := make([]byte, 0, 10)
|
||||
for i := 0; i < len(result); i++ {
|
||||
c := result[i]
|
||||
switch {
|
||||
case 'a' <= c && c <= 'z':
|
||||
case '0' <= c && c <= '9':
|
||||
case bytes.IndexByte([]byte("!#$%&'*+-=/?^_`.{|}~"), c) >= 0:
|
||||
default:
|
||||
invalid = append(invalid, c)
|
||||
}
|
||||
}
|
||||
if len(invalid) > 0 {
|
||||
return "", fmt.Errorf("Mailbox name contained invalid character(s): %q", invalid)
|
||||
}
|
||||
if idx := strings.Index(result, "+"); idx > -1 {
|
||||
result = result[0:idx]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -263,3 +239,32 @@ func ValidateDomainPart(domain string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 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
|
||||
// quoted according to RFC3696.
|
||||
func parseMailboxName(localPart string) (result string, err error) {
|
||||
if localPart == "" {
|
||||
return "", fmt.Errorf("Mailbox name cannot be empty")
|
||||
}
|
||||
result = strings.ToLower(localPart)
|
||||
invalid := make([]byte, 0, 10)
|
||||
for i := 0; i < len(result); i++ {
|
||||
c := result[i]
|
||||
switch {
|
||||
case 'a' <= c && c <= 'z':
|
||||
case '0' <= c && c <= '9':
|
||||
case bytes.IndexByte([]byte("!#$%&'*+-=/?^_`.{|}~"), c) >= 0:
|
||||
default:
|
||||
invalid = append(invalid, c)
|
||||
}
|
||||
}
|
||||
if len(invalid) > 0 {
|
||||
return "", fmt.Errorf("Mailbox name contained invalid character(s): %q", invalid)
|
||||
}
|
||||
if idx := strings.Index(result, "+"); idx > -1 {
|
||||
result = result[0:idx]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -122,7 +122,9 @@ func TestShouldStoreDomain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMailboxName(t *testing.T) {
|
||||
func TestExtractMailbox(t *testing.T) {
|
||||
addrPolicy := policy.Addressing{Config: &config.Root{}}
|
||||
|
||||
var validTable = []struct {
|
||||
input string
|
||||
expect string
|
||||
@@ -139,7 +141,7 @@ func TestParseMailboxName(t *testing.T) {
|
||||
{"chars|}~", "chars|}~"},
|
||||
}
|
||||
for _, tt := range validTable {
|
||||
if result, err := policy.ParseMailboxName(tt.input); err != nil {
|
||||
if result, err := addrPolicy.ExtractMailbox(tt.input); err != nil {
|
||||
t.Errorf("Error while parsing %q: %v", tt.input, err)
|
||||
} else {
|
||||
if result != tt.expect {
|
||||
@@ -157,7 +159,7 @@ func TestParseMailboxName(t *testing.T) {
|
||||
{"first\nlast", "Control chars not permitted"},
|
||||
}
|
||||
for _, tt := range invalidTable {
|
||||
if _, err := policy.ParseMailboxName(tt.input); err == nil {
|
||||
if _, err := addrPolicy.ExtractMailbox(tt.input); err == nil {
|
||||
t.Errorf("Didn't get an error while parsing %q: %v", tt.input, tt.msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jhillyerd/inbucket/pkg/config"
|
||||
"github.com/jhillyerd/inbucket/pkg/policy"
|
||||
"github.com/jhillyerd/inbucket/pkg/storage"
|
||||
"github.com/jhillyerd/inbucket/pkg/stringutil"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -66,10 +65,7 @@ func New(cfg config.Storage) (storage.Store, error) {
|
||||
|
||||
// AddMessage adds a message to the specified mailbox.
|
||||
func (fs *Store) AddMessage(m storage.Message) (id string, err error) {
|
||||
mb, err := fs.mbox(m.Mailbox())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
mb := fs.mbox(m.Mailbox())
|
||||
mb.Lock()
|
||||
defer mb.Unlock()
|
||||
r, err := m.Source()
|
||||
@@ -127,10 +123,7 @@ func (fs *Store) AddMessage(m storage.Message) (id string, err error) {
|
||||
|
||||
// GetMessage returns the messages in the named mailbox, or an error.
|
||||
func (fs *Store) GetMessage(mailbox, id string) (storage.Message, error) {
|
||||
mb, err := fs.mbox(mailbox)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mb := fs.mbox(mailbox)
|
||||
mb.RLock()
|
||||
defer mb.RUnlock()
|
||||
return mb.getMessage(id)
|
||||
@@ -138,10 +131,7 @@ func (fs *Store) GetMessage(mailbox, id string) (storage.Message, error) {
|
||||
|
||||
// GetMessages returns the messages in the named mailbox, or an error.
|
||||
func (fs *Store) GetMessages(mailbox string) ([]storage.Message, error) {
|
||||
mb, err := fs.mbox(mailbox)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mb := fs.mbox(mailbox)
|
||||
mb.RLock()
|
||||
defer mb.RUnlock()
|
||||
return mb.getMessages()
|
||||
@@ -149,10 +139,7 @@ func (fs *Store) GetMessages(mailbox string) ([]storage.Message, error) {
|
||||
|
||||
// MarkSeen flags the message as having been read.
|
||||
func (fs *Store) MarkSeen(mailbox, id string) error {
|
||||
mb, err := fs.mbox(mailbox)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mb := fs.mbox(mailbox)
|
||||
mb.Lock()
|
||||
defer mb.Unlock()
|
||||
if !mb.indexLoaded {
|
||||
@@ -175,10 +162,7 @@ func (fs *Store) MarkSeen(mailbox, id string) error {
|
||||
|
||||
// RemoveMessage deletes a message by ID from the specified mailbox.
|
||||
func (fs *Store) RemoveMessage(mailbox, id string) error {
|
||||
mb, err := fs.mbox(mailbox)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mb := fs.mbox(mailbox)
|
||||
mb.Lock()
|
||||
defer mb.Unlock()
|
||||
return mb.removeMessage(id)
|
||||
@@ -186,10 +170,7 @@ func (fs *Store) RemoveMessage(mailbox, id string) error {
|
||||
|
||||
// PurgeMessages deletes all messages in the named mailbox, or returns an error.
|
||||
func (fs *Store) PurgeMessages(mailbox string) error {
|
||||
mb, err := fs.mbox(mailbox)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mb := fs.mbox(mailbox)
|
||||
mb.Lock()
|
||||
defer mb.Unlock()
|
||||
return mb.purge()
|
||||
@@ -241,12 +222,8 @@ func (fs *Store) VisitMailboxes(f func([]storage.Message) (cont bool)) error {
|
||||
}
|
||||
|
||||
// mbox returns the named mailbox.
|
||||
func (fs *Store) mbox(mailbox string) (*mbox, error) {
|
||||
name, err := policy.ParseMailboxName(mailbox)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash := stringutil.HashMailboxName(name)
|
||||
func (fs *Store) mbox(mailbox string) *mbox {
|
||||
hash := stringutil.HashMailboxName(mailbox)
|
||||
s1 := hash[0:3]
|
||||
s2 := hash[0:6]
|
||||
path := filepath.Join(fs.mailPath, s1, s2, hash)
|
||||
@@ -254,11 +231,11 @@ func (fs *Store) mbox(mailbox string) (*mbox, error) {
|
||||
return &mbox{
|
||||
RWMutex: fs.hashLock.Get(hash),
|
||||
store: fs,
|
||||
name: name,
|
||||
name: mailbox,
|
||||
dirName: hash,
|
||||
path: path,
|
||||
indexPath: indexPath,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// mboxFromPath constructs a mailbox based on name hash.
|
||||
|
||||
@@ -3,6 +3,7 @@ package test
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/jhillyerd/inbucket/pkg/config"
|
||||
"github.com/jhillyerd/inbucket/pkg/message"
|
||||
"github.com/jhillyerd/inbucket/pkg/policy"
|
||||
"github.com/jhillyerd/inbucket/pkg/storage"
|
||||
@@ -55,7 +56,8 @@ func (m *ManagerStub) GetMetadata(mailbox string) ([]*message.Metadata, error) {
|
||||
|
||||
// MailboxForAddress invokes policy.ParseMailboxName.
|
||||
func (m *ManagerStub) MailboxForAddress(address string) (string, error) {
|
||||
return policy.ParseMailboxName(address)
|
||||
addrPolicy := &policy.Addressing{Config: &config.Root{}}
|
||||
return addrPolicy.ExtractMailbox(address)
|
||||
}
|
||||
|
||||
// MarkSeen marks a message as having been read.
|
||||
|
||||
Reference in New Issue
Block a user