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,6 +35,7 @@ type Manager interface {
|
|||||||
|
|
||||||
// StoreManager is a message Manager backed by the storage.Store.
|
// StoreManager is a message Manager backed by the storage.Store.
|
||||||
type StoreManager struct {
|
type StoreManager struct {
|
||||||
|
AddrPolicy *policy.Addressing
|
||||||
Store storage.Store
|
Store storage.Store
|
||||||
Hub *msghub.Hub
|
Hub *msghub.Hub
|
||||||
}
|
}
|
||||||
@@ -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.
|
// MailboxForAddress parses an email address to return the canonical mailbox name.
|
||||||
func (s *StoreManager) MailboxForAddress(mailbox string) (string, error) {
|
func (s *StoreManager) MailboxForAddress(mailbox string) (string, error) {
|
||||||
return policy.ParseMailboxName(mailbox)
|
return s.AddrPolicy.ExtractMailbox(mailbox)
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeMetadata populates Metadata from a storage.Message.
|
// makeMetadata populates Metadata from a storage.Message.
|
||||||
|
|||||||
@@ -15,13 +15,18 @@ type Addressing struct {
|
|||||||
Config *config.Root
|
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.
|
// NewRecipient parses an address into a Recipient.
|
||||||
func (a *Addressing) NewRecipient(address string) (*Recipient, error) {
|
func (a *Addressing) NewRecipient(address string) (*Recipient, error) {
|
||||||
local, domain, err := ParseEmailAddress(address)
|
local, domain, err := ParseEmailAddress(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mailbox, err := ParseMailboxName(local)
|
mailbox, err := a.ExtractMailbox(local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -66,35 +71,6 @@ func (a *Addressing) ShouldStoreDomain(domain string) bool {
|
|||||||
return false
|
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.
|
// 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.
|
||||||
@@ -263,3 +239,32 @@ func ValidateDomainPart(domain string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
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 {
|
var validTable = []struct {
|
||||||
input string
|
input string
|
||||||
expect string
|
expect string
|
||||||
@@ -139,7 +141,7 @@ func TestParseMailboxName(t *testing.T) {
|
|||||||
{"chars|}~", "chars|}~"},
|
{"chars|}~", "chars|}~"},
|
||||||
}
|
}
|
||||||
for _, tt := range validTable {
|
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)
|
t.Errorf("Error while parsing %q: %v", tt.input, err)
|
||||||
} else {
|
} else {
|
||||||
if result != tt.expect {
|
if result != tt.expect {
|
||||||
@@ -157,7 +159,7 @@ func TestParseMailboxName(t *testing.T) {
|
|||||||
{"first\nlast", "Control chars not permitted"},
|
{"first\nlast", "Control chars not permitted"},
|
||||||
}
|
}
|
||||||
for _, tt := range invalidTable {
|
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)
|
t.Errorf("Didn't get an error while parsing %q: %v", tt.input, tt.msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jhillyerd/inbucket/pkg/config"
|
"github.com/jhillyerd/inbucket/pkg/config"
|
||||||
"github.com/jhillyerd/inbucket/pkg/policy"
|
|
||||||
"github.com/jhillyerd/inbucket/pkg/storage"
|
"github.com/jhillyerd/inbucket/pkg/storage"
|
||||||
"github.com/jhillyerd/inbucket/pkg/stringutil"
|
"github.com/jhillyerd/inbucket/pkg/stringutil"
|
||||||
"github.com/rs/zerolog/log"
|
"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.
|
// AddMessage adds a message to the specified mailbox.
|
||||||
func (fs *Store) AddMessage(m storage.Message) (id string, err error) {
|
func (fs *Store) AddMessage(m storage.Message) (id string, err error) {
|
||||||
mb, err := fs.mbox(m.Mailbox())
|
mb := fs.mbox(m.Mailbox())
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
mb.Lock()
|
mb.Lock()
|
||||||
defer mb.Unlock()
|
defer mb.Unlock()
|
||||||
r, err := m.Source()
|
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.
|
// GetMessage returns the messages in the named mailbox, or an error.
|
||||||
func (fs *Store) GetMessage(mailbox, id string) (storage.Message, error) {
|
func (fs *Store) GetMessage(mailbox, id string) (storage.Message, error) {
|
||||||
mb, err := fs.mbox(mailbox)
|
mb := fs.mbox(mailbox)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mb.RLock()
|
mb.RLock()
|
||||||
defer mb.RUnlock()
|
defer mb.RUnlock()
|
||||||
return mb.getMessage(id)
|
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.
|
// GetMessages returns the messages in the named mailbox, or an error.
|
||||||
func (fs *Store) GetMessages(mailbox string) ([]storage.Message, error) {
|
func (fs *Store) GetMessages(mailbox string) ([]storage.Message, error) {
|
||||||
mb, err := fs.mbox(mailbox)
|
mb := fs.mbox(mailbox)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mb.RLock()
|
mb.RLock()
|
||||||
defer mb.RUnlock()
|
defer mb.RUnlock()
|
||||||
return mb.getMessages()
|
return mb.getMessages()
|
||||||
@@ -149,10 +139,7 @@ func (fs *Store) GetMessages(mailbox string) ([]storage.Message, error) {
|
|||||||
|
|
||||||
// MarkSeen flags the message as having been read.
|
// MarkSeen flags the message as having been read.
|
||||||
func (fs *Store) MarkSeen(mailbox, id string) error {
|
func (fs *Store) MarkSeen(mailbox, id string) error {
|
||||||
mb, err := fs.mbox(mailbox)
|
mb := fs.mbox(mailbox)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mb.Lock()
|
mb.Lock()
|
||||||
defer mb.Unlock()
|
defer mb.Unlock()
|
||||||
if !mb.indexLoaded {
|
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.
|
// RemoveMessage deletes a message by ID from the specified mailbox.
|
||||||
func (fs *Store) RemoveMessage(mailbox, id string) error {
|
func (fs *Store) RemoveMessage(mailbox, id string) error {
|
||||||
mb, err := fs.mbox(mailbox)
|
mb := fs.mbox(mailbox)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mb.Lock()
|
mb.Lock()
|
||||||
defer mb.Unlock()
|
defer mb.Unlock()
|
||||||
return mb.removeMessage(id)
|
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.
|
// PurgeMessages deletes all messages in the named mailbox, or returns an error.
|
||||||
func (fs *Store) PurgeMessages(mailbox string) error {
|
func (fs *Store) PurgeMessages(mailbox string) error {
|
||||||
mb, err := fs.mbox(mailbox)
|
mb := fs.mbox(mailbox)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
mb.Lock()
|
mb.Lock()
|
||||||
defer mb.Unlock()
|
defer mb.Unlock()
|
||||||
return mb.purge()
|
return mb.purge()
|
||||||
@@ -241,12 +222,8 @@ func (fs *Store) VisitMailboxes(f func([]storage.Message) (cont bool)) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mbox returns the named mailbox.
|
// mbox returns the named mailbox.
|
||||||
func (fs *Store) mbox(mailbox string) (*mbox, error) {
|
func (fs *Store) mbox(mailbox string) *mbox {
|
||||||
name, err := policy.ParseMailboxName(mailbox)
|
hash := stringutil.HashMailboxName(mailbox)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hash := stringutil.HashMailboxName(name)
|
|
||||||
s1 := hash[0:3]
|
s1 := hash[0:3]
|
||||||
s2 := hash[0:6]
|
s2 := hash[0:6]
|
||||||
path := filepath.Join(fs.mailPath, s1, s2, hash)
|
path := filepath.Join(fs.mailPath, s1, s2, hash)
|
||||||
@@ -254,11 +231,11 @@ func (fs *Store) mbox(mailbox string) (*mbox, error) {
|
|||||||
return &mbox{
|
return &mbox{
|
||||||
RWMutex: fs.hashLock.Get(hash),
|
RWMutex: fs.hashLock.Get(hash),
|
||||||
store: fs,
|
store: fs,
|
||||||
name: name,
|
name: mailbox,
|
||||||
dirName: hash,
|
dirName: hash,
|
||||||
path: path,
|
path: path,
|
||||||
indexPath: indexPath,
|
indexPath: indexPath,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mboxFromPath constructs a mailbox based on name hash.
|
// mboxFromPath constructs a mailbox based on name hash.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package test
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jhillyerd/inbucket/pkg/config"
|
||||||
"github.com/jhillyerd/inbucket/pkg/message"
|
"github.com/jhillyerd/inbucket/pkg/message"
|
||||||
"github.com/jhillyerd/inbucket/pkg/policy"
|
"github.com/jhillyerd/inbucket/pkg/policy"
|
||||||
"github.com/jhillyerd/inbucket/pkg/storage"
|
"github.com/jhillyerd/inbucket/pkg/storage"
|
||||||
@@ -55,7 +56,8 @@ func (m *ManagerStub) GetMetadata(mailbox string) ([]*message.Metadata, error) {
|
|||||||
|
|
||||||
// MailboxForAddress invokes policy.ParseMailboxName.
|
// MailboxForAddress invokes policy.ParseMailboxName.
|
||||||
func (m *ManagerStub) MailboxForAddress(address string) (string, error) {
|
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.
|
// MarkSeen marks a message as having been read.
|
||||||
|
|||||||
Reference in New Issue
Block a user