package store import ( "context" "crypto/tls" "fmt" "io" "os" "path/filepath" "github.com/asdine/storm" "github.com/coreos/bbolt" ) const ( DatabaseFilename = "crockery.db" MailboxesDirname = "mailboxes" ) type Interface interface { Domain() string TLS() tls.Certificate TLSConfig() *tls.Config SetDomain(string) error SetTLS([]byte, []byte) error AccountInterface MaildirInterface MessageInterface SpoolInterface io.Closer } func buildPath(parts ...string) string { if len(parts) == 0 || parts[0] == "" { return "" } return filepath.Join(parts...) } func BuildDatabasePath(home string) string { return buildPath(home, DatabaseFilename) } func BuildMailboxesPath(home string) string { return buildPath(home, MailboxesDirname) } func IsNotFound(err error) bool { return err.Error() == "not found" // Magic hardcoded value in storm } func New(ctx context.Context, home string) (Interface, error) { filename := BuildDatabasePath(home) db, err := storm.Open(filename, storm.BoltOptions(0600, &bolt.Options{})) if err != nil { return nil, err } out := &concrete{ home: home, storm: db, } if err := out.setup(); err != nil { return nil, err } return out, nil } func Init(home, domain string, certPEM, keyPEM []byte) error { filename := BuildDatabasePath(home) mailboxes := BuildMailboxesPath(home) if _, err := os.Stat(mailboxes); !os.IsNotExist(err) { return fmt.Errorf("mailboxes directory %q already exists", mailboxes) } db, err := storm.Open(filename, storm.BoltOptions(0600, &bolt.Options{})) if err != nil { return err } builder := &concrete{ home: home, storm: db, } defer builder.Close() if err := os.Mkdir(mailboxes, 0700); err != nil { return fmt.Errorf("Couldn't create mailboxes directory %q: %v", mailboxes, err) } if err := builder.SetDomain(domain); err != nil { return fmt.Errorf("Couldn't set domain: %v", err) } if err := builder.SetTLS(certPEM, keyPEM); err != nil { return fmt.Errorf("Couldn't set TLS: %v", err) } return nil } type concrete struct { home string storm *storm.DB domainBucket storm.Node // These are persisted in BoltDB, but we might as well keep them in memory // for the duration. Calls to SetDomain/SetTLS will invalidate them. domain string cert tls.Certificate } func (c *concrete) Domain() string { return c.domain } func (c *concrete) TLS() tls.Certificate { return c.cert } func (c *concrete) TLSConfig() *tls.Config { return &tls.Config{ GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) { certCopy := c.TLS() return &certCopy, nil }, ServerName: c.Domain(), } } func (c *concrete) SetDomain(domain string) error { if err := c.storm.Set("config", "domain", domain); err != nil { return err } c.domain = domain return nil } func (c *concrete) SetTLS(certPEM, keyPEM []byte) error { cert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { return fmt.Errorf("Couldn't parse data as TLS keypair: %v", err) } if cert.PrivateKey == nil { return fmt.Errorf("No private key for TLS certificate") } domainBucket := c.storm.From("domains").From(c.Domain()) tx, err := domainBucket.Begin(true) if err != nil { return err } if err := tx.Set("config", "cert", certPEM); err != nil { return err } if err := tx.Set("config", "key", keyPEM); err != nil { return err } if err := tx.Commit(); err != nil { return err } c.cert = cert return nil } func (c *concrete) Close() error { return c.storm.Close() } // Bootstraps all cached values from storm func (c *concrete) setup() error { var domain string var keyPEM []byte var certPEM []byte if err := c.storm.Get("config", "domain", &domain); err != nil { return fmt.Errorf("Couldn't read config/domain: %v", err) } domainBucket := c.storm.From("domains").From(domain) if err := domainBucket.Get("config", "cert", &certPEM); err != nil { return fmt.Errorf("Couldn't read domains/%s/config/cert: %v", domain, err) } if err := domainBucket.Get("config", "key", &keyPEM); err != nil { return fmt.Errorf("Couldn't read domains/%s/config/key: %v", domain, err) } cert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { return fmt.Errorf("Couldn't parse key and certificate: %v", err) } c.domainBucket = domainBucket c.domain = domain c.cert = cert return nil }