ReviewOS

also looking at this

mail-os/mail

feat(config): add hosted_domains for multi-domain hosting

#1
Open glennmichael123 wants to merge feat/hosted-domains into main
7 files +153 -42
packages/zig/src/core/config.zigmodified+59-0
Changes to packages/zig/src/core/config.zig
@@ -47,6 +47,10 @@ pub const Config = struct {
4747 rate_limit_cleanup_interval: u64,
4848 max_recipients: usize,
4949 hostname: []const u8,
50 /// Additional domains this server accepts mail for, beyond `hostname`
51 /// and its parent. Populated from TOML `server.hosted_domains` (comma-
52 /// separated string) or env `SMTP_HOSTED_DOMAINS`.
53 hosted_domains: []const []const u8 = &.{},
5054 webhook_url: ?[]const u8,
5155 webhook_enabled: bool,
5256 enable_dnsbl: bool,
@@ -86,6 +90,22 @@ pub const Config = struct {
8690 if (self.webhook_url) |url| allocator.free(url);
8791 if (self.acme_email) |email| allocator.free(email);
8892 if (self.list_unsubscribe_url) |url| allocator.free(url);
93 for (self.hosted_domains) |domain| allocator.free(domain);
94 if (self.hosted_domains.len > 0) allocator.free(self.hosted_domains);
95 }
96
97 /// Returns true if `domain` is one this server accepts mail for:
98 /// `hostname`, the parent of `hostname` (e.g. `mail.X` accepts `X`),
99 /// or any entry in `hosted_domains`. Match is case-insensitive.
100 pub fn isLocalDomain(self: Config, domain: []const u8) bool {
101 if (std.ascii.eqlIgnoreCase(domain, self.hostname)) return true;
102 if (std.mem.indexOfScalar(u8, self.hostname, '.')) |dot_pos| {
103 if (std.ascii.eqlIgnoreCase(domain, self.hostname[dot_pos + 1 ..])) return true;
104 }
105 for (self.hosted_domains) |hosted| {
106 if (std.ascii.eqlIgnoreCase(domain, hosted)) return true;
107 }
108 return false;
89109 }
90110
91111 /// Validates the configuration and returns detailed error messages
@@ -265,6 +285,31 @@ pub fn loadConfig(allocator: std.mem.Allocator, cli_args: args.Args) !Config {
265285 return cfg;
266286}
267287
288/// Parse a comma-separated list of domain names into an owned slice of
289/// allocator-owned strings. Empty entries and surrounding whitespace are
290/// trimmed; an empty input yields a zero-length slice.
291fn parseDomainList(allocator: std.mem.Allocator, raw: []const u8) ![]const []const u8 {
292 var list = std.ArrayList([]const u8).empty;
293 errdefer {
294 for (list.items) |item| allocator.free(item);
295 list.deinit(allocator);
296 }
297 var it = std.mem.splitScalar(u8, raw, ',');
298 while (it.next()) |part| {
299 const trimmed = std.mem.trim(u8, part, " \t\r\n");
300 if (trimmed.len == 0) continue;
301 const dup = try allocator.dupe(u8, trimmed);
302 errdefer allocator.free(dup);
303 try list.append(allocator, dup);
304 }
305 return list.toOwnedSlice(allocator);
306}
307
308fn freeDomainList(allocator: std.mem.Allocator, list: []const []const u8) void {
309 for (list) |entry| allocator.free(entry);
310 if (list.len > 0) allocator.free(list);
311}
312
268313/// Determine which configuration profile to use
269314fn determineProfile() config_profiles.Profile {
270315 if (env.get("SMTP_PROFILE")) |profile_str| {
@@ -304,6 +349,7 @@ fn loadDefaultsFromProfile(allocator: std.mem.Allocator, profile: config_profile
304349 .rate_limit_cleanup_interval = @as(u64, profile_config.rate_limit_window_seconds) * 60, // Convert to seconds
305350 .max_recipients = @intCast(profile_config.max_recipients),
306351 .hostname = try allocator.dupe(u8, "localhost"),
352 .hosted_domains = try allocator.alloc([]const u8, 0),
307353 .webhook_url = null,
308354 .webhook_enabled = false, // Only enable when URL is provided via env var
309355 .enable_dnsbl = false, // Not in profile config yet
@@ -348,6 +394,13 @@ fn applyEnvironmentVariables(allocator: std.mem.Allocator, cfg: *Config) !void {
348394 cfg.hostname = try allocator.dupe(u8, value);
349395 }
350396
397 // SMTP_HOSTED_DOMAINS — comma-separated list of additional hosted domains
398 if (env.get("SMTP_HOSTED_DOMAINS")) |value| {
399 const parsed = try parseDomainList(allocator, value);
400 freeDomainList(allocator, cfg.hosted_domains);
401 cfg.hosted_domains = parsed;
402 }
403
351404 // SMTP_MAX_CONNECTIONS
352405 if (env.get("SMTP_MAX_CONNECTIONS")) |value| {
353406 cfg.max_connections = std.fmt.parseInt(usize, value, 10) catch cfg.max_connections;
@@ -569,6 +622,12 @@ fn applyConfigFile(allocator: std.mem.Allocator, cfg: *Config, path: []const u8)
569622 allocator.free(cfg.hostname);
570623 cfg.hostname = try allocator.dupe(u8, value);
571624 }
625 // Comma-separated string until the TOML parser supports arrays.
626 if (server.getString("hosted_domains")) |value| {
627 const parsed = try parseDomainList(allocator, value);
628 freeDomainList(allocator, cfg.hosted_domains);
629 cfg.hosted_domains = parsed;
630 }
572631 if (server.getInt("max_connections")) |value| {
573632 cfg.max_connections = @intCast(value);
574633 }