143 lines
4.6 KiB
Go
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)
|
|
}
|