@stacksjs/mail 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Get the path to the mail binary for a given platform/arch.
3
+ * Checks the package's bin/ directory first, then falls back to zig-out/.
4
+ */
5
+ export declare function getBinaryPath(platform?: Platform, arch?: Arch): string | null;
6
+ /**
7
+ * Get the path to the Linux binary for deployment to EC2.
8
+ * This is the primary function used by the deploy script.
9
+ */
10
+ export declare function getLinuxBinaryPath(arch?: Arch): string | null;
11
+ /**
12
+ * Get the path to the Zig project source for building from source on the server.
13
+ */
14
+ export declare function getSourcePath(): string;
15
+ /**
16
+ * Build the mail binary for a specific target.
17
+ * Returns the path to the built binary.
18
+ */
19
+ export declare function buildForTarget(target?: string, optimize?: string): string | null;
20
+ /**
21
+ * Convert a MailServerConfig to environment variables.
22
+ */
23
+ export declare function configToEnv(config: MailServerConfig): Record<string, string>;
24
+ export declare function createMailAdminClient(options: MailAdminClientOptions): MailAdminClient;
25
+ /**
26
+ * Root of the sibling Zig mail package.
27
+ */
28
+ export declare const ZIG_PROJECT_ROOT: unknown;
29
+ /**
30
+ * Directory containing pre-built binaries within this package
31
+ */
32
+ export declare const BIN_DIR: unknown;
33
+ /**
34
+ * Default SMTP server ports
35
+ */
36
+ export declare const PORTS: {
37
+ smtp: 25;
38
+ smtps: 465;
39
+ submission: 587;
40
+ imap: 143;
41
+ imaps: 993;
42
+ pop3: 110;
43
+ pop3s: 995
44
+ };
45
+ export declare interface MailServerConfig {
46
+ host?: string
47
+ port?: number
48
+ hostname?: string
49
+ enableTls?: boolean
50
+ tlsCert?: string
51
+ tlsKey?: string
52
+ enableAuth?: boolean
53
+ dbPath?: string
54
+ maxConnections?: number
55
+ maxMessageSize?: number
56
+ maxRecipients?: number
57
+ rateLimitPerIp?: number
58
+ rateLimitPerUser?: number
59
+ logLevel?: 'debug' | 'info' | 'warn' | 'error'
60
+ enableJsonLogging?: boolean
61
+ mailboxPath?: string
62
+ backupPath?: string
63
+ profile?: 'development' | 'staging' | 'production'
64
+ }
65
+ export declare interface MailAdminClientOptions {
66
+ baseUrl: string
67
+ username?: string
68
+ password?: string
69
+ token?: string
70
+ fetch?: typeof fetch
71
+ }
72
+ export declare interface MailboxUser {
73
+ id?: number | string
74
+ username: string
75
+ email: string
76
+ quota_mb?: number
77
+ used_mb?: number
78
+ enabled?: boolean
79
+ created_at?: string
80
+ last_login?: string
81
+ }
82
+ export declare interface CreateMailboxInput {
83
+ username: string
84
+ email: string
85
+ password: string
86
+ quota_mb?: number
87
+ }
88
+ export declare interface MailServerStats {
89
+ messages_received?: number
90
+ messages_sent?: number
91
+ messages_queued?: number
92
+ connections_active?: number
93
+ [key: string]: unknown
94
+ }
95
+ export declare interface MailServerHealth {
96
+ status: 'healthy' | 'degraded' | 'unhealthy'
97
+ version?: string
98
+ uptime?: number
99
+ components?: Record<string, { status: string, message?: string }>
100
+ }
101
+ declare type Platform = 'linux' | 'macos' | 'windows';
102
+ declare type Arch = 'x86_64' | 'aarch64';
103
+ export declare class MailAdminError extends Error {
104
+ public status: number;
105
+ public body?: unknown;
106
+ constructor(status: number, message: string, body?: unknown);
107
+ }
108
+ export declare class MailAdminClient {
109
+ constructor(options: MailAdminClientOptions);
110
+ listMailboxes(options?: {
111
+ offset?: number;
112
+ limit?: number }): Promise<{ users: MailboxUser[];
113
+ total: number;
114
+ }>;
115
+ getMailbox(username: string): Promise<MailboxUser>;
116
+ createMailbox(input: CreateMailboxInput): Promise<MailboxUser>;
117
+ updateMailbox(username: string, patch: Partial<Omit<CreateMailboxInput, 'username'>> & { enabled?: boolean }): Promise<MailboxUser>;
118
+ deleteMailbox(username: string): Promise<{ success: boolean, message?: string }>;
119
+ ensureMailbox(input: CreateMailboxInput): Promise<{ mailbox: MailboxUser, created: boolean }>;
120
+ stats(): Promise<MailServerStats>;
121
+ health(): Promise<MailServerHealth>;
122
+ queue(options?: { offset?: number, limit?: number, status?: string }): Promise<unknown>;
123
+ getConfig(): Promise<Record<string, unknown>>;
124
+ updateConfig(patch: Record<string, unknown>): Promise<Record<string, unknown>>;
125
+ ensureDomain(domain: string): Promise<{ domain: string, configured: boolean }>;
126
+ }
package/dist/index.js ADDED
@@ -0,0 +1,274 @@
1
+ // @bun
2
+ // src/index.ts
3
+ import { existsSync } from "fs";
4
+ import { join, resolve } from "path";
5
+ import { execSync } from "child_process";
6
+ var ZIG_PROJECT_ROOT = resolve(import.meta.dir, "..", "..", "zig");
7
+ var BIN_DIR = join(import.meta.dir, "..", "bin");
8
+ var PORTS = {
9
+ smtp: 25,
10
+ smtps: 465,
11
+ submission: 587,
12
+ imap: 143,
13
+ imaps: 993,
14
+ pop3: 110,
15
+ pop3s: 995
16
+ };
17
+ function detectPlatform() {
18
+ switch (process.platform) {
19
+ case "darwin":
20
+ return "macos";
21
+ case "win32":
22
+ return "windows";
23
+ default:
24
+ return "linux";
25
+ }
26
+ }
27
+ function detectArch() {
28
+ return process.arch === "arm64" ? "aarch64" : "x86_64";
29
+ }
30
+ function getBinaryPath(platform, arch) {
31
+ const p = platform || detectPlatform();
32
+ const a = arch || detectArch();
33
+ const label = `${a}-${p}`;
34
+ const packageBinary = join(BIN_DIR, `mail-${label}`);
35
+ if (existsSync(packageBinary))
36
+ return packageBinary;
37
+ const zigOutPaths = [
38
+ join(ZIG_PROJECT_ROOT, "zig-out", "bin", label, `mail-${label}`),
39
+ join(ZIG_PROJECT_ROOT, "zig-out", "bin", `mail-${label}`)
40
+ ];
41
+ if (p === detectPlatform() && a === detectArch()) {
42
+ zigOutPaths.push(join(ZIG_PROJECT_ROOT, "zig-out", "bin", "mail"));
43
+ }
44
+ for (const path of zigOutPaths) {
45
+ if (existsSync(path))
46
+ return path;
47
+ }
48
+ return null;
49
+ }
50
+ function getLinuxBinaryPath(arch = "x86_64") {
51
+ return getBinaryPath("linux", arch);
52
+ }
53
+ function getSourcePath() {
54
+ return ZIG_PROJECT_ROOT;
55
+ }
56
+ function buildForTarget(target = "x86_64-linux-gnu", optimize = "ReleaseFast") {
57
+ try {
58
+ console.log(`Building mail for ${target}...`);
59
+ execSync(`zig build -Doptimize=${optimize} -Dtarget=${target}`, {
60
+ cwd: ZIG_PROJECT_ROOT,
61
+ stdio: "inherit"
62
+ });
63
+ const parts = target.split("-");
64
+ const arch = parts[0];
65
+ const os = parts[1];
66
+ const label = `${arch}-${os}`;
67
+ const possiblePaths = [
68
+ join(ZIG_PROJECT_ROOT, "zig-out", "bin", label, `mail-${label}`),
69
+ join(ZIG_PROJECT_ROOT, "zig-out", "bin", `mail-${label}`),
70
+ join(ZIG_PROJECT_ROOT, "zig-out", "bin", "mail")
71
+ ];
72
+ for (const path of possiblePaths) {
73
+ if (existsSync(path))
74
+ return path;
75
+ }
76
+ return null;
77
+ } catch (error) {
78
+ console.error(`Failed to build for ${target}:`, error);
79
+ return null;
80
+ }
81
+ }
82
+ function configToEnv(config) {
83
+ const env = {};
84
+ if (config.host)
85
+ env.SMTP_HOST = config.host;
86
+ if (config.port)
87
+ env.SMTP_PORT = String(config.port);
88
+ if (config.hostname)
89
+ env.SMTP_HOSTNAME = config.hostname;
90
+ if (config.enableTls !== undefined)
91
+ env.SMTP_ENABLE_TLS = String(config.enableTls);
92
+ if (config.tlsCert)
93
+ env.SMTP_TLS_CERT = config.tlsCert;
94
+ if (config.tlsKey)
95
+ env.SMTP_TLS_KEY = config.tlsKey;
96
+ if (config.enableAuth !== undefined)
97
+ env.SMTP_ENABLE_AUTH = String(config.enableAuth);
98
+ if (config.dbPath)
99
+ env.SMTP_DB_PATH = config.dbPath;
100
+ if (config.maxConnections)
101
+ env.SMTP_MAX_CONNECTIONS = String(config.maxConnections);
102
+ if (config.maxMessageSize)
103
+ env.SMTP_MAX_MESSAGE_SIZE = String(config.maxMessageSize);
104
+ if (config.maxRecipients)
105
+ env.SMTP_MAX_RECIPIENTS = String(config.maxRecipients);
106
+ if (config.rateLimitPerIp)
107
+ env.SMTP_RATE_LIMIT_PER_IP = String(config.rateLimitPerIp);
108
+ if (config.rateLimitPerUser)
109
+ env.SMTP_RATE_LIMIT_PER_USER = String(config.rateLimitPerUser);
110
+ if (config.logLevel)
111
+ env.SMTP_LOG_LEVEL = config.logLevel;
112
+ if (config.enableJsonLogging !== undefined)
113
+ env.SMTP_ENABLE_JSON_LOGGING = String(config.enableJsonLogging);
114
+ if (config.mailboxPath)
115
+ env.SMTP_MAILBOX_PATH = config.mailboxPath;
116
+ if (config.backupPath)
117
+ env.SMTP_BACKUP_PATH = config.backupPath;
118
+ if (config.profile)
119
+ env.SMTP_PROFILE = config.profile;
120
+ return env;
121
+ }
122
+
123
+ class MailAdminError extends Error {
124
+ status;
125
+ body;
126
+ constructor(status, message, body) {
127
+ super(message);
128
+ this.status = status;
129
+ this.body = body;
130
+ this.name = "MailAdminError";
131
+ }
132
+ }
133
+
134
+ class MailAdminClient {
135
+ baseUrl;
136
+ username;
137
+ password;
138
+ token;
139
+ requestFetch;
140
+ csrfToken;
141
+ constructor(options) {
142
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
143
+ this.username = options.username;
144
+ this.password = options.password;
145
+ this.token = options.token;
146
+ this.requestFetch = options.fetch ?? fetch;
147
+ }
148
+ headers(mutating = false) {
149
+ const headers = new Headers({ Accept: "application/json" });
150
+ if (this.token)
151
+ headers.set("Authorization", `Bearer ${this.token}`);
152
+ else if (this.username && this.password)
153
+ headers.set("Authorization", `Basic ${btoa(`${this.username}:${this.password}`)}`);
154
+ if (mutating)
155
+ headers.set("Content-Type", "application/json");
156
+ if (mutating && this.csrfToken)
157
+ headers.set("X-CSRF-Token", this.csrfToken);
158
+ return headers;
159
+ }
160
+ async csrf() {
161
+ if (this.csrfToken)
162
+ return this.csrfToken;
163
+ const response = await this.requestFetch(`${this.baseUrl}/api/csrf-token`, { headers: this.headers() });
164
+ const body = await this.readBody(response);
165
+ if (!response.ok)
166
+ throw new MailAdminError(response.status, "Unable to acquire mail admin CSRF token", body);
167
+ const record = body;
168
+ const token = String(record.csrf_token ?? record.token ?? "");
169
+ if (!token)
170
+ throw new MailAdminError(response.status, "Mail admin returned an empty CSRF token", body);
171
+ this.csrfToken = token;
172
+ return token;
173
+ }
174
+ async readBody(response) {
175
+ const text = await response.text();
176
+ if (!text)
177
+ return null;
178
+ try {
179
+ return JSON.parse(text);
180
+ } catch {
181
+ return text;
182
+ }
183
+ }
184
+ async request(path, init = {}) {
185
+ const method = String(init.method ?? "GET").toUpperCase();
186
+ const mutating = !["GET", "HEAD"].includes(method);
187
+ if (mutating)
188
+ await this.csrf();
189
+ const response = await this.requestFetch(`${this.baseUrl}${path}`, {
190
+ ...init,
191
+ headers: this.headers(mutating)
192
+ });
193
+ const body = await this.readBody(response);
194
+ if (!response.ok) {
195
+ const message = typeof body === "object" && body !== null && "message" in body ? String(body.message) : `Mail admin request failed with HTTP ${response.status}`;
196
+ throw new MailAdminError(response.status, message, body);
197
+ }
198
+ return body;
199
+ }
200
+ async listMailboxes(options = {}) {
201
+ const query = new URLSearchParams;
202
+ if (options.offset !== undefined)
203
+ query.set("offset", String(options.offset));
204
+ if (options.limit !== undefined)
205
+ query.set("limit", String(options.limit));
206
+ const result = await this.request(`/api/users${query.size ? `?${query}` : ""}`);
207
+ return { users: result.users, total: result.total ?? result.count ?? result.users.length };
208
+ }
209
+ async getMailbox(username) {
210
+ return this.request(`/api/users/${encodeURIComponent(username)}`);
211
+ }
212
+ async createMailbox(input) {
213
+ return this.request("/api/users", { method: "POST", body: JSON.stringify(input) });
214
+ }
215
+ async updateMailbox(username, patch) {
216
+ return this.request(`/api/users/${encodeURIComponent(username)}`, { method: "PUT", body: JSON.stringify(patch) });
217
+ }
218
+ async deleteMailbox(username) {
219
+ return this.request(`/api/users/${encodeURIComponent(username)}`, { method: "DELETE" });
220
+ }
221
+ async ensureMailbox(input) {
222
+ try {
223
+ return { mailbox: await this.getMailbox(input.username), created: false };
224
+ } catch (error) {
225
+ if (!(error instanceof MailAdminError) || error.status !== 404)
226
+ throw error;
227
+ return { mailbox: await this.createMailbox(input), created: true };
228
+ }
229
+ }
230
+ async stats() {
231
+ return this.request("/api/stats");
232
+ }
233
+ async health() {
234
+ return this.request("/health");
235
+ }
236
+ async queue(options = {}) {
237
+ const query = new URLSearchParams(Object.entries(options).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
238
+ return this.request(`/api/queue${query.size ? `?${query}` : ""}`);
239
+ }
240
+ async getConfig() {
241
+ const result = await this.request("/api/config");
242
+ return typeof result.config === "object" && result.config !== null ? result.config : result;
243
+ }
244
+ async updateConfig(patch) {
245
+ return this.request("/api/config", { method: "PUT", body: JSON.stringify(patch) });
246
+ }
247
+ async ensureDomain(domain) {
248
+ const normalized = domain.trim().toLowerCase();
249
+ if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(normalized))
250
+ throw new TypeError(`Invalid mail domain: ${domain}`);
251
+ const config = await this.getConfig();
252
+ const current = Array.isArray(config.extra_local_domains) ? config.extra_local_domains.map(String) : [];
253
+ if (current.includes(normalized))
254
+ return { domain: normalized, configured: false };
255
+ await this.updateConfig({ extra_local_domains: [...current, normalized] });
256
+ return { domain: normalized, configured: true };
257
+ }
258
+ }
259
+ function createMailAdminClient(options) {
260
+ return new MailAdminClient(options);
261
+ }
262
+ export {
263
+ getSourcePath,
264
+ getLinuxBinaryPath,
265
+ getBinaryPath,
266
+ createMailAdminClient,
267
+ configToEnv,
268
+ buildForTarget,
269
+ ZIG_PROJECT_ROOT,
270
+ PORTS,
271
+ MailAdminError,
272
+ MailAdminClient,
273
+ BIN_DIR
274
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@stacksjs/mail",
3
+ "type": "module",
4
+ "version": "0.3.1",
5
+ "description": "Typed runtime and management client for the mail server",
6
+ "author": "Chris Breuer",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/mail-os/mail.git",
11
+ "directory": "packages/ts"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "bun": "./dist/index.js",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./*": {
20
+ "bun": "./dist/*",
21
+ "import": "./dist/*"
22
+ }
23
+ },
24
+ "module": "dist/index.js",
25
+ "types": "dist/index.d.ts",
26
+ "files": [
27
+ "README.md",
28
+ "dist",
29
+ "bin"
30
+ ],
31
+ "scripts": {
32
+ "build": "bun run build:sdk && bun build.ts",
33
+ "build:sdk": "bun build src/index.ts --outdir dist --target bun && bun x @stacksjs/dtsx generate --root src --entrypoints index.ts --outdir dist --exclude '**/*.test.ts' --validate",
34
+ "build:linux-x64": "bun build.ts --target x86_64-linux-gnu",
35
+ "build:linux-arm64": "bun build.ts --target aarch64-linux-gnu",
36
+ "build:all": "bun build.ts --all",
37
+ "prepublishOnly": "bun run build:sdk && bun run build:all"
38
+ },
39
+ "devDependencies": {
40
+ "@stacksjs/dtsx": "^0.11.5"
41
+ },
42
+ "keywords": [
43
+ "smtp",
44
+ "mail",
45
+ "email",
46
+ "server",
47
+ "zig",
48
+ "imap",
49
+ "pop3"
50
+ ]
51
+ }