mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 17:44:35 +03:00
add config options to disable a domain and to disable logins for an account
to facilitate migrations from/to other mail setups. a domain can be added in "disabled" mode (or can be disabled/enabled later on). you can configure a disabled domain, but incoming/outgoing messages involving the domain are rejected with temporary error codes (as this may occur during a migration, remote servers will try again, hopefully to the correct machine or after this machine has been configured correctly). also, no acme tls certs will be requested for disabled domains (the autoconfig/mta-sts dns records may still point to the current/previous machine). accounts with addresses at disabled domains can still login, unless logins are disabled for their accounts. an account now has an option to disable logins. you can specify an error message to show. this will be shown in smtp, imap and the web interfaces. it could contain a message about migrations, and possibly a URL to a page with information about how to migrate. incoming/outgoing email involving accounts with login disabled are still accepted/delivered as normal (unless the domain involved in the messages is disabled too). account operations by the admin, such as importing/exporting messages still works. in the admin web interface, listings of domains/accounts show if they are disabled. domains & accounts can be enabled/disabled through the config file, cli commands and admin web interface. for issue #175 by RobSlgm
This commit is contained in:
@ -73,6 +73,7 @@ var (
|
||||
ErrUnknownCredentials = errors.New("credentials not found")
|
||||
ErrAccountUnknown = errors.New("no such account")
|
||||
ErrOverQuota = errors.New("account over quota")
|
||||
ErrLoginDisabled = errors.New("login disabled for account")
|
||||
)
|
||||
|
||||
var DefaultInitialMailboxes = config.InitialMailboxes{
|
||||
@ -876,7 +877,7 @@ func closeAccount(acc *Account) (rerr error) {
|
||||
//
|
||||
// No additional data path prefix or ".db" suffix should be added to the name.
|
||||
// A single shared account exists per name.
|
||||
func OpenAccount(log mlog.Log, name string) (*Account, error) {
|
||||
func OpenAccount(log mlog.Log, name string, checkLoginDisabled bool) (*Account, error) {
|
||||
openAccounts.Lock()
|
||||
defer openAccounts.Unlock()
|
||||
if acc, ok := openAccounts.names[name]; ok {
|
||||
@ -884,8 +885,10 @@ func OpenAccount(log mlog.Log, name string) (*Account, error) {
|
||||
return acc, nil
|
||||
}
|
||||
|
||||
if _, ok := mox.Conf.Account(name); !ok {
|
||||
if a, ok := mox.Conf.Account(name); !ok {
|
||||
return nil, ErrAccountUnknown
|
||||
} else if checkLoginDisabled && a.LoginDisabled != "" {
|
||||
return nil, fmt.Errorf("%w: %s", ErrLoginDisabled, a.LoginDisabled)
|
||||
}
|
||||
|
||||
acc, err := openAccount(log, name)
|
||||
@ -1657,6 +1660,13 @@ func (a *Account) SetPassword(log mlog.Log, password string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SessionsClear invalidates all (web) login sessions for the account.
|
||||
func (a *Account) SessionsClear(ctx context.Context, log mlog.Log) error {
|
||||
return a.DB.Write(ctx, func(tx *bstore.Tx) error {
|
||||
return sessionRemoveAll(ctx, log, tx, a.Name)
|
||||
})
|
||||
}
|
||||
|
||||
// Subjectpass returns the signing key for use with subjectpass for the given
|
||||
// email address with canonical localpart.
|
||||
func (a *Account) Subjectpass(email string) (key string, err error) {
|
||||
@ -2211,13 +2221,15 @@ func manageAuthCache() {
|
||||
// OpenEmailAuth opens an account given an email address and password.
|
||||
//
|
||||
// The email address may contain a catchall separator.
|
||||
func OpenEmailAuth(log mlog.Log, email string, password string) (acc *Account, rerr error) {
|
||||
func OpenEmailAuth(log mlog.Log, email string, password string, checkLoginDisabled bool) (acc *Account, rerr error) {
|
||||
password, err := precis.OpaqueString.String(password)
|
||||
if err != nil {
|
||||
return nil, ErrUnknownCredentials
|
||||
}
|
||||
|
||||
acc, _, rerr = OpenEmail(log, email)
|
||||
// We check for LoginDisabled after verifying the password. Otherwise users can get
|
||||
// messages about the account being disabled without knowing the password.
|
||||
acc, _, rerr = OpenEmail(log, email, false)
|
||||
if rerr != nil {
|
||||
return
|
||||
}
|
||||
@ -2240,34 +2252,40 @@ func OpenEmailAuth(log mlog.Log, email string, password string) (acc *Account, r
|
||||
authCache.Lock()
|
||||
ok := len(password) >= 8 && authCache.success[authKey{email, pw.Hash}] == password
|
||||
authCache.Unlock()
|
||||
if ok {
|
||||
return
|
||||
if !ok {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(pw.Hash), []byte(password)); err != nil {
|
||||
return acc, ErrUnknownCredentials
|
||||
}
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(pw.Hash), []byte(password)); err != nil {
|
||||
rerr = ErrUnknownCredentials
|
||||
} else {
|
||||
authCache.Lock()
|
||||
authCache.success[authKey{email, pw.Hash}] = password
|
||||
authCache.Unlock()
|
||||
if checkLoginDisabled {
|
||||
conf, aok := acc.Conf()
|
||||
if !aok {
|
||||
return acc, fmt.Errorf("cannot find config for account")
|
||||
} else if conf.LoginDisabled != "" {
|
||||
return acc, fmt.Errorf("%w: %s", ErrLoginDisabled, conf.LoginDisabled)
|
||||
}
|
||||
}
|
||||
authCache.Lock()
|
||||
authCache.success[authKey{email, pw.Hash}] = password
|
||||
authCache.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// OpenEmail opens an account given an email address.
|
||||
//
|
||||
// The email address may contain a catchall separator.
|
||||
func OpenEmail(log mlog.Log, email string) (*Account, config.Destination, error) {
|
||||
func OpenEmail(log mlog.Log, email string, checkLoginDisabled bool) (*Account, config.Destination, error) {
|
||||
addr, err := smtp.ParseAddress(email)
|
||||
if err != nil {
|
||||
return nil, config.Destination{}, fmt.Errorf("%w: %v", ErrUnknownCredentials, err)
|
||||
}
|
||||
accountName, _, _, dest, err := mox.LookupAddress(addr.Localpart, addr.Domain, false, false)
|
||||
accountName, _, _, dest, err := mox.LookupAddress(addr.Localpart, addr.Domain, false, false, false)
|
||||
if err != nil && (errors.Is(err, mox.ErrAddressNotFound) || errors.Is(err, mox.ErrDomainNotFound)) {
|
||||
return nil, config.Destination{}, ErrUnknownCredentials
|
||||
} else if err != nil {
|
||||
return nil, config.Destination{}, fmt.Errorf("looking up address: %v", err)
|
||||
}
|
||||
acc, err := OpenAccount(log, accountName)
|
||||
acc, err := OpenAccount(log, accountName, checkLoginDisabled)
|
||||
if err != nil {
|
||||
return nil, config.Destination{}, err
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ func TestMailbox(t *testing.T) {
|
||||
os.RemoveAll("../testdata/store/data")
|
||||
mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
|
||||
mox.MustLoadConfig(true, false)
|
||||
acc, err := OpenAccount(log, "mjl")
|
||||
acc, err := OpenAccount(log, "mjl", false)
|
||||
tcheck(t, err, "open account")
|
||||
defer func() {
|
||||
err = acc.Close()
|
||||
@ -224,30 +224,30 @@ func TestMailbox(t *testing.T) {
|
||||
|
||||
// Run the auth tests twice for possible cache effects.
|
||||
for i := 0; i < 2; i++ {
|
||||
_, err := OpenEmailAuth(log, "mjl@mox.example", "bogus")
|
||||
_, err := OpenEmailAuth(log, "mjl@mox.example", "bogus", false)
|
||||
if err != ErrUnknownCredentials {
|
||||
t.Fatalf("got %v, expected ErrUnknownCredentials", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
acc2, err := OpenEmailAuth(log, "mjl@mox.example", "testtest")
|
||||
acc2, err := OpenEmailAuth(log, "mjl@mox.example", "testtest", false)
|
||||
tcheck(t, err, "open for email with auth")
|
||||
err = acc2.Close()
|
||||
tcheck(t, err, "close account")
|
||||
}
|
||||
|
||||
acc2, err := OpenEmailAuth(log, "other@mox.example", "testtest")
|
||||
acc2, err := OpenEmailAuth(log, "other@mox.example", "testtest", false)
|
||||
tcheck(t, err, "open for email with auth")
|
||||
err = acc2.Close()
|
||||
tcheck(t, err, "close account")
|
||||
|
||||
_, err = OpenEmailAuth(log, "bogus@mox.example", "testtest")
|
||||
_, err = OpenEmailAuth(log, "bogus@mox.example", "testtest", false)
|
||||
if err != ErrUnknownCredentials {
|
||||
t.Fatalf("got %v, expected ErrUnknownCredentials", err)
|
||||
}
|
||||
|
||||
_, err = OpenEmailAuth(log, "mjl@test.example", "testtest")
|
||||
_, err = OpenEmailAuth(log, "mjl@test.example", "testtest", false)
|
||||
if err != ErrUnknownCredentials {
|
||||
t.Fatalf("got %v, expected ErrUnknownCredentials", err)
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ func TestExport(t *testing.T) {
|
||||
os.RemoveAll("../testdata/store/data")
|
||||
mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
|
||||
mox.MustLoadConfig(true, false)
|
||||
acc, err := OpenAccount(pkglog, "mjl")
|
||||
acc, err := OpenAccount(pkglog, "mjl", false)
|
||||
tcheck(t, err, "open account")
|
||||
defer func() {
|
||||
err := acc.Close()
|
||||
|
@ -38,17 +38,23 @@ var sessions = struct {
|
||||
pendingFlushes: map[string]map[SessionToken]struct{}{},
|
||||
}
|
||||
|
||||
// Ensure sessions for account are initialized from database. If the sessions were
|
||||
// initialized from the database, or when alwaysOpenAccount is true, an open
|
||||
// account is returned (assuming no error occurred).
|
||||
// Ensure sessions for account are initialized from database. If openAccount is
|
||||
// set, an account is returned on success.
|
||||
//
|
||||
// must be called with sessions lock held.
|
||||
func ensureAccountSessions(ctx context.Context, log mlog.Log, accountName string, alwaysOpenAccount bool) (*Account, error) {
|
||||
var acc *Account
|
||||
func ensureAccountSessions(ctx context.Context, log mlog.Log, accountName string, openAccount bool) (acc *Account, rerr error) {
|
||||
defer func() {
|
||||
if !openAccount && acc != nil {
|
||||
if err := acc.Close(); err != nil && rerr == nil {
|
||||
rerr = fmt.Errorf("closing account: %v", err)
|
||||
}
|
||||
acc = nil
|
||||
}
|
||||
}()
|
||||
accSessions := sessions.accounts[accountName]
|
||||
if accSessions == nil {
|
||||
var err error
|
||||
acc, err = OpenAccount(log, accountName)
|
||||
acc, err = OpenAccount(log, accountName, openAccount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -70,10 +76,10 @@ func ensureAccountSessions(ctx context.Context, log mlog.Log, accountName string
|
||||
|
||||
sessions.accounts[accountName] = accSessions
|
||||
}
|
||||
if acc == nil && alwaysOpenAccount {
|
||||
return OpenAccount(log, accountName)
|
||||
if acc == nil && openAccount {
|
||||
acc, rerr = OpenAccount(log, accountName, true)
|
||||
}
|
||||
return acc, nil
|
||||
return
|
||||
}
|
||||
|
||||
// SessionUse checks if a session is valid. If csrfToken is the empty string, no
|
||||
@ -83,13 +89,8 @@ func SessionUse(ctx context.Context, log mlog.Log, accountName string, sessionTo
|
||||
sessions.Lock()
|
||||
defer sessions.Unlock()
|
||||
|
||||
acc, err := ensureAccountSessions(ctx, log, accountName, false)
|
||||
if err != nil {
|
||||
if _, err := ensureAccountSessions(ctx, log, accountName, false); err != nil {
|
||||
return LoginSession{}, err
|
||||
} else if acc != nil {
|
||||
if err := acc.Close(); err != nil {
|
||||
return LoginSession{}, fmt.Errorf("closing account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return sessionUse(ctx, log, accountName, sessionToken, csrfToken)
|
||||
@ -149,7 +150,7 @@ func sessionsDelayedFlush(log mlog.Log, accountName string) {
|
||||
return
|
||||
}
|
||||
|
||||
acc, err := OpenAccount(log, accountName)
|
||||
acc, err := OpenAccount(log, accountName, false)
|
||||
if err != nil && errors.Is(err, ErrAccountUnknown) {
|
||||
// Account may have been removed. Nothing to flush.
|
||||
log.Infox("flushing sessions for account", err, slog.String("account", accountName))
|
||||
@ -282,7 +283,10 @@ func SessionRemove(ctx context.Context, log mlog.Log, accountName string, sessio
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer acc.Close()
|
||||
defer func() {
|
||||
err := acc.Close()
|
||||
log.Check(err, "closing account")
|
||||
}()
|
||||
|
||||
ls, ok := sessions.accounts[accountName][sessionToken]
|
||||
if !ok {
|
||||
|
@ -19,7 +19,7 @@ func TestThreadingUpgrade(t *testing.T) {
|
||||
os.RemoveAll("../testdata/store/data")
|
||||
mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
|
||||
mox.MustLoadConfig(true, false)
|
||||
acc, err := OpenAccount(log, "mjl")
|
||||
acc, err := OpenAccount(log, "mjl", true)
|
||||
tcheck(t, err, "open account")
|
||||
defer func() {
|
||||
err = acc.Close()
|
||||
@ -143,7 +143,7 @@ func TestThreadingUpgrade(t *testing.T) {
|
||||
tcheck(t, err, "closing db")
|
||||
|
||||
// Open the account again, that should get the account upgraded. Wait for upgrade to finish.
|
||||
acc, err = OpenAccount(log, "mjl")
|
||||
acc, err = OpenAccount(log, "mjl", true)
|
||||
tcheck(t, err, "open account")
|
||||
err = acc.ThreadingWait(log)
|
||||
tcheck(t, err, "wait for threading")
|
||||
|
Reference in New Issue
Block a user