177 lines
5.1 KiB
Go
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)
|
|
}
|