diff --git a/pkg/message/manager.go b/pkg/message/manager.go index 35d0fcd..b7aba5d 100644 --- a/pkg/message/manager.go +++ b/pkg/message/manager.go @@ -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. diff --git a/pkg/policy/address.go b/pkg/policy/address.go index 25cee9e..be353c0 100644 --- a/pkg/policy/address.go +++ b/pkg/policy/address.go @@ -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 +} diff --git a/pkg/policy/address_test.go b/pkg/policy/address_test.go index 56ae27f..63e5090 100644 --- a/pkg/policy/address_test.go +++ b/pkg/policy/address_test.go @@ -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) } } diff --git a/pkg/storage/file/fstore.go b/pkg/storage/file/fstore.go index b31ea0f..a52bc36 100644 --- a/pkg/storage/file/fstore.go +++ b/pkg/storage/file/fstore.go @@ -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. diff --git a/pkg/test/manager.go b/pkg/test/manager.go index f5053df..d719eb9 100644 --- a/pkg/test/manager.go +++ b/pkg/test/manager.go @@ -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.