207 lines
7.8 KiB
Go
207 lines
7.8 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
)
|
|
|
|
const mailboxColumns = `id, email, domain_id, password_hash, is_active, quota_bytes, used_bytes, dek_wrapped, dek_nonce, created_at, created_by, totp_secret, totp_enabled, mfa_exempt`
|
|
|
|
func scanMailbox(row *sql.Row) (*Mailbox, error) {
|
|
var m Mailbox
|
|
var createdAt string
|
|
var createdBy sql.NullInt64
|
|
if err := row.Scan(&m.ID, &m.Email, &m.DomainID, &m.PasswordHash, &m.IsActive, &m.QuotaBytes, &m.UsedBytes, &m.DEKWrapped, &m.DEKNonce, &createdAt, &createdBy, &m.TOTPSecret, &m.TOTPEnabled, &m.MFAExempt); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
m.CreatedAt, _ = parseTime(createdAt)
|
|
if createdBy.Valid {
|
|
m.CreatedBy = &createdBy.Int64
|
|
}
|
|
return &m, nil
|
|
}
|
|
|
|
// MailboxWithDomain joins a Mailbox with its Domain's name, mirroring SenderWithDomain.
|
|
type MailboxWithDomain struct {
|
|
Mailbox
|
|
DomainName string
|
|
}
|
|
|
|
func (d *DB) ListMailboxes() ([]MailboxWithDomain, error) {
|
|
rows, err := d.Query(`SELECT m.id, m.email, m.domain_id, m.password_hash, m.is_active, m.quota_bytes, m.used_bytes, m.dek_wrapped, m.dek_nonce, m.created_at, m.created_by, m.totp_secret, m.totp_enabled, m.mfa_exempt, dm.domain_name
|
|
FROM esrv_mailboxes m JOIN esrv_domains dm ON dm.id = m.domain_id ORDER BY m.email`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []MailboxWithDomain
|
|
for rows.Next() {
|
|
var m MailboxWithDomain
|
|
var createdAt string
|
|
var createdBy sql.NullInt64
|
|
if err := rows.Scan(&m.ID, &m.Email, &m.DomainID, &m.PasswordHash, &m.IsActive, &m.QuotaBytes, &m.UsedBytes, &m.DEKWrapped, &m.DEKNonce, &createdAt, &createdBy, &m.TOTPSecret, &m.TOTPEnabled, &m.MFAExempt, &m.DomainName); err != nil {
|
|
return nil, err
|
|
}
|
|
m.CreatedAt, _ = parseTime(createdAt)
|
|
if createdBy.Valid {
|
|
m.CreatedBy = &createdBy.Int64
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (d *DB) ListMailboxesForDomain(domainID int64) ([]Mailbox, error) {
|
|
rows, err := d.Query(`SELECT `+mailboxColumns+` FROM esrv_mailboxes WHERE domain_id = ? ORDER BY email`, domainID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Mailbox
|
|
for rows.Next() {
|
|
var m Mailbox
|
|
var createdAt string
|
|
var createdBy sql.NullInt64
|
|
if err := rows.Scan(&m.ID, &m.Email, &m.DomainID, &m.PasswordHash, &m.IsActive, &m.QuotaBytes, &m.UsedBytes, &m.DEKWrapped, &m.DEKNonce, &createdAt, &createdBy, &m.TOTPSecret, &m.TOTPEnabled, &m.MFAExempt); err != nil {
|
|
return nil, err
|
|
}
|
|
m.CreatedAt, _ = parseTime(createdAt)
|
|
if createdBy.Valid {
|
|
m.CreatedBy = &createdBy.Int64
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (d *DB) GetMailboxByID(id int64) (*Mailbox, error) {
|
|
row := d.QueryRow(`SELECT `+mailboxColumns+` FROM esrv_mailboxes WHERE id = ?`, id)
|
|
return scanMailbox(row)
|
|
}
|
|
|
|
// GetMailboxByEmail mirrors GetSenderByEmail: case-insensitive, active-only. Used both
|
|
// for SMTP local-delivery recipient resolution and as the base lookup for app-password
|
|
// verification — an alias never resolves here directly, per "login is always the
|
|
// primary mailbox address."
|
|
func (d *DB) GetMailboxByEmail(email string) (*Mailbox, error) {
|
|
row := d.QueryRow(`SELECT `+mailboxColumns+` FROM esrv_mailboxes WHERE lower(email) = lower(?) AND is_active = 1`, email)
|
|
return scanMailbox(row)
|
|
}
|
|
|
|
func (d *DB) MailboxEmailExists(email string, excludeID int64) (bool, error) {
|
|
var n int
|
|
err := d.QueryRow(`SELECT COUNT(*) FROM esrv_mailboxes WHERE lower(email) = lower(?) AND id != ?`, email, excludeID).Scan(&n)
|
|
return n > 0, err
|
|
}
|
|
|
|
// CreateMailbox inserts a new mailbox with its wrapped per-mailbox data encryption key
|
|
// (see internal/mailstore for how wrappedDEK/dekNonce are produced).
|
|
func (d *DB) CreateMailbox(email, passwordHash string, domainID int64, quotaBytes int64, wrappedDEK, dekNonce []byte) (int64, error) {
|
|
res, err := d.Exec(`INSERT INTO esrv_mailboxes (email, domain_id, password_hash, quota_bytes, dek_wrapped, dek_nonce)
|
|
VALUES (?, ?, ?, ?, ?, ?)`, email, domainID, passwordHash, quotaBytes, wrappedDEK, dekNonce)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
func (d *DB) SetMailboxActive(id int64, active bool) error {
|
|
_, err := d.Exec(`UPDATE esrv_mailboxes SET is_active = ? WHERE id = ?`, active, id)
|
|
return err
|
|
}
|
|
|
|
// SetMailboxMFAExempt overrides [Auth] enforce_mailbox_mfa off for this mailbox
|
|
// specifically, even if its domain isn't exempt.
|
|
func (d *DB) SetMailboxMFAExempt(id int64, exempt bool) error {
|
|
_, err := d.Exec(`UPDATE esrv_mailboxes SET mfa_exempt = ? WHERE id = ?`, exempt, id)
|
|
return err
|
|
}
|
|
|
|
func (d *DB) SetMailboxQuota(id int64, quotaBytes int64) error {
|
|
_, err := d.Exec(`UPDATE esrv_mailboxes SET quota_bytes = ? WHERE id = ?`, quotaBytes, id)
|
|
return err
|
|
}
|
|
|
|
func (d *DB) SetMailboxPasswordHash(id int64, passwordHash string) error {
|
|
_, err := d.Exec(`UPDATE esrv_mailboxes SET password_hash = ? WHERE id = ?`, passwordHash, id)
|
|
return err
|
|
}
|
|
|
|
func (d *DB) SetMailboxTOTPSecret(id int64, secret string, enabled bool) error {
|
|
_, err := d.Exec(`UPDATE esrv_mailboxes SET totp_secret = ?, totp_enabled = ? WHERE id = ?`, secret, enabled, id)
|
|
return err
|
|
}
|
|
|
|
func (d *DB) DisableMailboxTOTP(id int64) error {
|
|
_, err := d.Exec(`UPDATE esrv_mailboxes SET totp_secret = '', totp_enabled = 0 WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
// ResetMailboxMFA clears every second factor a mailbox owner has enrolled — TOTP and
|
|
// every registered passkey — e.g. after a lost device, or (under enforce_mailbox_mfa)
|
|
// to let an admin get them unstuck without needing a domain/mailbox exemption. Distinct
|
|
// from DisableMailboxTOTP (TOTP only, self-service from the webmail portal): this is
|
|
// the admin-management action taken from the Mailboxes list on someone else's account.
|
|
func (d *DB) ResetMailboxMFA(id int64) error {
|
|
tx, err := d.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
if _, err := tx.Exec(`UPDATE esrv_mailboxes SET totp_secret = '', totp_enabled = 0 WHERE id = ?`, id); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(`DELETE FROM esrv_mailbox_webauthn_credentials WHERE mailbox_id = ?`, id); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// AddMailboxUsedBytes adjusts the cached running total by delta (positive on store,
|
|
// negative on delete) in a single statement, avoiding a read-modify-write race.
|
|
func (d *DB) AddMailboxUsedBytes(id int64, delta int64) error {
|
|
_, err := d.Exec(`UPDATE esrv_mailboxes SET used_bytes = used_bytes + ? WHERE id = ?`, delta, id)
|
|
return err
|
|
}
|
|
|
|
func (d *DB) GetDomainDefaultQuota(domainID int64) (int64, error) {
|
|
var n int64
|
|
err := d.QueryRow(`SELECT default_mailbox_quota_bytes FROM esrv_domains WHERE id = ?`, domainID).Scan(&n)
|
|
return n, err
|
|
}
|
|
|
|
func (d *DB) SetDomainDefaultQuota(domainID int64, bytes int64) error {
|
|
_, err := d.Exec(`UPDATE esrv_domains SET default_mailbox_quota_bytes = ? WHERE id = ?`, bytes, domainID)
|
|
return err
|
|
}
|
|
|
|
// RemoveMailboxCascade hard-deletes a mailbox and its app passwords / any remaining
|
|
// message rows. Callers should delete each message's on-disk ciphertext via
|
|
// mailstore.DeleteMessage first (see ListMessageUIDsForMailbox) — the message-row
|
|
// DELETE here is just a safety net for any that weren't individually cleaned up.
|
|
func (d *DB) RemoveMailboxCascade(id int64) error {
|
|
tx, err := d.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
for _, stmt := range []string{
|
|
`DELETE FROM esrv_mailbox_app_passwords WHERE mailbox_id = ?`,
|
|
`DELETE FROM esrv_mailbox_aliases WHERE mailbox_id = ?`,
|
|
`DELETE FROM esrv_mailbox_allowblock WHERE mailbox_id = ?`,
|
|
`DELETE FROM esrv_mailbox_filter_rules WHERE mailbox_id = ?`,
|
|
`DELETE FROM esrv_mailbox_sessions WHERE mailbox_id = ?`,
|
|
`DELETE FROM esrv_mailbox_webauthn_credentials WHERE mailbox_id = ?`,
|
|
`DELETE FROM esrv_mailbox_messages WHERE mailbox_id = ?`,
|
|
`DELETE FROM esrv_mailboxes WHERE id = ?`,
|
|
} {
|
|
if _, err := tx.Exec(stmt, id); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|