Files
mailgoserver/internal/db/queries.go
T
2026-08-12 12:56:22 +01:00

143 lines
4.6 KiB
Go

package db
import (
"database/sql"
"errors"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
)
func equalFold(a, b string) bool { return strings.EqualFold(a, b) }
func domainPart(address string) string {
i := strings.LastIndex(address, "@")
if i < 0 {
return ""
}
return strings.ToLower(address[i+1:])
}
// bcryptCost is pinned to 12 to match Python's bcrypt.gensalt() default, since Go's
// bcrypt.DefaultCost is 10 and would otherwise silently produce weaker hashes.
const bcryptCost = 12
// HashPassword mirrors models.hash_password.
func HashPassword(password string) (string, error) {
b, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
if err != nil {
return "", err
}
return string(b), nil
}
// CheckPassword mirrors models.check_password.
func CheckPassword(password, hash string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}
// GetSenderByEmail mirrors models.get_sender_by_email: case-insensitive match against
// the lower-cased stored email, active senders only.
func (d *DB) GetSenderByEmail(email string) (*Sender, error) {
row := d.QueryRow(`SELECT id, email, password_hash, domain_id, can_send_as_domain, is_active, created_at, store_message_content
FROM esrv_senders WHERE lower(email) = lower(?) AND is_active = 1`, email)
var s Sender
var createdAt string
if err := row.Scan(&s.ID, &s.Email, &s.PasswordHash, &s.DomainID, &s.CanSendAsDomain, &s.IsActive, &createdAt, &s.StoreMessageContent); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
s.CreatedAt, _ = parseTime(createdAt)
return &s, nil
}
const domainColumns = `id, domain_name, is_active, created_at, verification_token, is_verified, verified_at`
// scanDomain scans a row selected with domainColumns, in that order.
func scanDomain(row *sql.Row) (*Domain, error) {
var dom Domain
var createdAt string
var verifiedAt sql.NullString
if err := row.Scan(&dom.ID, &dom.DomainName, &dom.IsActive, &createdAt, &dom.VerificationToken, &dom.IsVerified, &verifiedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
dom.CreatedAt, _ = parseTime(createdAt)
if verifiedAt.Valid {
t, _ := parseTime(verifiedAt.String)
dom.VerifiedAt = &t
}
return &dom, nil
}
// GetDomainByName mirrors models.get_domain_by_name.
func (d *DB) GetDomainByName(name string) (*Domain, error) {
row := d.QueryRow(`SELECT `+domainColumns+` FROM esrv_domains
WHERE lower(domain_name) = lower(?) AND is_active = 1`, name)
return scanDomain(row)
}
// GetWhitelistedIP mirrors models.get_whitelisted_ip. domainName == "" means no domain
// filter, matching the Python default parameter.
func (d *DB) GetWhitelistedIP(ipAddress, domainName string) (*WhitelistedIP, error) {
var row *sql.Row
if domainName == "" {
row = d.QueryRow(`SELECT id, ip_address, domain_id, is_active, created_at, store_message_content
FROM esrv_whitelisted_ips WHERE ip_address = ? AND is_active = 1`, ipAddress)
} else {
dom, err := d.GetDomainByName(domainName)
if err != nil {
return nil, err
}
if dom == nil {
return nil, nil
}
row = d.QueryRow(`SELECT id, ip_address, domain_id, is_active, created_at, store_message_content
FROM esrv_whitelisted_ips WHERE ip_address = ? AND is_active = 1 AND domain_id = ?`, ipAddress, dom.ID)
}
var w WhitelistedIP
var createdAt string
if err := row.Scan(&w.ID, &w.IPAddress, &w.DomainID, &w.IsActive, &createdAt, &w.StoreMessageContent); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
w.CreatedAt, _ = parseTime(createdAt)
return &w, nil
}
// CanSendForDomain mirrors WhitelistedIP.can_send_for_domain. Not called from the live
// auth path (models.py's own equivalent isn't either) — kept for interface parity.
func (w WhitelistedIP) CanSendForDomain(d *DB, domainName string) (bool, error) {
if !w.IsActive {
return false, nil
}
dom, err := d.GetDomainByName(domainName)
if err != nil || dom == nil {
return false, err
}
return w.DomainID == dom.ID, nil
}
// LogAuthAttempt mirrors models.log_auth_attempt.
func (d *DB) LogAuthAttempt(authType, identifier, ipAddress string, success bool, message string) error {
_, err := d.Exec(`INSERT INTO esrv_auth_logs (auth_type, identifier, ip_address, success, message)
VALUES (?, ?, ?, ?, ?)`, authType, identifier, ipAddress, success, message)
return err
}
func parseTime(s string) (time.Time, error) {
for _, layout := range []string{"2006-01-02 15:04:05.999999999-07:00", "2006-01-02 15:04:05", time.RFC3339} {
if t, err := time.Parse(layout, s); err == nil {
return t, nil
}
}
return time.Time{}, errors.New("unparseable time: " + s)
}