Files
mailgosend/internal/db/relay.go
T
2026-05-25 14:30:41 +00:00

177 lines
5.1 KiB
Go

package db
import (
"context"
"net"
"strings"
"ghb.freebede.com/nahakubuilder/mailgosend/internal/models"
)
// ---- Relay send-as (per user) ----
// GetRelaySendAs returns all allowed sender patterns for a relay user.
func (d *DB) GetRelaySendAs(ctx context.Context, userID int64) ([]*models.RelaySendAs, error) {
rows, err := d.db.QueryContext(ctx,
"SELECT id, user_id, pattern, created_at FROM relay_send_as WHERE user_id=? ORDER BY id", userID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*models.RelaySendAs
for rows.Next() {
r := &models.RelaySendAs{}
if err := rows.Scan(&r.ID, &r.UserID, &r.Pattern, &r.CreatedAt); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
// AddRelaySendAs adds a sender pattern for a relay user. Duplicate patterns are ignored.
func (d *DB) AddRelaySendAs(ctx context.Context, userID int64, pattern string) error {
_, err := d.db.ExecContext(ctx,
"INSERT OR IGNORE INTO relay_send_as (user_id, pattern) VALUES (?,?)", userID, pattern)
return err
}
// DeleteRelaySendAs removes a sender pattern. userID is verified to prevent cross-user deletion.
func (d *DB) DeleteRelaySendAs(ctx context.Context, id, userID int64) error {
_, err := d.db.ExecContext(ctx,
"DELETE FROM relay_send_as WHERE id=? AND user_id=?", id, userID)
return err
}
// IsRelaySenderAllowed returns true when the relay user is permitted to send as fromEmail.
// Own email is always allowed. Otherwise checks relay_send_as patterns.
func (d *DB) IsRelaySenderAllowed(ctx context.Context, userID int64, fromEmail string) (bool, error) {
user, err := d.GetUserByID(ctx, userID)
if err != nil {
return false, err
}
if user != nil && strings.EqualFold(user.Email, fromEmail) {
return true, nil
}
rows, err := d.db.QueryContext(ctx,
"SELECT pattern FROM relay_send_as WHERE user_id=?", userID)
if err != nil {
return false, err
}
defer rows.Close()
for rows.Next() {
var pattern string
if err := rows.Scan(&pattern); err != nil {
return false, err
}
if matchSenderPattern(pattern, fromEmail) {
return true, nil
}
}
return false, rows.Err()
}
// ---- Relay IP rules (per domain) ----
// GetRelayIPRules returns all IP relay rules for a domain.
func (d *DB) GetRelayIPRules(ctx context.Context, domainID int64) ([]*models.RelayIPRule, error) {
rows, err := d.db.QueryContext(ctx, `
SELECT id, domain_id, cidr, sender_pattern, description, created_at
FROM relay_ip_rules WHERE domain_id=? ORDER BY id`, domainID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*models.RelayIPRule
for rows.Next() {
r := &models.RelayIPRule{}
if err := rows.Scan(&r.ID, &r.DomainID, &r.CIDR, &r.SenderPattern, &r.Description, &r.CreatedAt); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
// AddRelayIPRule adds an IP relay rule for a domain.
func (d *DB) AddRelayIPRule(ctx context.Context, domainID int64, cidr, senderPattern, description string) error {
_, err := d.db.ExecContext(ctx, `
INSERT INTO relay_ip_rules (domain_id, cidr, sender_pattern, description)
VALUES (?,?,?,?)`, domainID, cidr, senderPattern, description)
return err
}
// DeleteRelayIPRule removes an IP relay rule. domainID is verified.
func (d *DB) DeleteRelayIPRule(ctx context.Context, id, domainID int64) error {
_, err := d.db.ExecContext(ctx,
"DELETE FROM relay_ip_rules WHERE id=? AND domain_id=?", id, domainID)
return err
}
// CheckIPRelay returns true when the client IP is authorized to send as senderEmail
// via any active IP relay rule across all enabled domains.
func (d *DB) CheckIPRelay(ctx context.Context, clientIP, senderEmail string) (bool, error) {
rows, err := d.db.QueryContext(ctx, `
SELECT r.cidr, r.sender_pattern
FROM relay_ip_rules r
JOIN domains d ON d.id = r.domain_id
WHERE d.enabled = 1`)
if err != nil {
return false, err
}
defer rows.Close()
for rows.Next() {
var cidr, pattern string
if err := rows.Scan(&cidr, &pattern); err != nil {
return false, err
}
if ipInCIDR(cidr, clientIP) && matchSenderPattern(pattern, senderEmail) {
return true, nil
}
}
return false, rows.Err()
}
// ---- Helpers ----
// matchSenderPattern checks if email matches pattern.
// "*@domain.com" matches any address at that domain.
// Anything else is an exact case-insensitive match.
func matchSenderPattern(pattern, email string) bool {
pattern = strings.ToLower(strings.TrimSpace(pattern))
email = strings.ToLower(strings.TrimSpace(email))
if pattern == email {
return true
}
if strings.HasPrefix(pattern, "*@") {
domain := pattern[2:]
at := strings.LastIndex(email, "@")
return at > 0 && email[at+1:] == domain
}
return false
}
// ipInCIDR returns true when ip falls within the cidr range.
// cidr may be a plain IP (treated as single-host) or CIDR notation.
func ipInCIDR(cidr, ip string) bool {
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return false
}
if !strings.Contains(cidr, "/") {
// Plain IP — exact match.
target := net.ParseIP(cidr)
return target != nil && target.Equal(parsedIP)
}
_, network, err := net.ParseCIDR(cidr)
if err != nil {
return false
}
return network.Contains(parsedIP)
}